diff --git a/acceptance/tests/modules/install/already_installed.rb b/acceptance/tests/modules/install/already_installed.rb
new file mode 100644
index 000000000..70abc8506
--- /dev/null
+++ b/acceptance/tests/modules/install/already_installed.rb
@@ -0,0 +1,63 @@
+begin test_name "puppet module install (already installed)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules/nginx',
+ ]: ensure => directory;
+ '/etc/puppet/modules/nginx/metadata.json':
+ content => '{
+ "name": "pmtacceptance/nginx",
+ "version": "0.0.1",
+ "source": "",
+ "author": "pmtacceptance",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+
+step "Try to install a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+step "Try to install a specific version of a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx --version 1.x"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (v1.x)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+step "Install a module that is already installed (with --force)"
+on master, puppet("module install pmtacceptance-nginx --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/already_installed_elsewhere.rb b/acceptance/tests/modules/install/already_installed_elsewhere.rb
new file mode 100644
index 000000000..eee1a5f1c
--- /dev/null
+++ b/acceptance/tests/modules/install/already_installed_elsewhere.rb
@@ -0,0 +1,66 @@
+begin test_name "puppet module install (already installed elsewhere)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/nginx',
+ ]: ensure => directory;
+ '/usr/share/puppet/modules/nginx/metadata.json':
+ content => '{
+ "name": "pmtacceptance/nginx",
+ "version": "0.0.1",
+ "source": "",
+ "author": "pmtacceptance",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+
+step "Try to install a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/nginx ]'
+
+step "Try to install a specific version of a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx --version 1.x"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (v1.x)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/nginx ]'
+
+step "Install a module that is already installed (with --force)"
+on master, puppet("module install pmtacceptance-nginx --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/already_installed_with_local_changes.rb b/acceptance/tests/modules/install/already_installed_with_local_changes.rb
new file mode 100644
index 000000000..55b46c477
--- /dev/null
+++ b/acceptance/tests/modules/install/already_installed_with_local_changes.rb
@@ -0,0 +1,70 @@
+begin test_name "puppet module install (already installed with local changes)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules/nginx',
+ ]: ensure => directory;
+ '/etc/puppet/modules/nginx/metadata.json':
+ content => '{
+ "name": "pmtacceptance/nginx",
+ "version": "0.0.1",
+ "source": "",
+ "author": "pmtacceptance",
+ "license": "MIT",
+ "checksums": {
+ "README": "2a3adc3b053ef1004df0a02cefbae31f"
+ },
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/nginx/README':
+ content => 'Nginx module';
+}
+PP
+
+step "Try to install a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Installed module has had changes made locally
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+step "Try to install a specific version of a module that is already installed"
+on master, puppet("module install pmtacceptance-nginx --version 1.x"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (v1.x)
+ STDERR> Module 'pmtacceptance-nginx' (v0.0.1) is already installed
+ STDERR> Installed module has had changes made locally
+ STDERR> Use `puppet module upgrade` to install a different version
+ STDERR> Use `puppet module install --force` to re-install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+step "Install a module that is already installed (with --force)"
+on master, puppet("module install pmtacceptance-nginx --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/force_ignores_dependencies.rb b/acceptance/tests/modules/install/force_ignores_dependencies.rb
new file mode 100644
index 000000000..a8feea158
--- /dev/null
+++ b/acceptance/tests/modules/install/force_ignores_dependencies.rb
@@ -0,0 +1,40 @@
+begin test_name "puppet module install (force ignores dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Try to install an unsatisfiable module"
+on master, puppet("module install pmtacceptance-php"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-php' (latest: v0.0.2)
+ STDERR> No version of 'pmtacceptance-php' will satisfy dependencies
+ STDERR> You specified 'pmtacceptance-php' (latest: v0.0.2),
+ STDERR> which depends on 'pmtacceptance-apache' (v0.0.1),
+ STDERR> which depends on 'pmtacceptance-php' (v0.0.1)
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/php ]'
+on master, '[ ! -d /etc/puppet/modules/apache ]'
+
+step "Install an unsatisfiable module with force"
+on master, puppet("module install pmtacceptance-php --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-php (\e[0;36mv0.0.2\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/php ]'
+on master, '[ ! -d /etc/puppet/modules/apache ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/ignoring_dependencies.rb b/acceptance/tests/modules/install/ignoring_dependencies.rb
new file mode 100644
index 000000000..791383354
--- /dev/null
+++ b/acceptance/tests/modules/install/ignoring_dependencies.rb
@@ -0,0 +1,24 @@
+begin test_name "puppet module install (ignoring dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install a module, but ignore dependencies"
+on master, puppet("module install pmtacceptance-java --ignore-dependencies") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.7.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/java ]'
+on master, '[ ! -d /etc/puppet/modules/stdlib ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/nonexistent_directory.rb b/acceptance/tests/modules/install/nonexistent_directory.rb
new file mode 100644
index 000000000..6789a86c3
--- /dev/null
+++ b/acceptance/tests/modules/install/nonexistent_directory.rb
@@ -0,0 +1,40 @@
+begin test_name "puppet module install (nonexistent directory)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/tmp/modules',
+ ]: ensure => absent, recurse => true, force => true;
+}
+PP
+
+step "Try to install a module to a non-existent directory"
+on master, puppet("module install pmtacceptance-nginx --target-dir /tmp/modules"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /tmp/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Directory /tmp/modules does not exist\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/nginx ]'
+
+step "Try to install a module to a non-existent implicit directory"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest)
+ STDERR> Directory /etc/puppet/modules does not exist\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': ensure => directory }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_cycles.rb b/acceptance/tests/modules/install/with_cycles.rb
new file mode 100644
index 000000000..d7d27bd04
--- /dev/null
+++ b/acceptance/tests/modules/install/with_cycles.rb
@@ -0,0 +1,32 @@
+begin test_name "puppet module install (with cycles)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install a module with cycles"
+on master, puppet("module install pmtacceptance-php --version 0.0.1") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-php (\e[0;36mv0.0.1\e[0m)
+ └── pmtacceptance-apache (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-apache (\e[0;36mv0.0.1\e[0m)
+ └── pmtacceptance-php (\e[0;36mv0.0.1\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_dependencies.rb b/acceptance/tests/modules/install/with_dependencies.rb
new file mode 100644
index 000000000..f0d87f6b7
--- /dev/null
+++ b/acceptance/tests/modules/install/with_dependencies.rb
@@ -0,0 +1,25 @@
+begin test_name "puppet module install (with dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install a module with dependencies"
+on master, puppet("module install pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.7.1\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/java ]'
+on master, '[ -d /etc/puppet/modules/stdlib ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_existing_module_directory.rb b/acceptance/tests/modules/install/with_existing_module_directory.rb
new file mode 100644
index 000000000..a21fbd1b6
--- /dev/null
+++ b/acceptance/tests/modules/install/with_existing_module_directory.rb
@@ -0,0 +1,96 @@
+begin test_name "puppet module install (with existing module directory)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules/nginx',
+ '/etc/puppet/modules/apache',
+ ]: ensure => directory;
+ '/etc/puppet/modules/nginx/metadata.json':
+ content => '{
+ "name": "notpmtacceptance/nginx",
+ "version": "0.0.3",
+ "source": "",
+ "author": "notpmtacceptance",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ [
+ '/etc/puppet/modules/nginx/extra.json',
+ '/etc/puppet/modules/apache/extra.json',
+ ]: content => '';
+}
+PP
+
+step "Try to install an module with a name collision"
+on master, puppet("module install pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-nginx' (latest: v0.0.1)
+ STDERR> Installation would overwrite /etc/puppet/modules/nginx
+ STDERR> Currently, 'notpmtacceptance-nginx' (v0.0.3) is installed to that directory
+ STDERR> Use `puppet module install --dir
` to install modules elsewhere
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ -f /etc/puppet/modules/nginx/extra.json ]'
+
+step "Try to install an module with a path collision"
+on master, puppet("module install pmtacceptance-apache"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-apache' (latest: v0.0.1)
+ STDERR> Installation would overwrite /etc/puppet/modules/apache
+ STDERR> Use `puppet module install --dir ` to install modules elsewhere
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ -f /etc/puppet/modules/apache/extra.json ]'
+
+step "Try to install an module with a dependency that has collides"
+on master, puppet("module install pmtacceptance-php --version 0.0.1"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-php' (v0.0.1)
+ STDERR> Dependency 'pmtacceptance-apache' (v0.0.1) would overwrite /etc/puppet/modules/apache
+ STDERR> Use `puppet module install --dir ` to install modules elsewhere
+ STDERR> Use `puppet module install --ignore-dependencies` to install only this module\e[0m
+ OUTPUT
+end
+on master, '[ -f /etc/puppet/modules/apache/extra.json ]'
+
+step "Install an module with a name collision by using --force"
+on master, puppet("module install pmtacceptance-nginx --force"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ ! -f /etc/puppet/modules/nginx/extra.json ]'
+
+step "Install an module with a name collision by using --force"
+on master, puppet("module install pmtacceptance-apache --force"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-apache (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ ! -f /etc/puppet/modules/apache/extra.json ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_necessary_upgrade.rb b/acceptance/tests/modules/install/with_necessary_upgrade.rb
new file mode 100644
index 000000000..453dbf147
--- /dev/null
+++ b/acceptance/tests/modules/install/with_necessary_upgrade.rb
@@ -0,0 +1,55 @@
+begin test_name "puppet module install (with necessary dependency upgrade)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install an older module version"
+on master, puppet("module install pmtacceptance-java --version 1.6.0") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list --tree') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+
+step "Install a module that requires the older module dependency be upgraded"
+on master, puppet("module install pmtacceptance-apollo") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.7.1\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_no_dependencies.rb b/acceptance/tests/modules/install/with_no_dependencies.rb
new file mode 100644
index 000000000..ca2aa1d3a
--- /dev/null
+++ b/acceptance/tests/modules/install/with_no_dependencies.rb
@@ -0,0 +1,23 @@
+begin test_name "puppet module install (with no dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install a module with no dependencies"
+on master, puppet("module install pmtacceptance-nginx") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-nginx (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/nginx ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_unnecessary_upgrade.rb b/acceptance/tests/modules/install/with_unnecessary_upgrade.rb
new file mode 100644
index 000000000..532f1f012
--- /dev/null
+++ b/acceptance/tests/modules/install/with_unnecessary_upgrade.rb
@@ -0,0 +1,54 @@
+begin test_name "puppet module install (with unnecessary dependency upgrade)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+
+step "Install an older module version"
+on master, puppet("module install pmtacceptance-java --version 1.7.0") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+
+step "Install a module that depends on a dependency that could be upgraded, but already satisfies constraints"
+on master, puppet("module install pmtacceptance-apollo") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+
+on master, puppet('module list') do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/install/with_unsatisfied_constraints.rb b/acceptance/tests/modules/install/with_unsatisfied_constraints.rb
new file mode 100644
index 000000000..16ddd1c34
--- /dev/null
+++ b/acceptance/tests/modules/install/with_unsatisfied_constraints.rb
@@ -0,0 +1,97 @@
+begin test_name "puppet module install (with unsatisfied constraints)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules/crakorn',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "pmtacceptance/stdlib", "version_requirement": "1.x" }
+ ]
+ }';
+}
+PP
+
+step "Try to install a module that has an unsatisfiable dependency"
+on master, puppet("module install pmtacceptance-git"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-git' (latest: v0.0.1)
+ STDERR> No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ STDERR> 'jimmy-crakorn' (v0.0.1) requires 'pmtacceptance-stdlib' (v1.x)
+ STDERR> 'pmtacceptance-git' (v0.0.1) requires 'pmtacceptance-stdlib' (>= 2.0.0)
+ STDERR> Use `puppet module install --ignore-dependencies` to install only this module\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/git ]'
+
+step "Install the module with an unsatisfiable dependency"
+on master, puppet("module install pmtacceptance-git --ignore-dependencies") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-git (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/git ]'
+
+step "Try to install a specific version of the unsatisfiable dependency"
+on master, puppet("module install pmtacceptance-stdlib --version 1.x"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-stdlib' (v1.x)
+ STDERR> No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ STDERR> You specified 'pmtacceptance-stdlib' (v1.x)
+ STDERR> 'jimmy-crakorn' (v0.0.1) requires 'pmtacceptance-stdlib' (v1.x)
+ STDERR> 'pmtacceptance-git' (v0.0.1) requires 'pmtacceptance-stdlib' (>= 2.0.0)
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/stdlib ]'
+
+step "Try to install any version of the unsatisfiable dependency"
+on master, puppet("module install pmtacceptance-stdlib"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to install into /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not install module 'pmtacceptance-stdlib' (best: v1.0.0)
+ STDERR> No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ STDERR> You specified 'pmtacceptance-stdlib' (best: v1.0.0)
+ STDERR> 'jimmy-crakorn' (v0.0.1) requires 'pmtacceptance-stdlib' (v1.x)
+ STDERR> 'pmtacceptance-git' (v0.0.1) requires 'pmtacceptance-stdlib' (>= 2.0.0)
+ STDERR> Use `puppet module install --force` to install this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/stdlib ]'
+
+step "Install the unsatisfiable dependency with --force"
+on master, puppet("module install pmtacceptance-stdlib --force") do
+ assert_output <<-OUTPUT
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/stdlib ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { '/etc/puppet/modules': recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_circular_dependencies.rb b/acceptance/tests/modules/list/with_circular_dependencies.rb
new file mode 100644
index 000000000..499b0446b
--- /dev/null
+++ b/acceptance/tests/modules/list/with_circular_dependencies.rb
@@ -0,0 +1,69 @@
+begin test_name "puppet module list (with circular dependencies)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/appleseed',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crakorn',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/usr/share/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/appleseed", "version_requirement": "1.1.0" }
+ ]
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /usr/share/puppet/modules/crakorn ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.4.0\e[0m) [/usr/share/puppet/modules]
+/usr/share/puppet/modules
+└─┬ jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+ └── jimmy-appleseed (\e[0;36mv1.1.0\e[0m) [/etc/puppet/modules]
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_installed_modules.rb b/acceptance/tests/modules/list/with_installed_modules.rb
new file mode 100644
index 000000000..1014c2f62
--- /dev/null
+++ b/acceptance/tests/modules/list/with_installed_modules.rb
@@ -0,0 +1,96 @@
+begin test_name "puppet module list (with installed modules)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ '/etc/puppet/modules/appleseed',
+ '/etc/puppet/modules/thelock',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crick',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+ '/etc/puppet/modules/thelock/metadata.json':
+ content => '{
+ "name": "jimmy/thelock",
+ "version": "1.0.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/appleseed", "version_requirement": "1.x" }
+ ]
+ }';
+ '/usr/share/puppet/modules/crick/metadata.json':
+ content => '{
+ "name": "jimmy/crick",
+ "version": "1.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.x" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /etc/puppet/modules/thelock ]'
+on master, '[ -d /usr/share/puppet/modules/crick ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+├── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+├── jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+└── jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crick (\e[0;36mv1.0.1\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+ └─┬ jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+/usr/share/puppet/modules
+└─┬ jimmy-crick (\e[0;36mv1.0.1\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.4.0\e[0m) [/etc/puppet/modules]
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_invalid_dependencies.rb b/acceptance/tests/modules/list/with_invalid_dependencies.rb
new file mode 100644
index 000000000..93f5da8f4
--- /dev/null
+++ b/acceptance/tests/modules/list/with_invalid_dependencies.rb
@@ -0,0 +1,102 @@
+begin test_name "puppet module list (with invalid dependencies)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/appleseed',
+ '/etc/puppet/modules/crakorn',
+ '/etc/puppet/modules/thelock',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crick',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.3.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.x" }
+ ]
+ }';
+ '/etc/puppet/modules/thelock/metadata.json':
+ content => '{
+ "name": "jimmy/thelock",
+ "version": "1.0.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/appleseed", "version_requirement": "1.x" }
+ ]
+ }';
+ '/usr/share/puppet/modules/crick/metadata.json':
+ content => '{
+ "name": "jimmy/crick",
+ "version": "1.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.x" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/thelock ]'
+on master, '[ -d /usr/share/puppet/modules/crick ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal <<-STDERR, stderr
+\e[1;31mWarning: Module 'jimmy-crakorn' (v0.3.0) fails to meet some dependencies:
+ 'jimmy-crick' (v1.0.1) requires 'jimmy-crakorn' (v0.4.x)\e[0m
+STDERR
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+├── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+├── jimmy-crakorn (\e[0;36mv0.3.0\e[0m) \e[0;31minvalid\e[0m
+└── jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crick (\e[0;36mv1.0.1\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal <<-STDERR, stderr
+\e[1;31mWarning: Module 'jimmy-crakorn' (v0.3.0) fails to meet some dependencies:
+ 'jimmy-crick' (v1.0.1) requires 'jimmy-crakorn' (v0.4.x)\e[0m
+STDERR
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+ └─┬ jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.3.0\e[0m)
+/usr/share/puppet/modules
+└─┬ jimmy-crick (\e[0;36mv1.0.1\e[0m)
+ └── jimmy-crakorn (\e[0;36mv0.3.0\e[0m) [/etc/puppet/modules] \e[0;31minvalid\e[0m
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_missing_dependencies.rb b/acceptance/tests/modules/list/with_missing_dependencies.rb
new file mode 100644
index 000000000..fdf029961
--- /dev/null
+++ b/acceptance/tests/modules/list/with_missing_dependencies.rb
@@ -0,0 +1,98 @@
+begin test_name "puppet module list (with missing dependencies)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/appleseed',
+ '/etc/puppet/modules/thelock',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crick',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+ '/etc/puppet/modules/thelock/metadata.json':
+ content => '{
+ "name": "jimmy/thelock",
+ "version": "1.0.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/appleseed", "version_requirement": "1.x" },
+ { "name": "jimmy/sprinkles", "version_requirement": "2.x" }
+ ]
+ }';
+ '/usr/share/puppet/modules/crick/metadata.json':
+ content => '{
+ "name": "jimmy/crick",
+ "version": "1.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.x" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /etc/puppet/modules/thelock ]'
+on master, '[ -d /usr/share/puppet/modules/crick ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal <<-STDERR, stderr
+\e[1;31mWarning: Missing dependency 'jimmy-crakorn':
+ 'jimmy-appleseed' (v1.1.0) requires 'jimmy-crakorn' (v0.4.0)
+ 'jimmy-crick' (v1.0.1) requires 'jimmy-crakorn' (v0.4.x)\e[0m
+\e[1;31mWarning: Missing dependency 'jimmy-sprinkles':
+ 'jimmy-thelock' (v1.0.0) requires 'jimmy-sprinkles' (v2.x)\e[0m
+STDERR
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+├── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+└── jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crick (\e[0;36mv1.0.1\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal <<-STDERR, stderr
+\e[1;31mWarning: Missing dependency 'jimmy-crakorn':
+ 'jimmy-appleseed' (v1.1.0) requires 'jimmy-crakorn' (v0.4.0)
+ 'jimmy-crick' (v1.0.1) requires 'jimmy-crakorn' (v0.4.x)\e[0m
+\e[1;31mWarning: Missing dependency 'jimmy-sprinkles':
+ 'jimmy-thelock' (v1.0.0) requires 'jimmy-sprinkles' (v2.x)\e[0m
+STDERR
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+ ├── \e[0;41mUNMET DEPENDENCY\e[0m jimmy-sprinkles (\e[0;36mv2.x\e[0m)
+ └─┬ jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+ └── \e[0;41mUNMET DEPENDENCY\e[0m jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+/usr/share/puppet/modules
+└─┬ jimmy-crick (\e[0;36mv1.0.1\e[0m)
+ └── \e[0;41mUNMET DEPENDENCY\e[0m jimmy-crakorn (\e[0;36mv0.4.x\e[0m)
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/with_repeated_dependencies.rb b/acceptance/tests/modules/list/with_repeated_dependencies.rb
new file mode 100644
index 000000000..50dc8b78d
--- /dev/null
+++ b/acceptance/tests/modules/list/with_repeated_dependencies.rb
@@ -0,0 +1,113 @@
+begin test_name "puppet module list (with repeated dependencies)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ '/etc/puppet/modules/steward',
+ '/etc/puppet/modules/appleseed',
+ '/etc/puppet/modules/thelock',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crick',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/steward", "version_requirement": ">= 0.0.0" }
+ ]
+ }';
+ '/etc/puppet/modules/steward/metadata.json':
+ content => '{
+ "name": "jimmy/steward",
+ "version": "0.9.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+ '/etc/puppet/modules/thelock/metadata.json':
+ content => '{
+ "name": "jimmy/thelock",
+ "version": "1.0.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": ">= 0.0.0" },
+ { "name": "jimmy/appleseed", "version_requirement": "1.x" }
+ ]
+ }';
+ '/usr/share/puppet/modules/crick/metadata.json':
+ content => '{
+ "name": "jimmy/crick",
+ "version": "1.0.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.x" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/steward ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+on master, '[ -d /etc/puppet/modules/thelock ]'
+on master, '[ -d /usr/share/puppet/modules/crick ]'
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+├── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+├── jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+├── jimmy-steward (\e[0;36mv0.9.0\e[0m)
+└── jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+/usr/share/puppet/modules
+└── jimmy-crick (\e[0;36mv1.0.1\e[0m)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list --tree') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules
+└─┬ jimmy-thelock (\e[0;36mv1.0.0\e[0m)
+ ├─┬ jimmy-crakorn (\e[0;36mv0.4.0\e[0m)
+ │ └── jimmy-steward (\e[0;36mv0.9.0\e[0m)
+ └── jimmy-appleseed (\e[0;36mv1.1.0\e[0m)
+/usr/share/puppet/modules
+└─┬ jimmy-crick (\e[0;36mv1.0.1\e[0m)
+ └─┬ jimmy-crakorn (\e[0;36mv0.4.0\e[0m) [/etc/puppet/modules]
+ └── jimmy-steward (\e[0;36mv0.9.0\e[0m) [/etc/puppet/modules]
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/list/without_installed_modules.rb b/acceptance/tests/modules/list/without_installed_modules.rb
new file mode 100644
index 000000000..6ec3ed33b
--- /dev/null
+++ b/acceptance/tests/modules/list/without_installed_modules.rb
@@ -0,0 +1,36 @@
+begin test_name "puppet module list (without installed modules)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/usr/share/puppet/modules',
+ ]: ensure => directory,
+ recurse => true,
+ purge => true,
+ force => true;
+}
+PP
+
+step "List the installed modules"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules (no modules installed)
+/usr/share/puppet/modules (no modules installed)
+STDOUT
+end
+
+step "List the installed modules as a dependency tree"
+on master, puppet('module list') do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+/etc/puppet/modules (no modules installed)
+/usr/share/puppet/modules (no modules installed)
+STDOUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/search/by_description.rb b/acceptance/tests/modules/search/by_description.rb
new file mode 100644
index 000000000..a1acfb8a4
--- /dev/null
+++ b/acceptance/tests/modules/search/by_description.rb
@@ -0,0 +1,28 @@
+begin test_name 'puppet module search should do substring matches on description'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step 'Search for a module by description'
+on master, puppet("module search dummy") do
+ assert_equal '', stderr
+ # FIXME: The Forge does not presently match against description.
+# assert_equal <<-STDOUT, stdout
+# Searching http://forge.puppetlabs.com ...
+# NAME DESCRIPTION AUTHOR KEYWORDS
+# pmtacceptance-nginx This is a dummy nginx mo... @pmtacceptance nginx
+# pmtacceptance-thin This is a dummy thin mod... @pmtacceptance ruby thin
+# pmtacceptance-apollo This is a dummy apollo m... @pmtacceptance stomp apollo
+# pmtacceptance-java This is a dummy java mod... @pmtacceptance java
+# pmtacceptance-stdlib This is a dummy stdlib m... @pmtacceptance stdlib libs
+# pmtacceptance-git This is a dummy git modu... @pmtacceptance git dvcs
+# pmtacceptance-apache This is a dummy apache m... @pmtacceptance apache php
+# pmtacceptance-php This is a dummy php modu... @pmtacceptance apache php
+# pmtacceptance-geordi This is a module that do... @pmtacceptance star trek
+# STDOUT
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/by_keyword.rb b/acceptance/tests/modules/search/by_keyword.rb
new file mode 100644
index 000000000..93323d8d4
--- /dev/null
+++ b/acceptance/tests/modules/search/by_keyword.rb
@@ -0,0 +1,29 @@
+begin test_name 'puppet module search should do exact keyword matches'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step 'Search for a module by exact keyword'
+on master, puppet("module search github") do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+NAME DESCRIPTION AUTHOR KEYWORDS
+pmtacceptance-git This is a dummy git module... @pmtacceptance git \e[0;32mgithub\e[0m
+STDOUT
+end
+
+# FIXME: The Forge presently matches partial keywords.
+# step 'Search for a module by partial keyword'
+# on master, puppet("module search hub") do
+# assert_equal '', stderr
+# assert_equal <<-STDOUT, stdout
+# Searching http://forge.puppetlabs.com ...
+# No results found for 'hub'.
+# STDOUT
+# end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/by_module_name.rb b/acceptance/tests/modules/search/by_module_name.rb
new file mode 100644
index 000000000..be528e99f
--- /dev/null
+++ b/acceptance/tests/modules/search/by_module_name.rb
@@ -0,0 +1,40 @@
+begin test_name 'puppet module search should do substring matches on module name'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step 'Search for modules by partial name'
+on master, puppet("module search geordi") do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+NAME DESCRIPTION AUTHOR KEYWORDS
+pmtacceptance-\e[0;32mgeordi\e[0m This is a module that do... @pmtacceptance star trek
+STDOUT
+end
+
+# FIXME: The Forge does not presently support matches by dashed full name.
+# step 'Search for modules by partial full name (dashed)'
+# on master, puppet("module search tance-ge") do
+# assert_equal '', stderr
+# assert_equal <<-STDOUT, stdout
+# Searching http://forge.puppetlabs.com ...
+# NAME DESCRIPTION AUTHOR KEYWORDS
+# pmtacceptance-geordi This is a module that do... @pmtacceptance star trek
+# STDOUT
+# end
+
+step 'Search for modules by partial full name (slashed)'
+on master, puppet("module search tance/ge") do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+NAME DESCRIPTION AUTHOR KEYWORDS
+pmtaccep\e[0;32mtance-ge\e[0mordi This is a module that do... @pmtacceptance star trek
+STDOUT
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/communication_error.rb b/acceptance/tests/modules/search/communication_error.rb
new file mode 100644
index 000000000..8fb396e26
--- /dev/null
+++ b/acceptance/tests/modules/search/communication_error.rb
@@ -0,0 +1,20 @@
+begin test_name 'puppet module search should print a reasonable message on communication errors'
+
+step 'Stub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '127.0.0.2' }"
+
+step "Search against a non-existent Forge"
+on master, puppet("module search yup"), :acceptable_exit_codes => [1] do
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+STDOUT
+ assert_equal <<-STDERR, stderr
+Error: Could not connect to http://forge.puppetlabs.com
+ There was a network communications problem
+ Check your network connection and try again
+STDERR
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/formatting.rb b/acceptance/tests/modules/search/formatting.rb
new file mode 100644
index 000000000..c196af9b0
--- /dev/null
+++ b/acceptance/tests/modules/search/formatting.rb
@@ -0,0 +1,22 @@
+begin test_name 'puppet module search output should be well structured'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step 'Search results should line up by column'
+on master, puppet("module search apache") do
+ assert_equal('', stderr)
+
+ assert_equal "Searching http://forge.puppetlabs.com ...\n", stdout.lines.first
+ columns = stdout.lines.to_a[1].split(/\s{2}(?=\S)/)
+ pattern = /^#{ columns.map { |c| c.chomp.gsub(/./, '.') }.join(' ') }$/
+
+ stdout.gsub(/\e.*?m/, '').lines.to_a[1..-1].each do |line|
+ assert_match(pattern, line.chomp, 'columns were misaligned')
+ end
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/multiple_search_terms.rb b/acceptance/tests/modules/search/multiple_search_terms.rb
new file mode 100644
index 000000000..7ba88554b
--- /dev/null
+++ b/acceptance/tests/modules/search/multiple_search_terms.rb
@@ -0,0 +1,25 @@
+begin test_name 'puppet module search should handle multiple search terms sensibly'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+# FIXME: The Forge doesn't properly handle multi-term searches.
+# step 'Search for a module by description'
+# on master, puppet("module search 'notice here'") do
+# assert stdout !~ /'notice here'/
+# end
+#
+# step 'Search for a module by name'
+# on master, puppet("module search 'ance-geo ance-std'") do
+# assert stdout !~ /'ance-geo ance-std'/
+# end
+#
+# step 'Search for multiple keywords'
+# on master, puppet("module search 'star trek'") do
+# assert stdout !~ /'star trek'/
+# end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/search/no_results.rb b/acceptance/tests/modules/search/no_results.rb
new file mode 100644
index 000000000..7d8ee887a
--- /dev/null
+++ b/acceptance/tests/modules/search/no_results.rb
@@ -0,0 +1,18 @@
+begin test_name 'puppet module search should print a reasonable message for no results'
+
+step 'Stub http://forge.puppetlabs.com'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+
+step "Search for a module that doesn't exist"
+on master, puppet("module search module_not_appearing_in_this_forge") do
+ assert_equal '', stderr
+ assert_equal <<-STDOUT, stdout
+Searching http://forge.puppetlabs.com ...
+No results found for 'module_not_appearing_in_this_forge'.
+STDOUT
+end
+
+ensure step 'Unstub http://forge.puppetlabs.com'
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+end
diff --git a/acceptance/tests/modules/uninstall/using_directory_name.rb b/acceptance/tests/modules/uninstall/using_directory_name.rb
new file mode 100644
index 000000000..63e94e0c1
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/using_directory_name.rb
@@ -0,0 +1,47 @@
+begin test_name "puppet module uninstall (using directory name)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/apache',
+ '/etc/puppet/modules/crakorn',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/apache ]'
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+
+step "Try to uninstall the module apache"
+on master, puppet('module uninstall apache') do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'apache' ...
+ Removed 'apache' from /etc/puppet/modules
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/apache ]'
+
+step "Try to uninstall the module crakorn"
+on master, puppet('module uninstall crakorn'), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'crakorn' ...
+ STDERR> \e[1;31mError: Could not uninstall module 'crakorn'
+ STDERR> Module 'crakorn' is not installed
+ STDERR> You may have meant `puppet module uninstall jimmy-crakorn`\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/uninstall/using_version_filter.rb b/acceptance/tests/modules/uninstall/using_version_filter.rb
new file mode 100644
index 000000000..b779556f5
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/using_version_filter.rb
@@ -0,0 +1,59 @@
+begin test_name "puppet module uninstall (with module installed)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ '/usr/share/puppet',
+ '/usr/share/puppet/modules',
+ '/usr/share/puppet/modules/crakorn',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/usr/share/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.5.1",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /usr/share/puppet/modules/crakorn ]'
+
+step "Uninstall jimmy-crakorn version 0.5.x"
+on master, puppet('module uninstall jimmy-crakorn --version 0.5.x') do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'jimmy-crakorn' (\e[0;36mv0.5.x\e[0m) ...
+ Removed 'jimmy-crakorn' (\e[0;36mv0.5.1\e[0m) from /usr/share/puppet/modules
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ ! -d /usr/share/puppet/modules/crakorn ]'
+
+step "Try to uninstall jimmy-crakorn v0.4.0 with `--version 0.5.x`"
+on master, puppet('module uninstall jimmy-crakorn --version 0.5.x'), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'jimmy-crakorn' (\e[0;36mv0.5.x\e[0m) ...
+ STDERR> \e[1;31mError: Could not uninstall module 'jimmy-crakorn' (v0.5.x)
+ STDERR> No installed version of 'jimmy-crakorn' matches (v0.5.x)
+ STDERR> 'jimmy-crakorn' (v0.4.0) is installed in /etc/puppet/modules\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/uninstall/with_active_dependency.rb b/acceptance/tests/modules/uninstall/with_active_dependency.rb
new file mode 100644
index 000000000..1d7e42c20
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/with_active_dependency.rb
@@ -0,0 +1,74 @@
+begin test_name "puppet module uninstall (with active dependency)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ '/etc/puppet/modules/appleseed',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ '/etc/puppet/modules/appleseed/metadata.json':
+ content => '{
+ "name": "jimmy/appleseed",
+ "version": "1.1.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "jimmy/crakorn", "version_requirement": "0.4.0" }
+ ]
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+
+step "Try to uninstall the module jimmy-crakorn"
+on master, puppet('module uninstall jimmy-crakorn'), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'jimmy-crakorn' ...
+ STDERR> \e[1;31mError: Could not uninstall module 'jimmy-crakorn'
+ STDERR> Other installed modules have dependencies on 'jimmy-crakorn' (v0.4.0)
+ STDERR> 'jimmy/appleseed' (v1.1.0) requires 'jimmy-crakorn' (v0.4.0)
+ STDERR> Use `puppet module uninstall --force` to uninstall this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+
+step "Try to uninstall the module jimmy-crakorn with a version range"
+on master, puppet('module uninstall jimmy-crakorn --version 0.x'), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'jimmy-crakorn' (\e[0;36mv0.x\e[0m) ...
+ STDERR> \e[1;31mError: Could not uninstall module 'jimmy-crakorn' (v0.x)
+ STDERR> Other installed modules have dependencies on 'jimmy-crakorn' (v0.4.0)
+ STDERR> 'jimmy/appleseed' (v1.1.0) requires 'jimmy-crakorn' (v0.4.0)
+ STDERR> Use `puppet module uninstall --force` to uninstall this module anyway\e[0m
+ OUTPUT
+end
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+
+step "Uninstall the module jimmy-crakorn forcefully"
+on master, puppet('module uninstall jimmy-crakorn --force') do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'jimmy-crakorn' ...
+ Removed 'jimmy-crakorn' (\e[0;36mv0.4.0\e[0m) from /etc/puppet/modules
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/crakorn ]'
+on master, '[ -d /etc/puppet/modules/appleseed ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/uninstall/with_module_installed.rb b/acceptance/tests/modules/uninstall/with_module_installed.rb
new file mode 100644
index 000000000..374e5225e
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/with_module_installed.rb
@@ -0,0 +1,34 @@
+begin test_name "puppet module uninstall (with module installed)"
+
+step "Setup"
+apply_manifest_on master, <<-PP
+file {
+ [
+ '/etc/puppet/modules',
+ '/etc/puppet/modules/crakorn',
+ ]: ensure => directory;
+ '/etc/puppet/modules/crakorn/metadata.json':
+ content => '{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+ }';
+}
+PP
+on master, '[ -d /etc/puppet/modules/crakorn ]'
+
+step "Uninstall the module jimmy-crakorn"
+on master, puppet('module uninstall jimmy-crakorn') do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'jimmy-crakorn' ...
+ Removed 'jimmy-crakorn' (\e[0;36mv0.4.0\e[0m) from /etc/puppet/modules
+ OUTPUT
+end
+on master, '[ ! -d /etc/puppet/modules/crakorn ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb b/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb
new file mode 100644
index 000000000..37309aa01
--- /dev/null
+++ b/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb
@@ -0,0 +1,43 @@
+begin test_name "puppet module uninstall (with multiple modules installed)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0 --modulepath /etc/puppet/modules")
+on master, puppet("module install pmtacceptance-java --version 1.7.0 --modulepath /usr/share/puppet/modules")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+step "Try to uninstall a module that exists multiple locations in the module path"
+on master, puppet("module uninstall pmtacceptance-java"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to uninstall 'pmtacceptance-java' ...
+ STDERR> \e[1;31mError: Could not uninstall module 'pmtacceptance-java'
+ STDERR> Module 'pmtacceptance-java' appears multiple places in the module path
+ STDERR> 'pmtacceptance-java' (v1.6.0) was found in /etc/puppet/modules
+ STDERR> 'pmtacceptance-java' (v1.7.0) was found in /usr/share/puppet/modules
+ STDERR> Use the `--modulepath` option to limit the search to specific directories\e[0m
+ OUTPUT
+end
+
+step "Uninstall a module that exists multiple locations by restricting the --modulepath"
+on master, puppet("module uninstall pmtacceptance-java --modulepath /etc/puppet/modules") do
+ assert_output <<-OUTPUT
+ Preparing to uninstall 'pmtacceptance-java' ...
+ Removed 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) from /etc/puppet/modules
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/in_a_secondary_directory.rb b/acceptance/tests/modules/upgrade/in_a_secondary_directory.rb
new file mode 100644
index 000000000..d7b9d4df8
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/in_a_secondary_directory.rb
@@ -0,0 +1,32 @@
+begin test_name "puppet module upgrade (in a secondary directory)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0 --target-dir /usr/share/puppet/modules")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules (no modules installed)
+ /usr/share/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+step "Upgrade a module that has a more recent version published"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /usr/share/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /usr/share/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/introducing_new_dependencies.rb b/acceptance/tests/modules/upgrade/introducing_new_dependencies.rb
new file mode 100644
index 000000000..14e924da3
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/introducing_new_dependencies.rb
@@ -0,0 +1,36 @@
+begin test_name "puppet module upgrade (introducing new dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-stdlib --version 1.0.0")
+on master, puppet("module install pmtacceptance-java --version 1.7.0")
+on master, puppet("module install pmtacceptance-postgresql --version 0.0.2")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ ├── pmtacceptance-postgresql (\e[0;36mv0.0.2\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a module to a version that introduces new dependencies"
+on master, puppet("module upgrade pmtacceptance-postgresql") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-postgresql' ...
+ Found 'pmtacceptance-postgresql' (\e[0;36mv0.0.2\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-postgresql (\e[0;36mv0.0.2 -> v1.0.0\e[0m)
+ └── pmtacceptance-geordi (\e[0;36mv0.0.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/not_upgradable.rb b/acceptance/tests/modules/upgrade/not_upgradable.rb
new file mode 100644
index 000000000..dfb8ed5ff
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/not_upgradable.rb
@@ -0,0 +1,82 @@
+begin test_name "puppet module upgrade (not upgradable)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+ file {
+ [
+ '/etc/puppet/modules/nginx',
+ '/etc/puppet/modules/unicorns',
+ ]: ensure => directory;
+ '/etc/puppet/modules/unicorns/metadata.json':
+ content => '{
+ "name": "notpmtacceptance/unicorns",
+ "version": "0.0.3",
+ "source": "",
+ "author": "notpmtacceptance",
+ "license": "MIT",
+ "dependencies": []
+ }';
+ }
+PP
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── nginx (\e[0;36m???\e[0m)
+ ├── notpmtacceptance-unicorns (\e[0;36mv0.0.3\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Try to upgrade a module that is not installed"
+on master, puppet("module upgrade pmtacceptance-nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-nginx' ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-nginx'
+ STDERR> Module 'pmtacceptance-nginx' is not installed
+ STDERR> Use `puppet module install` to install this module\e[0m
+ OUTPUT
+end
+
+step "Try to upgrade a local module"
+on master, puppet("module upgrade nginx"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'nginx' ...
+ STDOUT> Found 'nginx' (\e[0;36m???\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'nginx' (??? -> latest)
+ STDERR> Module 'nginx' does not exist on http://forge.puppetlabs.com\e[0m
+ OUTPUT
+end
+
+step "Try to upgrade a module that doesn't exist"
+on master, puppet("module upgrade notpmtacceptance-unicorns"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'notpmtacceptance-unicorns' ...
+ STDOUT> Found 'notpmtacceptance-unicorns' (\e[0;36mv0.0.3\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'notpmtacceptance-unicorns' (v0.0.3 -> latest)
+ STDERR> Module 'notpmtacceptance-unicorns' does not exist on http://forge.puppetlabs.com\e[0m
+ OUTPUT
+end
+
+step "Try to upgrade an installed module to a version that doesn't exist"
+on master, puppet("module upgrade pmtacceptance-java --version 2.0.0"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.6.0 -> v2.0.0)
+ STDERR> No version matching '2.0.0' exists on http://forge.puppetlabs.com\e[0m
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/that_was_installed_twice.rb b/acceptance/tests/modules/upgrade/that_was_installed_twice.rb
new file mode 100644
index 000000000..c42952634
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/that_was_installed_twice.rb
@@ -0,0 +1,47 @@
+begin test_name "puppet module upgrade (that was installed twice)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0 --modulepath /etc/puppet/modules")
+on master, puppet("module install pmtacceptance-java --version 1.7.0 --modulepath /usr/share/puppet/modules")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ OUTPUT
+end
+
+step "Try to upgrade a module that exists multiple locations in the module path"
+on master, puppet("module upgrade pmtacceptance-java"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java'
+ STDERR> Module 'pmtacceptance-java' appears multiple places in the module path
+ STDERR> 'pmtacceptance-java' (v1.6.0) was found in /etc/puppet/modules
+ STDERR> 'pmtacceptance-java' (v1.7.0) was found in /usr/share/puppet/modules
+ STDERR> Use the `--modulepath` option to limit the search to specific directories\e[0m
+ OUTPUT
+end
+
+step "Upgrade a module that exists multiple locations by restricting the --modulepath"
+on master, puppet("module upgrade pmtacceptance-java --modulepath /etc/puppet/modules") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/to_a_specific_version.rb b/acceptance/tests/modules/upgrade/to_a_specific_version.rb
new file mode 100644
index 000000000..77889d7d3
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/to_a_specific_version.rb
@@ -0,0 +1,44 @@
+begin test_name "puppet module upgrade (to a specific version)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a module to a specific (greater) version"
+on master, puppet("module upgrade pmtacceptance-java --version 1.7.0") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.0\e[0m)
+ OUTPUT
+end
+
+step "Upgrade a module to a specific (lesser) version"
+on master, puppet("module upgrade pmtacceptance-java --version 1.6.0") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.7.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.7.0 -> v1.6.0\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/to_installed_version.rb b/acceptance/tests/modules/upgrade/to_installed_version.rb
new file mode 100644
index 000000000..496749ba1
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/to_installed_version.rb
@@ -0,0 +1,81 @@
+begin test_name "puppet module upgrade (to installed version)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Try to upgrade a module to the current version"
+on master, puppet("module upgrade pmtacceptance-java --version 1.6.x"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.6.0 -> v1.6.x)
+ STDERR> The installed version is already the best fit for the current dependencies
+ STDERR> You specified 'pmtacceptance-java' (v1.6.x)
+ STDERR> Use `puppet module install --force` to re-install this module\e[0m
+ OUTPUT
+end
+
+step "Upgrade a module to the current version with --force"
+on master, puppet("module upgrade pmtacceptance-java --version 1.6.x --force") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.6.0\e[0m)
+ OUTPUT
+end
+
+step "Upgrade to the latest version"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+step "Try to upgrade a module to the latest version with the latest version installed"
+on master, puppet("module upgrade pmtacceptance-java"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.7.1\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.7.1 -> latest: v1.7.1)
+ STDERR> The installed version is already the latest version
+ STDERR> Use `puppet module install --force` to re-install this module\e[0m
+ OUTPUT
+end
+
+step "Upgrade a module to the latest version with --force"
+on master, puppet("module upgrade pmtacceptance-java --force") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.7.1\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.7.1 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_constraints_on_it.rb b/acceptance/tests/modules/upgrade/with_constraints_on_it.rb
new file mode 100644
index 000000000..0a5238acc
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_constraints_on_it.rb
@@ -0,0 +1,48 @@
+begin test_name "puppet module upgrade (with constraints on it)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.7.0")
+on master, puppet("module install pmtacceptance-apollo")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-apollo (\e[0;36mv0.0.1\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.7.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a version-constrained module that has an upgrade"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.7.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.7.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+step "Try to upgrade a version-constrained module that has no upgrade"
+on master, puppet("module upgrade pmtacceptance-stdlib"), :acceptable_exit_codes => [0] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-stdlib' ...
+ STDOUT> Found 'pmtacceptance-stdlib' (\e[0;36mv1.0.0\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-stdlib' (v1.0.0 -> best: v1.0.0)
+ STDERR> The installed version is already the best fit for the current dependencies
+ STDERR> 'pmtacceptance-apollo' (v0.0.1) requires 'pmtacceptance-stdlib' (>= 1.0.0)
+ STDERR> 'pmtacceptance-java' (v1.7.1) requires 'pmtacceptance-stdlib' (v1.0.0)
+ STDERR> Use `puppet module install --force` to re-install this module\e[0m
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_constraints_on_its_dependencies.rb b/acceptance/tests/modules/upgrade/with_constraints_on_its_dependencies.rb
new file mode 100644
index 000000000..03f131d8c
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_constraints_on_its_dependencies.rb
@@ -0,0 +1,90 @@
+begin test_name "puppet module upgrade (with constraints on its dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+apply_manifest_on master, <<-PP
+ file {
+ [
+ '/etc/puppet/modules/unicorns',
+ ]: ensure => directory;
+ '/etc/puppet/modules/unicorns/metadata.json':
+ content => '{
+ "name": "notpmtacceptance/unicorns",
+ "version": "0.0.3",
+ "source": "",
+ "author": "notpmtacceptance",
+ "license": "MIT",
+ "dependencies": [
+ { "name": "pmtacceptance/stdlib", "version_requirement": "0.0.2" }
+ ]
+ }';
+ }
+PP
+on master, puppet("module install pmtacceptance-stdlib --version 0.0.2")
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── notpmtacceptance-unicorns (\e[0;36mv0.0.3\e[0m)
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Try to upgrade a module with constraints on its dependencies that cannot be met"
+on master, puppet("module upgrade pmtacceptance-java"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ STDOUT> Downloading from http://forge.puppetlabs.com ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.6.0 -> latest: v1.7.1)
+ STDERR> No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ STDERR> 'notpmtacceptance-unicorns' (v0.0.3) requires 'pmtacceptance-stdlib' (v0.0.2)
+ STDERR> 'pmtacceptance-java' (v1.7.1) requires 'pmtacceptance-stdlib' (v1.0.0)
+ STDERR> Use `puppet module upgrade --ignore-dependencies` to upgrade only this module\e[0m
+ OUTPUT
+end
+
+step "Relax constraints"
+on master, puppet("module uninstall notpmtacceptance-unicorns")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a single module, ignoring its dependencies"
+on master, puppet("module upgrade pmtacceptance-java --version 1.7.0 --ignore-dependencies") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.0\e[0m)
+ OUTPUT
+end
+
+step "Upgrade a module with constraints on its dependencies that can be met"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.7.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-java (\e[0;36mv1.7.0 -> v1.7.1\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2 -> v1.0.0\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_local_changes.rb b/acceptance/tests/modules/upgrade/with_local_changes.rb
new file mode 100644
index 000000000..3d75a317d
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_local_changes.rb
@@ -0,0 +1,53 @@
+begin test_name "puppet module upgrade (with local changes)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+apply_manifest_on master, <<-PP
+ file {
+ '/etc/puppet/modules/java/README': content => "I CHANGE MY READMES";
+ '/etc/puppet/modules/java/NEWFILE': content => "I don't exist.'";
+ }
+PP
+
+step "Try to upgrade a module with local changes"
+on master, puppet("module upgrade pmtacceptance-java"), :acceptable_exit_codes => [1] do
+ assert_output <<-OUTPUT
+ STDOUT> Preparing to upgrade 'pmtacceptance-java' ...
+ STDOUT> Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ STDERR> \e[1;31mError: Could not upgrade module 'pmtacceptance-java' (v1.6.0 -> latest)
+ STDERR> Installed module has had changes made locally
+ STDERR> Use `puppet module upgrade --force` to upgrade this module anyway\e[0m
+ OUTPUT
+end
+on master, '[[ "$(cat /etc/puppet/modules/java/README)" == "I CHANGE MY READMES" ]]'
+on master, '[ -f /etc/puppet/modules/java/NEWFILE ]'
+
+step "Upgrade a module with local changes with --force"
+on master, puppet("module upgrade pmtacceptance-java --force") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+on master, '[[ "$(cat /etc/puppet/modules/java/README)" != "I CHANGE MY READMES" ]]'
+on master, '[ ! -f /etc/puppet/modules/java/NEWFILE ]'
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_scattered_dependencies.rb b/acceptance/tests/modules/upgrade/with_scattered_dependencies.rb
new file mode 100644
index 000000000..53612d217
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_scattered_dependencies.rb
@@ -0,0 +1,38 @@
+begin test_name "puppet module upgrade (with scattered dependencies)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-stdlib --version 0.0.2 --target-dir /usr/share/puppet/modules")
+on master, puppet("module install pmtacceptance-java --version 1.6.0 --target-dir /etc/puppet/modules --ignore-dependencies")
+on master, puppet("module install pmtacceptance-postgresql --version 0.0.1 --target-dir /etc/puppet/modules --ignore-dependencies")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-postgresql (\e[0;36mv0.0.1\e[0m)
+ /usr/share/puppet/modules
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2\e[0m)
+ OUTPUT
+end
+
+step "Upgrade a module that has a more recent version published"
+on master, puppet("module upgrade pmtacceptance-postgresql --version 0.0.2") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-postgresql' ...
+ Found 'pmtacceptance-postgresql' (\e[0;36mv0.0.1\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └─┬ pmtacceptance-postgresql (\e[0;36mv0.0.1 -> v0.0.2\e[0m)
+ ├─┬ pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.0\e[0m)
+ │ └── pmtacceptance-stdlib (\e[0;36mv0.0.2 -> v1.0.0\e[0m) [/usr/share/puppet/modules]
+ └── pmtacceptance-stdlib (\e[0;36mv0.0.2 -> v1.0.0\e[0m) [/usr/share/puppet/modules]
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/acceptance/tests/modules/upgrade/with_update_available.rb b/acceptance/tests/modules/upgrade/with_update_available.rb
new file mode 100644
index 000000000..0109f81e2
--- /dev/null
+++ b/acceptance/tests/modules/upgrade/with_update_available.rb
@@ -0,0 +1,32 @@
+begin test_name "puppet module upgrade (with update available)"
+
+step 'Setup'
+require 'resolv'; ip = Resolv.getaddress('forge-dev.puppetlabs.com')
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ip => '#{ip}' }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+on master, puppet("module install pmtacceptance-java --version 1.6.0")
+on master, puppet("module list") do
+ assert_output <<-OUTPUT
+ /etc/puppet/modules
+ ├── pmtacceptance-java (\e[0;36mv1.6.0\e[0m)
+ └── pmtacceptance-stdlib (\e[0;36mv1.0.0\e[0m)
+ /usr/share/puppet/modules (no modules installed)
+ OUTPUT
+end
+
+step "Upgrade a module that has a more recent version published"
+on master, puppet("module upgrade pmtacceptance-java") do
+ assert_output <<-OUTPUT
+ Preparing to upgrade 'pmtacceptance-java' ...
+ Found 'pmtacceptance-java' (\e[0;36mv1.6.0\e[0m) in /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Upgrading -- do not interrupt ...
+ /etc/puppet/modules
+ └── pmtacceptance-java (\e[0;36mv1.6.0 -> v1.7.1\e[0m)
+ OUTPUT
+end
+
+ensure step "Teardown"
+apply_manifest_on master, "host { 'forge.puppetlabs.com': ensure => absent }"
+apply_manifest_on master, "file { ['/etc/puppet/modules', '/usr/share/puppet/modules']: ensure => directory, recurse => true, purge => true, force => true }"
+end
diff --git a/lib/puppet/application/module.rb b/lib/puppet/application/module.rb
new file mode 100644
index 000000000..e10766035
--- /dev/null
+++ b/lib/puppet/application/module.rb
@@ -0,0 +1,11 @@
+require 'puppet/application/face_base'
+
+class Puppet::Application::Module < Puppet::Application::FaceBase
+ def setup
+ super
+ if self.render_as.name == :console
+ Puppet::Util::Log.close(:console)
+ Puppet::Util::Log.newdestination(:telly_prototype_console)
+ end
+ end
+end
diff --git a/lib/puppet/face/help/man.erb b/lib/puppet/face/help/man.erb
index 90e27964c..5a3aa2cbe 100644
--- a/lib/puppet/face/help/man.erb
+++ b/lib/puppet/face/help/man.erb
@@ -1,136 +1,136 @@
puppet-<%= face.name %>(8) -- <%= face.summary || "Undocumented subcommand." %>
<%= '=' * (_erbout.length - 1) %>
<% if face.synopsis -%>
SYNOPSIS
--------
<%= face.synopsis %>
<% end
if face.description -%>
DESCRIPTION
-----------
<%= face.description.strip %>
<% end -%>
OPTIONS
-------
Note that any configuration parameter that's valid in the configuration
file is also a valid long argument, although it may or may not be
relevant to the present action. For example, `server` is a valid
configuration parameter, so you can specify `--server ` as
an argument.
See the configuration file documentation at
for the
full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet with
`--genconfig`.
* --mode MODE:
The run mode to use for the current action. Valid modes are `user`, `agent`,
and `master`.
* --render-as FORMAT:
The format in which to render output. The most common formats are `json`,
`s` (string), `yaml`, and `console`, but other options such as `dot` are
sometimes available.
* --verbose:
Whether to log verbosely.
* --debug:
Whether to log debug information.
<% unless face.options.empty?
face.options.sort.each do |name|
option = face.get_option name -%>
<%= "* " + option.optparse.join(" | " ) %>:
<%= option.description.gsub(/^/, ' ') || ' ' + option.summary %>
<% end
end -%>
ACTIONS
-------
<% face.actions.each do |actionname|
action = face.get_action(actionname) -%>
* `<%= action.name.to_s %>` - <%= action.summary %>:
<% if action.synopsis -%>
`SYNOPSIS`
<%= action.synopsis %>
<% end -%>
`DESCRIPTION`
<% if action.description -%>
<%= action.description.gsub(/^/, ' ') %>
<% else -%>
<%= action.summary || "Undocumented action." %>
<% end -%>
<% unique_options = action.options - face.options
unless unique_options.empty? -%>
`OPTIONS`
<% unique_options.sort.each do |name|
option = action.get_option name
- text = option.description || option.summary -%>
+ text = (option.description || option.summary).chomp + "\n" -%>
<%= '<' + option.optparse.join("> | <") + '>' %> -
<%= text.gsub(/^/, ' ') %>
<% end -%>
<% end -%>
<% if action.returns -%>
`RETURNS`
<%= action.returns.gsub(/^/, ' ') %>
<% end
if action.notes -%>
`NOTES`
<%= action.notes.gsub(/^/, ' ') %>
<% end
end
if face.examples or face.actions.any? {|actionname| face.get_action(actionname).examples} -%>
EXAMPLES
--------
<% end
if face.examples -%>
<%= face.examples %>
<% end
face.actions.each do |actionname|
action = face.get_action(actionname)
if action.examples -%>
`<%= action.name.to_s %>`
<%= action.examples.strip %>
<% end
end -%>
<% if face.notes or face.respond_to? :indirection -%>
NOTES
-----
<% if face.notes -%>
<%= face.notes.strip %>
<% end # notes
if face.respond_to? :indirection -%>
This subcommand is an indirector face, which exposes `find`, `search`, `save`,
and `destroy` actions for an indirected subsystem of Puppet. Valid termini for
this face include:
* `<%= face.class.terminus_classes(face.indirection.name).join("`\n* `") %>`
<% end # indirection
end # notes or indirection
unless face.authors.empty? -%>
AUTHOR
------
<%= face.authors.join("\n").gsub(/^/, ' * ') %>
<% end -%>
COPYRIGHT AND LICENSE
---------------------
<%= face.copyright %>
<%= face.license %>
diff --git a/lib/puppet/face/module.rb b/lib/puppet/face/module.rb
new file mode 100644
index 000000000..87c14a8d1
--- /dev/null
+++ b/lib/puppet/face/module.rb
@@ -0,0 +1,17 @@
+require 'puppet/face'
+require 'puppet/module_tool'
+require 'puppet/util/colors'
+
+Puppet::Face.define(:module, '1.0.0') do
+ extend Puppet::Util::Colors
+
+ copyright "Puppet Labs", 2012
+ license "Apache 2 license; see COPYING"
+
+ summary "Creates, installs and searches for modules on the Puppet Forge."
+ description <<-EOT
+ This subcommand can find, install, and manage modules from the Puppet Forge,
+ a repository of user-contributed Puppet code. It can also generate empty
+ modules, and prepare locally developed modules for release on the Forge.
+ EOT
+end
diff --git a/lib/puppet/face/module/build.rb b/lib/puppet/face/module/build.rb
index 227363448..f1a0ef466 100644
--- a/lib/puppet/face/module/build.rb
+++ b/lib/puppet/face/module/build.rb
@@ -1,31 +1,39 @@
Puppet::Face.define(:module, '1.0.0') do
action(:build) do
summary "Build a module release package."
description <<-EOT
- Build a module release archive file by processing the Modulefile in the
- module directory. The release archive file will be stored in the pkg
- directory of the module directory.
+ Prepares a local module for release on the Puppet Forge by building a
+ ready-to-upload archive file. Before using this action, make sure that
+ the module directory's name is in the standard -
+ format.
+
+ This action uses the Modulefile in the module directory to set metadata
+ used by the Forge. See for more
+ about writing modulefiles.
+
+ After being built, the release archive file can be found in the module's
+ `pkg` directory.
EOT
returns "Pathname object representing the path to the release archive."
examples <<-EOT
Build a module release:
$ puppet module build puppetlabs-apache
notice: Building /Users/kelseyhightower/puppetlabs-apache for release
puppetlabs-apache/pkg/puppetlabs-apache-0.0.1.tar.gz
EOT
arguments ""
when_invoked do |path, options|
Puppet::Module::Tool::Applications::Builder.run(path, options)
end
when_rendering :console do |return_value|
# Get the string representation of the Pathname object.
return_value.to_s
end
end
end
diff --git a/lib/puppet/face/module/changes.rb b/lib/puppet/face/module/changes.rb
index 026661107..602e423dc 100644
--- a/lib/puppet/face/module/changes.rb
+++ b/lib/puppet/face/module/changes.rb
@@ -1,38 +1,38 @@
Puppet::Face.define(:module, '1.0.0') do
action(:changes) do
summary "Show modified files of an installed module."
description <<-EOT
- Show files that have been modified after installation of a given module
- by comparing the on-disk md5 checksum of each file against the module's
- metadata.
+ Shows any files in a module that have been modified since it was
+ installed. This action compares the files on disk to the md5 checksums
+ included in the module's metadata.
EOT
returns "Array of strings representing paths of modified files."
examples <<-EOT
Show modified files of an installed module:
$ puppet module changes /etc/puppet/modules/vcsrepo/
warning: 1 files modified
lib/puppet/provider/vcsrepo.rb
EOT
arguments ""
when_invoked do |path, options|
root_path = Puppet::Module::Tool.find_module_root(path)
Puppet::Module::Tool::Applications::Checksummer.run(root_path, options)
end
when_rendering :console do |return_value|
if return_value.empty?
Puppet.notice "No modified files"
else
Puppet.warning "#{return_value.size} files modified"
end
return_value.map do |changed_file|
"#{changed_file}"
end.join("\n")
end
end
end
diff --git a/lib/puppet/face/module/clean.rb b/lib/puppet/face/module/clean.rb
deleted file mode 100644
index 637263057..000000000
--- a/lib/puppet/face/module/clean.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-Puppet::Face.define(:module, '1.0.0') do
- action(:clean) do
- summary "Clean the module download cache."
- description <<-EOT
- Clean the module download cache.
- EOT
-
- returns <<-EOT
- Return a status Hash:
-
- { :status => "success", :msg => "Cleaned module cache." }
- EOT
-
- examples <<-EOT
- Clean the module download cache:
-
- $ puppet module clean
- Cleaned module cache.
- EOT
-
- when_invoked do |options|
- Puppet::Module::Tool::Applications::Cleaner.run(options)
- end
-
- when_rendering :console do |return_value|
- # Print the status message to the console.
- return_value[:msg]
- end
- end
-end
diff --git a/lib/puppet/face/module/generate.rb b/lib/puppet/face/module/generate.rb
index b9dc354bf..8f1622cd6 100644
--- a/lib/puppet/face/module/generate.rb
+++ b/lib/puppet/face/module/generate.rb
@@ -1,40 +1,42 @@
Puppet::Face.define(:module, '1.0.0') do
action(:generate) do
summary "Generate boilerplate for a new module."
description <<-EOT
- Generate boilerplate for a new module by creating a directory
- pre-populated with a directory structure and files recommended for
- Puppet best practices.
+ Generates boilerplate for a new module by creating the directory
+ structure and files recommended for the Puppet community's best practices.
+
+ A module may need additional directories beyond this boilerplate
+ if it provides plugins, files, or templates.
EOT
returns "Array of Pathname objects representing paths of generated files."
examples <<-EOT
Generate a new module in the current directory:
$ puppet module generate puppetlabs-ssh
notice: Generating module at /Users/kelseyhightower/puppetlabs-ssh
puppetlabs-ssh
puppetlabs-ssh/tests
puppetlabs-ssh/tests/init.pp
puppetlabs-ssh/spec
puppetlabs-ssh/spec/spec_helper.rb
puppetlabs-ssh/spec/spec.opts
puppetlabs-ssh/README
puppetlabs-ssh/Modulefile
puppetlabs-ssh/metadata.json
puppetlabs-ssh/manifests
puppetlabs-ssh/manifests/init.pp
EOT
arguments ""
when_invoked do |name, options|
Puppet::Module::Tool::Applications::Generator.run(name, options)
end
when_rendering :console do |return_value|
return_value.map {|f| f.to_s }.join("\n")
end
end
end
diff --git a/lib/puppet/face/module/install.rb b/lib/puppet/face/module/install.rb
index 8f95ff485..c3e0754eb 100644
--- a/lib/puppet/face/module/install.rb
+++ b/lib/puppet/face/module/install.rb
@@ -1,83 +1,173 @@
+# encoding: UTF-8
+
Puppet::Face.define(:module, '1.0.0') do
action(:install) do
summary "Install a module from a repository or release archive."
description <<-EOT
- Install a module from a release archive file on-disk or by downloading
- one from a repository. Unpack the archive into the install directory
- specified by the --install-dir option, which defaults to
- #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).first}
+ Installs a module from the Puppet Forge, from a release archive file
+ on-disk, or from a private Forge-like repository.
+
+ The specified module will be installed into the directory
+ specified with the `--target-dir` option, which defaults to
+ #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).first}.
EOT
returns "Pathname object representing the path to the installed module."
examples <<-EOT
- Install a module from the default repository:
+ Install a module:
+
+ $ puppet module install puppetlabs-vcsrepo
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
- $ puppet module install puppetlabs/vcsrepo
- notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /etc/puppet/modules/vcsrepo
- /etc/puppet/modules/vcsrepo
+ Install a module to a specific environment:
- Install a specific module version from a repository:
+ $ puppet module install puppetlabs-vcsrepo --environment development
+ Preparing to install into /etc/puppet/environments/development/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/environments/development/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
- $ puppet module install puppetlabs/vcsrepo -v 0.0.4
- notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /etc/puppet/modules/vcsrepo
- /etc/puppet/modules/vcsrepo
+ Install a specific module version:
+
+ $ puppet module install puppetlabs-vcsrepo -v 0.0.4
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
Install a module into a specific directory:
- $ puppet module install puppetlabs/vcsrepo --install-dir=/usr/share/puppet/modules
- notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /usr/share/puppet/modules/vcsrepo
- /usr/share/puppet/modules/vcsrepo
+ $ puppet module install puppetlabs-vcsrepo --target-dir=/usr/share/puppet/modules
+ Preparing to install into /usr/share/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /usr/share/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
+
+ Install a module into a specific directory and check for dependencies in other directories:
+
+ $ puppet module install puppetlabs-vcsrepo --target-dir=/usr/share/puppet/modules --modulepath /etc/puppet/modules
+ Preparing to install into /usr/share/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /usr/share/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
Install a module from a release archive:
$ puppet module install puppetlabs-vcsrepo-0.0.4.tar.gz
- notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /etc/puppet/modules/vcsrepo
- /etc/puppet/modules/vcsrepo
+ Preparing to install into /etc/puppet/modules ...
+ Downloading from http://forge.puppetlabs.com ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
+
+ Install a module from a release archive and ignore dependencies:
+
+ $ puppet module install puppetlabs-vcsrepo-0.0.4.tar.gz --ignore-dependencies
+ Preparing to install into /etc/puppet/modules ...
+ Installing -- do not interrupt ...
+ /etc/puppet/modules
+ └── puppetlabs-vcsrepo (v0.0.4)
+
EOT
arguments ""
option "--force", "-f" do
summary "Force overwrite of existing module, if any."
description <<-EOT
Force overwrite of existing module, if any.
EOT
end
- option "--install-dir=", "-i=" do
- default_to { Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).first }
+ option "--target-dir DIR", "-i DIR" do
summary "The directory into which modules are installed."
description <<-EOT
- The directory into which modules are installed, defaults to the first
+ The directory into which modules are installed; defaults to the first
directory in the modulepath.
+
+ Specifying this option will change the installation directory, and
+ will use the existing modulepath when checking for dependencies. If
+ you wish to check a different set of directories for dependencies, you
+ must also use the `--environment` or `--modulepath` options.
EOT
end
- option "--module-repository=", "-r=" do
- default_to { Puppet.settings[:module_repository] }
- summary "Module repository to use."
+ option "--ignore-dependencies" do
+ summary "Do not attempt to install dependencies"
description <<-EOT
- Module repository to use.
+ Do not attempt to install dependencies.
EOT
end
- option "--version=", "-v=" do
+ option "--modulepath MODULEPATH" do
+ default_to { Puppet.settings[:modulepath] }
+ summary "Which directories to look for modules in"
+ description <<-EOT
+ The list of directories to check for modules. When installing a new
+ module, this setting determines where the module tool will look for
+ its dependencies. If the `--target dir` option is not specified, the
+ first directory in the modulepath will also be used as the install
+ directory.
+
+ When installing a module into an environment whose modulepath is
+ specified in puppet.conf, you can use the `--environment` option
+ instead, and its modulepath will be used automatically.
+
+ This setting should be a list of directories separated by the path
+ separator character. (The path separator is `:` on Unix-like platforms
+ and `;` on Windows.)
+ EOT
+ end
+
+ option "--version VER", "-v VER" do
summary "Module version to install."
description <<-EOT
- Module version to install, can be a requirement string, eg '>= 1.0.3',
- defaults to latest version.
+ Module version to install; can be an exact version or a requirement string,
+ eg '>= 1.0.3'. Defaults to latest version.
+ EOT
+ end
+
+ option "--environment NAME" do
+ default_to { "production" }
+ summary "The target environment to install modules into."
+ description <<-EOT
+ The target environment to install modules into. Only applicable if
+ multiple environments (with different modulepaths) have been
+ configured in puppet.conf.
EOT
end
when_invoked do |name, options|
+ sep = File::PATH_SEPARATOR
+ if options[:target_dir]
+ options[:modulepath] = "#{options[:target_dir]}#{sep}#{options[:modulepath]}"
+ end
+
+ Puppet.settings[:modulepath] = options[:modulepath]
+ options[:target_dir] = Puppet.settings[:modulepath].split(sep).first
+
+ Puppet.notice "Preparing to install into #{options[:target_dir]} ..."
Puppet::Module::Tool::Applications::Installer.run(name, options)
end
- when_rendering :console do |return_value|
- # Get the string representation of the Pathname object and print it to
- # the console.
- return_value.to_s
+ when_rendering :console do |return_value, name, options|
+ if return_value[:result] == :failure
+ Puppet.err(return_value[:error][:multiline])
+ exit 1
+ else
+ tree = Puppet::Module::Tool.build_tree(return_value[:installed_modules], return_value[:install_dir])
+ return_value[:install_dir] + "\n" +
+ Puppet::Module::Tool.format_tree(tree)
+ end
end
end
end
diff --git a/lib/puppet/face/module/list.rb b/lib/puppet/face/module/list.rb
index 7162dfe46..e3acbd9ae 100644
--- a/lib/puppet/face/module/list.rb
+++ b/lib/puppet/face/module/list.rb
@@ -1,84 +1,285 @@
+# encoding: UTF-8
+
Puppet::Face.define(:module, '1.0.0') do
action(:list) do
summary "List installed modules"
description <<-HEREDOC
- List puppet modules from a specific environment, specified modulepath or
- default to listing modules in the default modulepath. The output will
- include information about unmet module dependencies based on information
- from module metadata.
- #{Puppet.settings[:modulepath]}
+ Lists the installed puppet modules. By default, this action scans the
+ modulepath from puppet.conf's `[main]` block; use the --modulepath
+ option to change which directories are scanned.
+
+ The output of this action includes information from the module's
+ metadata, including version numbers and unmet module dependencies.
HEREDOC
returns "hash of paths to module objects"
- option "--env ENVIRONMENT" do
+ option "--environment NAME" do
+ default_to { "production" }
summary "Which environments' modules to list"
+ description <<-EOT
+ Which environments' modules to list.
+ EOT
end
option "--modulepath MODULEPATH" do
summary "Which directories to look for modules in"
+ description <<-EOT
+ Which directories to look for modules in; use the system path separator
+ character (`:` on Unix-like systems and `;` on Windows) to specify
+ multiple directories.
+ EOT
+ end
+
+ option "--tree" do
+ summary "Whether to show dependencies as a tree view"
end
examples <<-EOT
List installed modules:
$ puppet module list
/etc/puppet/modules
- bacula (0.0.2)
- /usr/share/puppet/modules
- apache (0.0.3)
- bacula (0.0.1)
+ ├── bodepd-create_resources (v0.0.1)
+ ├── puppetlabs-bacula (v0.0.2)
+ ├── puppetlabs-mysql (v0.0.1)
+ ├── puppetlabs-sqlite (v0.0.1)
+ └── puppetlabs-stdlib (v2.2.1)
+ /usr/share/puppet/modules (no modules installed)
- List installed modules from a specified environment:
+ List installed modules in a tree view:
- $ puppet module list --env 'test'
- Missing dependency `stdlib`:
- `rrd` (0.0.2) requires `puppetlabs/stdlib` (>= 2.2.0)
+ $ puppet module list --tree
+ /etc/puppet/modules
+ └─┬ puppetlabs-bacula (v0.0.2)
+ ├── puppetlabs-stdlib (v2.2.1)
+ ├─┬ puppetlabs-mysql (v0.0.1)
+ │ └── bodepd-create_resources (v0.0.1)
+ └── puppetlabs-sqlite (v0.0.1)
+ /usr/share/puppet/modules (no modules installed)
+
+ List installed modules from a specified environment:
- /tmp/puppet/modules
- rrd (0.0.2)
+ $ puppet module list --environment production
+ /etc/puppet/modules
+ ├── bodepd-create_resources (v0.0.1)
+ ├── puppetlabs-bacula (v0.0.2)
+ ├── puppetlabs-mysql (v0.0.1)
+ ├── puppetlabs-sqlite (v0.0.1)
+ └── puppetlabs-stdlib (v2.2.1)
+ /usr/share/puppet/modules (no modules installed)
List installed modules from a specified modulepath:
- $ puppet module list --modulepath /tmp/facts1:/tmp/facts2
- /tmp/facts1
- stdlib
- /tmp/facts2
- nginx (1.0.0)
+ $ puppet module list --modulepath /usr/share/puppet/modules
+ /usr/share/puppet/modules (no modules installed)
EOT
when_invoked do |options|
Puppet[:modulepath] = options[:modulepath] if options[:modulepath]
- environment = Puppet::Node::Environment.new(options[:env])
+ environment = Puppet::Node::Environment.new(options[:environment])
environment.modules_by_path
end
when_rendering :console do |modules_by_path, options|
output = ''
Puppet[:modulepath] = options[:modulepath] if options[:modulepath]
- environment = Puppet::Node::Environment.new(options[:env])
+ environment = Puppet::Node::Environment.new(options[:production])
- dependency_errors = false
+ error_types = {
+ :non_semantic_version => {
+ :title => "Non semantic version dependency"
+ },
+ :missing => {
+ :title => "Missing dependency"
+ },
+ :version_mismatch => {
+ :title => "Module '%s' (v%s) fails to meet some dependencies:"
+ }
+ }
+ @unmet_deps = {}
+ error_types.each_key do |type|
+ @unmet_deps[type] = Hash.new do |hash, key|
+ hash[key] = { :errors => [], :parent => nil }
+ end
+ end
+
+ # Prepare the unmet dependencies for display on the console.
environment.modules.sort_by {|mod| mod.name}.each do |mod|
- mod.unmet_dependencies.sort_by {|dep| dep[:name]}.each do |dep|
- dependency_errors = true
- $stderr.puts dep[:error]
+ unmet_grouped = Hash.new { |h,k| h[k] = [] }
+ unmet_grouped = mod.unmet_dependencies.inject(unmet_grouped) do |acc, dep|
+ acc[dep[:reason]] << dep
+ acc
+ end
+ unmet_grouped.each do |type, deps|
+ unless deps.empty?
+ unmet_grouped[type].sort_by { |dep| dep[:name] }.each do |dep|
+ dep_name = dep[:name].gsub('/', '-')
+ installed_version = dep[:mod_details][:installed_version]
+ version_constraint = dep[:version_constraint]
+ parent_name = dep[:parent][:name].gsub('/', '-')
+ parent_version = dep[:parent][:version]
+
+ msg = "'#{parent_name}' (#{parent_version})"
+ msg << " requires '#{dep_name}' (#{version_constraint})"
+ @unmet_deps[type][dep[:name]][:errors] << msg
+ @unmet_deps[type][dep[:name]][:parent] = {
+ :name => dep[:parent][:name],
+ :version => parent_version
+ }
+ @unmet_deps[type][dep[:name]][:version] = installed_version
+ end
+ end
+ end
+ end
+
+ # Display unmet dependencies by category.
+ error_display_order = [:non_semantic_version, :version_mismatch, :missing]
+ error_display_order.each do |type|
+ unless @unmet_deps[type].empty?
+ @unmet_deps[type].keys.sort_by {|dep| dep }.each do |dep|
+ name = dep.gsub('/', '-')
+ title = error_types[type][:title]
+ errors = @unmet_deps[type][dep][:errors]
+ version = @unmet_deps[type][dep][:version]
+
+ msg = case type
+ when :version_mismatch
+ title % [name, version] + "\n"
+ when :non_semantic_version
+ title + " '#{name}' (v#{version}):\n"
+ else
+ title + " '#{name}':\n"
+ end
+
+ errors.each { |error_string| msg << " #{error_string}\n" }
+ Puppet.warning msg.chomp
+ end
end
end
- output << "\n" if dependency_errors
+ environment.modulepath.each do |path|
+ modules = modules_by_path[path]
+ no_mods = modules.empty? ? ' (no modules installed)' : ''
+ output << "#{path}#{no_mods}\n"
- modules_by_path.each do |path, modules|
- output << "#{path}\n"
- modules.sort_by {|mod| mod.name }.each do |mod|
- version_string = mod.version ? "(#{mod.version})" : ''
- output << " #{mod.name} #{version_string}\n"
+ if options[:tree]
+ # The modules with fewest things depending on them will be the
+ # parent of the tree. Can't assume to start with 0 dependencies
+ # since dependencies may be cyclical.
+ modules_by_num_requires = modules.sort_by {|m| m.required_by.size}
+ @seen = {}
+ tree = list_build_tree(modules_by_num_requires, [], nil,
+ :label_unmet => true, :path => path, :label_invalid => false)
+ else
+ tree = []
+ modules.sort_by { |mod| mod.forge_name or mod.name }.each do |mod|
+ tree << list_build_node(mod, path, :label_unmet => false,
+ :path => path, :label_invalid => true)
+ end
end
+
+ output << Puppet::Module::Tool.format_tree(tree)
end
+
output
end
+ end
+
+ # Prepare a list of module objects and their dependencies for print in a
+ # tree view.
+ #
+ # Returns an Array of Hashes
+ #
+ # Example:
+ #
+ # [
+ # {
+ # :text => "puppetlabs-bacula (v0.0.2)",
+ # :dependencies=> [
+ # { :text => "puppetlabs-stdlib (v2.2.1)", :dependencies => [] },
+ # {
+ # :text => "puppetlabs-mysql (v1.0.0)"
+ # :dependencies => [
+ # {
+ # :text => "bodepd-create_resources (v0.0.1)",
+ # :dependencies => []
+ # }
+ # ]
+ # },
+ # { :text => "puppetlabs-sqlite (v0.0.1)", :dependencies => [] },
+ # ]
+ # }
+ # ]
+ #
+ # When the above data structure is passed to Puppet::Module::Tool.build_tree
+ # you end up with something like this:
+ #
+ # /etc/puppet/modules
+ # └─┬ puppetlabs-bacula (v0.0.2)
+ # ├── puppetlabs-stdlib (v2.2.1)
+ # ├─┬ puppetlabs-mysql (v1.0.0)
+ # │ └── bodepd-create_resources (v0.0.1)
+ # └── puppetlabs-sqlite (v0.0.1)
+ #
+ def list_build_tree(list, ancestors=[], parent=nil, params={})
+ list.map do |mod|
+ next if @seen[(mod.forge_name or mod.name)]
+ node = list_build_node(mod, parent, params)
+ @seen[(mod.forge_name or mod.name)] = true
+
+ unless ancestors.include?(mod)
+ node[:dependencies] ||= []
+ missing_deps = mod.unmet_dependencies.select do |dep|
+ dep[:reason] == :missing
+ end
+ missing_deps.map do |mis_mod|
+ str = "#{colorize(:bg_red, 'UNMET DEPENDENCY')} #{mis_mod[:name].gsub('/', '-')} "
+ str << "(#{colorize(:cyan, mis_mod[:version_constraint])})"
+ node[:dependencies] << { :text => str }
+ end
+ node[:dependencies] += list_build_tree(mod.dependencies_as_modules,
+ ancestors + [mod], mod, params)
+ end
+
+ node
+ end.compact
+ end
+
+ # Prepare a module object for print in a tree view. Each node in the tree
+ # must be a Hash in the following format:
+ #
+ # { :text => "puppetlabs-mysql (v1.0.0)" }
+ #
+ # The value of a module's :text is affected by three (3) factors: the format
+ # of the tree, it's dependency status, and the location in the modulepath
+ # relative to it's parent.
+ #
+ # Returns a Hash
+ #
+ def list_build_node(mod, parent, params)
+ str = ''
+ str << (mod.forge_name ? mod.forge_name.gsub('/', '-') : mod.name)
+ str << ' (' + colorize(:cyan, mod.version ? "v#{mod.version}" : '???') + ')'
+
+ unless File.dirname(mod.path) == params[:path]
+ str << " [#{File.dirname(mod.path)}]"
+ end
+
+ if @unmet_deps[:version_mismatch].include?(mod.forge_name)
+ if params[:label_invalid]
+ str << ' ' + colorize(:red, 'invalid')
+ elsif parent.respond_to?(:forge_name)
+ unmet_parent = @unmet_deps[:version_mismatch][mod.forge_name][:parent]
+ if (unmet_parent[:name] == parent.forge_name &&
+ unmet_parent[:version] == "v#{parent.version}")
+ str << ' ' + colorize(:red, 'invalid')
+ end
+ end
+ end
+ { :text => str }
end
end
diff --git a/lib/puppet/face/module/search.rb b/lib/puppet/face/module/search.rb
index cec8d9089..0c488082f 100644
--- a/lib/puppet/face/module/search.rb
+++ b/lib/puppet/face/module/search.rb
@@ -1,55 +1,90 @@
+require 'puppet/util/terminal'
+
Puppet::Face.define(:module, '1.0.0') do
action(:search) do
summary "Search a repository for a module."
description <<-EOT
- Search a repository for modules whose names match a specific substring.
+ Searches a repository for modules whose names, descriptions, or keywords
+ match the provided search term.
EOT
returns "Array of module metadata hashes"
examples <<-EOT
Search the default repository for a module:
$ puppet module search puppetlabs
NAME DESCRIPTION AUTHOR KEYWORDS
bacula This is a generic Apache module @puppetlabs backups
EOT
- arguments ""
-
- option "--module-repository=", "-r=" do
- default_to { Puppet.settings[:module_repository] }
- summary "Module repository to use."
- description <<-EOT
- Module repository to use.
- EOT
- end
+ arguments ""
when_invoked do |term, options|
+ server = Puppet.settings[:module_repository].sub(/^(?!https?:\/\/)/, 'http://')
+ Puppet.notice "Searching #{server} ..."
Puppet::Module::Tool::Applications::Searcher.run(term, options)
end
- when_rendering :console do |return_value|
+ when_rendering :console do |results, term, options|
+ return "No results found for '#{term}'." if results.empty?
+
+ padding = ' '
+ headers = {
+ 'full_name' => 'NAME',
+ 'desc' => 'DESCRIPTION',
+ 'author' => 'AUTHOR',
+ 'tag_list' => 'KEYWORDS',
+ }
- FORMAT = "%-10s %-32s %-14s %s\n"
+ min_widths = Hash[ *headers.map { |k,v| [k, v.length] }.flatten ]
+ min_widths['full_name'] = min_widths['author'] = 12
- def header
- FORMAT % ['NAME', 'DESCRIPTION', 'AUTHOR', 'KEYWORDS']
+ min_width = min_widths.inject(0) { |sum,pair| sum += pair.last } + (padding.length * (headers.length - 1))
+
+ terminal_width = [Puppet::Util::Terminal.width, min_width].max
+
+ columns = results.inject(min_widths) do |hash, result|
+ {
+ 'full_name' => [ hash['full_name'], result['full_name'].length ].max,
+ 'desc' => [ hash['desc'], result['desc'].length ].max,
+ 'author' => [ hash['author'], "@#{result['author']}".length ].max,
+ 'tag_list' => [ hash['tag_list'], result['tag_list'].join(' ').length ].max,
+ }
end
- def format_row(name, description, author, tag_list)
- keywords = tag_list.join(' ')
- FORMAT % [name[0..10], description[0..32], "@#{author[0..14]}", keywords]
+ flex_width = terminal_width - columns['full_name'] - columns['author'] - (padding.length * (headers.length - 1))
+ tag_lists = results.map { |r| r['tag_list'] }
+
+ while (columns['tag_list'] > flex_width / 3)
+ longest_tag_list = tag_lists.sort_by { |tl| tl.join(' ').length }.last
+ break if [ [], [term] ].include? longest_tag_list
+ longest_tag_list.delete(longest_tag_list.sort_by { |t| t == term ? -1 : t.length }.last)
+ columns['tag_list'] = tag_lists.map { |tl| tl.join(' ').length }.max
end
- output = ''
- output << header unless return_value.empty?
+ columns['tag_list'] = [
+ flex_width / 3,
+ tag_lists.map { |tl| tl.join(' ').length }.max,
+ ].max
+ columns['desc'] = flex_width - columns['tag_list']
+
+ format = %w{full_name desc author tag_list}.map do |k|
+ "%-#{ [ columns[k], min_widths[k] ].max }s"
+ end.join(padding) + "\n"
- return_value.map do |match|
- output << format_row(match['name'], match['desc'], match['author'], match['tag_list'])
+ highlight = proc do |s|
+ s = s.gsub(term, colorize(:green, term))
+ s = s.gsub(term.gsub('/', '-'), colorize(:green, term.gsub('/', '-'))) if term =~ /\//
+ s
end
- output
+ format % [ headers['full_name'], headers['desc'], headers['author'], headers['tag_list'] ] +
+ results.map do |match|
+ name, desc, author, keywords = %w{full_name desc author tag_list}.map { |k| match[k] }
+ desc = desc[0...(columns['desc'] - 3)] + '...' if desc.length > columns['desc']
+ highlight[format % [ name.sub('/', '-'), desc, "@#{author}", [keywords].flatten.join(' ') ]]
+ end.join
end
end
end
diff --git a/lib/puppet/face/module/uninstall.rb b/lib/puppet/face/module/uninstall.rb
index 802507c58..1effa6c38 100644
--- a/lib/puppet/face/module/uninstall.rb
+++ b/lib/puppet/face/module/uninstall.rb
@@ -1,91 +1,86 @@
Puppet::Face.define(:module, '1.0.0') do
action(:uninstall) do
summary "Uninstall a puppet module."
description <<-EOT
- Uninstall a puppet module from the modulepath or a specific
- target directory which defaults to
- #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).join(', ')}.
+ Uninstalls a puppet module from the modulepath (or a specific
+ target directory).
EOT
returns "Hash of module objects representing uninstalled modules and related errors."
examples <<-EOT
- Uninstall a module from all directories in the modulepath:
+ Uninstall a module:
- $ puppet module uninstall ssh
+ $ puppet module uninstall puppetlabs-ssh
Removed /etc/puppet/modules/ssh (v1.0.0)
Uninstall a module from a specific directory:
- $ puppet module uninstall --modulepath /usr/share/puppet/modules ssh
+ $ puppet module uninstall puppetlabs-ssh --modulepath /usr/share/puppet/modules
Removed /usr/share/puppet/modules/ssh (v1.0.0)
Uninstall a module from a specific environment:
- $ puppet module uninstall --environment development
+ $ puppet module uninstall puppetlabs-ssh --environment development
Removed /etc/puppet/environments/development/modules/ssh (v1.0.0)
-
+
Uninstall a specific version of a module:
- $ puppet module uninstall --version 2.0.0 ssh
+ $ puppet module uninstall puppetlabs-ssh --version 2.0.0
Removed /etc/puppet/modules/ssh (v2.0.0)
EOT
arguments ""
- option "--environment=NAME", "--env=NAME" do
+ option "--force", "-f" do
+ summary "Force uninstall of an installed module."
+ description <<-EOT
+ Force the uninstall of an installed module even if there are local
+ changes or the possibility of causing broken dependencies.
+ EOT
+ end
+
+ option "--environment NAME" do
default_to { "production" }
- summary "The target environment to search for modules."
+ summary "The target environment to uninstall modules from."
description <<-EOT
- The target environment to search for modules.
+ The target environment to uninstall modules from.
EOT
end
-
+
option "--version=" do
summary "The version of the module to uninstall"
description <<-EOT
- The version of the module to uninstall. When using this option a module
- that matches the specified version must be installed or an error is raised.
+ The version of the module to uninstall. When using this option, a module
+ matching the specified version must be installed or else an error is raised.
EOT
end
option "--modulepath=" do
summary "The target directory to search for modules."
description <<-EOT
The target directory to search for modules.
EOT
end
when_invoked do |name, options|
- if options[:modulepath]
- unless File.directory?(options[:modulepath])
- raise ArgumentError, "Directory #{options[:modulepath]} does not exist"
- end
- end
-
Puppet[:modulepath] = options[:modulepath] if options[:modulepath]
- options[:name] = name
+ name = name.gsub('/', '-')
+ Puppet.notice "Preparing to uninstall '#{name}'" << (options[:version] ? " (#{colorize(:cyan, options[:version].sub(/^(?=\d)/, 'v'))})" : '') << " ..."
Puppet::Module::Tool::Applications::Uninstaller.run(name, options)
end
when_rendering :console do |return_value|
- output = ''
-
- return_value[:removed_mods].each do |mod|
- output << "Removed #{mod.path} (v#{mod.version})\n"
+ if return_value[:result] == :failure
+ Puppet.err(return_value[:error][:multiline])
+ exit 1
+ else
+ mod = return_value[:affected_modules].first
+ "Removed '#{return_value[:module_name]}'" <<
+ (mod.version ? " (#{colorize(:cyan, mod.version.to_s.sub(/^(?=\d)/, 'v'))})" : '') <<
+ " from #{mod.modulepath}"
end
-
- return_value[:errors].map do |mod_name, errors|
- if ! errors.empty?
- header = "Could not uninstall module #{return_value[:options][:name]}"
- header << " (v#{return_value[:options][:version]})" if return_value[:options][:version]
- output << "#{header}:\n"
- errors.map { |error| output << " #{error}\n" }
- end
- end
-
- output
end
end
end
diff --git a/lib/puppet/face/module/upgrade.rb b/lib/puppet/face/module/upgrade.rb
new file mode 100644
index 000000000..e62a7c535
--- /dev/null
+++ b/lib/puppet/face/module/upgrade.rb
@@ -0,0 +1,84 @@
+# encoding: UTF-8
+
+Puppet::Face.define(:module, '1.0.0') do
+ action(:upgrade) do
+ summary "Upgrade a puppet module."
+ description <<-EOT
+ Upgrades a puppet module.
+ EOT
+
+ returns "Hash"
+
+ examples <<-EOT
+ upgrade an installed module to the latest version
+
+ $ puppet module upgrade puppetlabs-apache
+ /etc/puppet/modules
+ └── puppetlabs-apache (v1.0.0 -> v2.4.0)
+
+ upgrade an installed module to a specific version
+
+ $ puppet module upgrade puppetlabs-apache --version 2.1.0
+ /etc/puppet/modules
+ └── puppetlabs-apache (v1.0.0 -> v2.1.0)
+
+ upgrade an installed module for a specific environment
+
+ $ puppet module upgrade puppetlabs-apache --environment test
+ /usr/share/puppet/environments/test/modules
+ └── puppetlabs-apache (v1.0.0 -> v2.4.0)
+ EOT
+
+ arguments ""
+
+ option "--force", "-f" do
+ summary "Force upgrade of an installed module."
+ description <<-EOT
+ Force the upgrade of an installed module even if there are local
+ changes or the possibility of causing broken dependencies.
+ EOT
+ end
+
+ option "--ignore-dependencies" do
+ summary "Do not attempt to install dependencies."
+ description <<-EOT
+ Do not attempt to install dependencies
+ EOT
+ end
+
+ option "--environment NAME" do
+ default_to { "production" }
+ summary "The target environment to search for modules."
+ description <<-EOT
+ The target environment to search for modules.
+ EOT
+ end
+
+ option "--version=" do
+ summary "The version of the module to upgrade to."
+ description <<-EOT
+ The version of the module to upgrade to.
+ EOT
+ end
+
+ when_invoked do |name, options|
+ name = name.gsub('/', '-')
+ Puppet.notice "Preparing to upgrade '#{name}' ..."
+ Puppet::Module::Tool::Applications::Upgrader.new(name, options).run
+ end
+
+ when_rendering :console do |return_value|
+ if return_value[:result] == :failure
+ Puppet.err(return_value[:error][:multiline])
+ exit 1
+ elsif return_value[:result] == :noop
+ Puppet.err(return_value[:error][:multiline])
+ exit 0
+ else
+ tree = Puppet::Module::Tool.build_tree(return_value[:affected_modules], return_value[:base_dir])
+ return_value[:base_dir] + "\n" +
+ Puppet::Module::Tool.format_tree(tree)
+ end
+ end
+ end
+end
diff --git a/lib/puppet/forge.rb b/lib/puppet/forge.rb
index 6b3c5742f..8eeb991ae 100644
--- a/lib/puppet/forge.rb
+++ b/lib/puppet/forge.rb
@@ -1,153 +1,96 @@
require 'net/http'
require 'open-uri'
require 'pathname'
require 'uri'
require 'puppet/forge/cache'
require 'puppet/forge/repository'
module Puppet::Forge
- class Forge
- def initialize(url=Puppet.settings[:module_repository])
- @uri = URI.parse(url)
+ # Return a list of module metadata hashes that match the search query.
+ # This return value is used by the module_tool face install search,
+ # and displayed to on the console.
+ #
+ # Example return value:
+ #
+ # [
+ # {
+ # "author" => "puppetlabs",
+ # "name" => "bacula",
+ # "tag_list" => ["backup", "bacula"],
+ # "releases" => [{"version"=>"0.0.1"}, {"version"=>"0.0.2"}],
+ # "full_name" => "puppetlabs/bacula",
+ # "version" => "0.0.2",
+ # "project_url" => "http://github.com/puppetlabs/puppetlabs-bacula",
+ # "desc" => "bacula"
+ # }
+ # ]
+ #
+ def self.search(term)
+ request = Net::HTTP::Get.new("/modules.json?q=#{URI.escape(term)}")
+ response = repository.make_http_request(request)
+
+ case response.code
+ when "200"
+ matches = PSON.parse(response.body)
+ else
+ raise RuntimeError, "Could not execute search (HTTP #{response.code})"
+ matches = []
end
- # Return a list of module metadata hashes that match the search query.
- # This return value is used by the module_tool face install search,
- # and displayed to on the console.
- #
- # Example return value:
- #
- # [
- # {
- # "author" => "puppetlabs",
- # "name" => "bacula",
- # "tag_list" => ["backup", "bacula"],
- # "releases" => [{"version"=>"0.0.1"}, {"version"=>"0.0.2"}],
- # "full_name" => "puppetlabs/bacula",
- # "version" => "0.0.2",
- # "project_url" => "http://github.com/puppetlabs/puppetlabs-bacula",
- # "desc" => "bacula"
- # }
- # ]
- #
- def search(term)
- request = Net::HTTP::Get.new("/modules.json?q=#{URI.escape(term)}")
- response = repository.make_http_request(request)
+ matches
+ end
- case response.code
- when "200"
- matches = PSON.parse(response.body)
+ def self.remote_dependency_info(author, mod_name, version)
+ version_string = version ? "&version=#{version}" : ''
+ request = Net::HTTP::Get.new("/api/v1/releases.json?module=#{author}/#{mod_name}" + version_string)
+ response = repository.make_http_request(request)
+ json = PSON.parse(response.body) rescue {}
+ case response.code
+ when "200"
+ return json
+ else
+ error = json['error'] || ''
+ if error =~ /^Module #{author}\/#{mod_name} has no release/
+ return []
else
- raise RuntimeError, "Could not execute search (HTTP #{response.code})"
- matches = []
+ raise RuntimeError, "Could not find release information for this module (#{author}/#{mod_name}) (HTTP #{response.code})"
end
-
- matches
end
+ end
- # Return a Pathname object representing the path to the module
- # release package in the `Puppet.settings[:module_working_dir]`.
- def get_release_package(params)
+ def self.get_release_packages_from_repository(install_list)
+ install_list.map do |release|
+ modname, version, file = release
cache_path = nil
- case params[:source]
- when :repository
- if not (params[:author] && params[:modname])
- raise ArgumentError, ":author and :modename required"
- end
- cache_path = get_release_package_from_repository(params[:author], params[:modname], params[:version])
- when :filesystem
- if not params[:filename]
- raise ArgumentError, ":filename required"
- end
- cache_path = get_release_package_from_filesystem(params[:filename])
- else
- raise ArgumentError, "Could not determine installation source"
- end
-
- cache_path
- end
-
- def get_releases(author, modname)
- request_string = "/#{author}/#{modname}"
-
- begin
- response = repository.make_http_request(request_string)
- rescue => e
- raise ArgumentError, "Could not find a release for this module (#{e.message})"
- end
-
- results = PSON.parse(response.body)
- # At this point releases look like this:
- # [{"version" => "0.0.1"}, {"version" => "0.0.2"},{"version" => "0.0.3"}]
- #
- # Lets fix this up a bit and return something like this to the caller
- # ["0.0.1", "0.0.2", "0.0.3"]
- results["releases"].collect {|release| release["version"]}
- end
-
- private
-
- # Locate and download a module release package from the remote forge
- # repository into the `Puppet.settings[:module_working_dir]`. Do not
- # unpack it, just return the location of the package on disk.
- def get_release_package_from_repository(author, modname, version=nil)
- release = get_release(author, modname, version)
- if release['file']
+ if file
begin
- cache_path = repository.retrieve(release['file'])
+ cache_path = repository.retrieve(file)
rescue OpenURI::HTTPError => e
raise RuntimeError, "Could not download module: #{e.message}"
end
else
raise RuntimeError, "Malformed response from module repository."
end
-
- cache_path
- end
-
- # Locate a module release package on the local filesystem and move it
- # into the `Puppet.settings[:module_working_dir]`. Do not unpack it, just
- # return the location of the package on disk.
- def get_release_package_from_filesystem(filename)
- if File.exist?(File.expand_path(filename))
- repository = Repository.new('file:///')
- uri = URI.parse("file://#{URI.escape(File.expand_path(filename))}")
- cache_path = repository.retrieve(uri)
- else
- raise ArgumentError, "File does not exists: #{filename}"
- end
-
cache_path
end
+ end
- def repository
- @repository ||= Puppet::Forge::Repository.new(@uri)
+ # Locate a module release package on the local filesystem and move it
+ # into the `Puppet.settings[:module_working_dir]`. Do not unpack it, just
+ # return the location of the package on disk.
+ def self.get_release_package_from_filesystem(filename)
+ if File.exist?(File.expand_path(filename))
+ repository = Repository.new('file:///')
+ uri = URI.parse("file://#{URI.escape(File.expand_path(filename))}")
+ cache_path = repository.retrieve(uri)
+ else
+ raise ArgumentError, "File does not exists: #{filename}"
end
- # Connect to the remote repository and locate a specific module release
- # by author/name combination. If a version requirement is specified, search
- # for that exact version, or grab the latest release available.
- #
- # Return the following response to the caller:
- #
- # {"file"=>"/system/releases/p/puppetlabs/puppetlabs-apache-0.0.3.tar.gz", "version"=>"0.0.3"}
- #
- #
- def get_release(author, modname, version_requirement=nil)
- request_string = "/users/#{author}/modules/#{modname}/releases/find.json"
- if version_requirement
- request_string + "?version=#{URI.escape(version_requirement)}"
- end
- request = Net::HTTP::Get.new(request_string)
-
- begin
- response = repository.make_http_request(request)
- rescue => e
- raise ArgumentError, "Could not find a release for this module (#{e.message})"
- end
+ cache_path
+ end
- PSON.parse(response.body)
- end
+ def self.repository
+ @repository ||= Puppet::Forge::Repository.new
end
end
-
diff --git a/lib/puppet/forge/cache.rb b/lib/puppet/forge/cache.rb
index 253f24761..592bf1ace 100644
--- a/lib/puppet/forge/cache.rb
+++ b/lib/puppet/forge/cache.rb
@@ -1,55 +1,55 @@
require 'uri'
module Puppet::Forge
# = Cache
#
# Provides methods for reading files from local cache, filesystem or network.
class Cache
# Instantiate new cahe for the +repositry+ instance.
def initialize(repository, options = {})
@repository = repository
@options = options
end
# Return filename retrieved from +uri+ instance. Will download this file and
# cache it if needed.
#
# TODO: Add checksum support.
# TODO: Add error checking.
def retrieve(url)
(path + File.basename(url.to_s)).tap do |cached_file|
uri = url.is_a?(::URI) ? url : ::URI.parse(url)
unless cached_file.file?
if uri.scheme == 'file'
FileUtils.cp(URI.unescape(uri.path), cached_file)
else
# TODO: Handle HTTPS; probably should use repository.contact
data = read_retrieve(uri)
cached_file.open('wb') { |f| f.write data }
end
end
end
end
# Return contents of file at the given URI's +uri+.
def read_retrieve(uri)
return uri.read
end
# Return Pathname for repository's cache directory, create it if needed.
def path
- return @path ||= (self.class.base_path + @repository.cache_key).tap{ |o| o.mkpath }
+ (self.class.base_path + @repository.cache_key).tap{ |o| o.mkpath }
end
# Return the base Pathname for all the caches.
def self.base_path
Pathname(Puppet.settings[:module_working_dir]) + 'cache'
end
# Clean out all the caches.
def self.clean
base_path.rmtree if base_path.exist?
end
end
end
diff --git a/lib/puppet/forge/repository.rb b/lib/puppet/forge/repository.rb
index 2e101496b..fd92b7d44 100644
--- a/lib/puppet/forge/repository.rb
+++ b/lib/puppet/forge/repository.rb
@@ -1,121 +1,102 @@
require 'net/http'
require 'digest/sha1'
require 'uri'
-require 'puppet/module_tool/utils'
-
module Puppet::Forge
- # Directory names that should not be checksummed.
- ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage']
- FULL_MODULE_NAME_PATTERN = /\A([^-\/|.]+)[-|\/](.+)\z/
- REPOSITORY_URL = Puppet.settings[:module_repository]
-
# = Repository
#
# This class is a file for accessing remote repositories with modules.
class Repository
- include Puppet::Module::Tool::Utils::Interrogation
attr_reader :uri, :cache
# Instantiate a new repository instance rooted at the optional string
# +url+, else an instance of the default Puppet modules repository.
def initialize(url=Puppet[:module_repository])
- @uri = url.is_a?(::URI) ? url : ::URI.parse(url)
+ @uri = url.is_a?(::URI) ? url : ::URI.parse(url.sub(/^(?!https?:\/\/)/, 'http://'))
@cache = Cache.new(self)
end
# Read HTTP proxy configurationm from Puppet's config file, or the
# http_proxy environment variable.
def http_proxy_env
proxy_env = ENV["http_proxy"] || ENV["HTTP_PROXY"] || nil
begin
return URI.parse(proxy_env) if proxy_env
rescue URI::InvalidURIError
return nil
end
return nil
end
def http_proxy_host
env = http_proxy_env
if env and env.host then
return env.host
end
if Puppet.settings[:http_proxy_host] == 'none'
return nil
end
return Puppet.settings[:http_proxy_host]
end
def http_proxy_port
env = http_proxy_env
if env and env.port then
return env.port
end
return Puppet.settings[:http_proxy_port]
end
# Return a Net::HTTPResponse read for this +request+.
- #
- # Options:
- # * :authenticate => Request authentication on the terminal. Defaults to false.
def make_http_request(request, options = {})
- if options[:authenticate]
- authenticate(request)
- end
if ! @uri.user.nil? && ! @uri.password.nil?
request.basic_auth(@uri.user, @uri.password)
end
return read_response(request)
end
# Return a Net::HTTPResponse read from this HTTPRequest +request+.
def read_response(request)
begin
Net::HTTP::Proxy(
http_proxy_host,
http_proxy_port
).start(@uri.host, @uri.port) do |http|
http.request(request)
end
rescue Errno::ECONNREFUSED, SocketError
- raise RuntimeError, "Could not reach remote repository"
+ msg = "Error: Could not connect to #{@uri}\n"
+ msg << " There was a network communications problem\n"
+ msg << " Check your network connection and try again\n"
+ $stderr << msg
+ exit(1)
end
end
- # Set the HTTP Basic Authentication parameters for the Net::HTTPRequest
- # +request+ by asking the user for input on the console.
- def authenticate(request)
- Puppet.notice "Authenticating for #{@uri}"
- email = prompt('Email Address')
- password = prompt('Password', true)
- request.basic_auth(email, password)
- end
-
# Return the local file name containing the data downloaded from the
# repository at +release+ (e.g. "myuser-mymodule").
def retrieve(release)
return cache.retrieve(@uri + release)
end
# Return the URI string for this repository.
def to_s
return @uri.to_s
end
# Return the cache key for this repository, this a hashed string based on
# the URI.
def cache_key
return @cache_key ||= [
@uri.to_s.gsub(/[^[:alnum:]]+/, '_').sub(/_$/, ''),
Digest::SHA1.hexdigest(@uri.to_s)
].join('-')
end
end
end
diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb
index c29939911..5bfb1306d 100644
--- a/lib/puppet/module.rb
+++ b/lib/puppet/module.rb
@@ -1,268 +1,319 @@
require 'puppet/util/logging'
require 'semver'
require 'puppet/module_tool/applications'
# Support for modules
class Puppet::Module
class Error < Puppet::Error; end
class MissingModule < Error; end
class IncompatibleModule < Error; end
class UnsupportedPlatform < Error; end
class IncompatiblePlatform < Error; end
class MissingMetadata < Error; end
class InvalidName < Error; end
include Puppet::Util::Logging
TEMPLATES = "templates"
FILES = "files"
MANIFESTS = "manifests"
PLUGINS = "plugins"
FILETYPES = [MANIFESTS, FILES, TEMPLATES, PLUGINS]
# Find and return the +module+ that +path+ belongs to. If +path+ is
# absolute, or if there is no module whose name is the first component
# of +path+, return +nil+
def self.find(modname, environment = nil)
return nil unless modname
Puppet::Node::Environment.new(environment).module(modname)
end
attr_reader :name, :environment
attr_writer :environment
attr_accessor :dependencies, :forge_name
attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page
def has_metadata?
return false unless metadata_file
return false unless FileTest.exist?(metadata_file)
metadata = PSON.parse File.read(metadata_file)
return metadata.is_a?(Hash) && !metadata.keys.empty?
end
def initialize(name, options = {})
@name = name
@path = options[:path]
assert_validity
if options[:environment].is_a?(Puppet::Node::Environment)
@environment = options[:environment]
else
@environment = Puppet::Node::Environment.new(options[:environment])
end
load_metadata if has_metadata?
validate_puppet_version
end
FILETYPES.each do |type|
# A boolean method to let external callers determine if
# we have files of a given type.
define_method(type +'?') do
unless path
Puppet.debug("No #{type} found; path not specified")
return false
end
type_subpath = subpath(type)
unless FileTest.exist?(type_subpath)
Puppet.debug("No #{type} found in subpath '#{type_subpath}' " +
"(file / directory does not exist)")
return false
end
return true
end
# A method for returning a given file of a given type.
# e.g., file = mod.manifest("my/manifest.pp")
#
# If the file name is nil, then the base directory for the
# file type is passed; this is used for fileserving.
define_method(type.to_s.sub(/s$/, '')) do |file|
return nil unless path
# If 'file' is nil then they're asking for the base path.
# This is used for things like fileserving.
if file
full_path = File.join(subpath(type), file)
else
full_path = subpath(type)
end
return nil unless FileTest.exist?(full_path)
return full_path
end
end
def exist?
! path.nil?
end
def license_file
return @license_file if defined?(@license_file)
return @license_file = nil unless path
@license_file = File.join(path, "License")
end
def load_metadata
data = PSON.parse File.read(metadata_file)
@forge_name = data['name'].gsub('-', '/') if data['name']
[:source, :author, :version, :license, :puppetversion, :dependencies].each do |attr|
unless value = data[attr.to_s]
unless attr == :puppetversion
raise MissingMetadata, "No #{attr} module metadata provided for #{self.name}"
end
end
+
+ # NOTICE: The fallback to `versionRequirement` is something we'd like to
+ # not have to support, but we have a reasonable number of releases that
+ # don't use `version_requirement`. When we can deprecate this, we should.
+ if attr == :dependencies
+ value.tap do |dependencies|
+ dependencies.each do |dep|
+ dep['version_requirement'] ||= dep['versionRequirement'] || '>= 0.0.0'
+ end
+ end
+ end
+
send(attr.to_s + "=", value)
end
end
# Return the list of manifests matching the given glob pattern,
# defaulting to 'init.{pp,rb}' for empty modules.
def match_manifests(rest)
pat = File.join(path, MANIFESTS, rest || 'init')
[manifest("init.pp"),manifest("init.rb")].compact + Dir.
glob(pat + (File.extname(pat).empty? ? '.{pp,rb}' : '')).
reject { |f| FileTest.directory?(f) }
end
def metadata_file
return @metadata_file if defined?(@metadata_file)
return @metadata_file = nil unless path
@metadata_file = File.join(path, "metadata.json")
end
# Find this module in the modulepath.
def path
@path ||= environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.directory?(d) }
end
+ def modulepath
+ File.dirname(path) if path
+ end
+
# Find all plugin directories. This is used by the Plugins fileserving mount.
def plugin_directory
subpath("plugins")
end
def supports(name, version = nil)
@supports ||= []
@supports << [name, version]
end
def to_s
result = "Module #{name}"
result += "(#{path})" if path
result
end
def dependencies_as_modules
dependent_modules = []
dependencies and dependencies.each do |dep|
author, dep_name = dep["name"].split('/')
found_module = environment.module(dep_name)
dependent_modules << found_module if found_module
end
dependent_modules
end
def required_by
environment.module_requirements[self.forge_name] || {}
end
def has_local_changes?
changes = Puppet::Module::Tool::Applications::Checksummer.run(path)
!changes.empty?
end
- def unmet_dependencies
- return [] unless dependencies
+ def local_changes
+ Puppet::Module::Tool::Applications::Checksummer.run(path)
+ end
+ # Identify and mark unmet dependencies. A dependency will be marked unmet
+ # for the following reasons:
+ #
+ # * not installed and is thus considered missing
+ # * installed and does not meet the version requirements for this module
+ # * installed and doesn't use semantic versioning
+ #
+ # Returns a list of hashes representing the details of an unmet dependency.
+ #
+ # Example:
+ #
+ # [
+ # {
+ # :reason => :missing,
+ # :name => 'puppetlabs-mysql',
+ # :version_constraint => 'v0.0.1',
+ # :mod_details => {
+ # :installed_version => '0.0.1'
+ # }
+ # :parent => {
+ # :name => 'puppetlabs-bacula',
+ # :version => 'v1.0.0'
+ # }
+ # }
+ # ]
+ #
+ def unmet_dependencies
unmet_dependencies = []
+ return unmet_dependencies unless dependencies
dependencies.each do |dependency|
forge_name = dependency['name']
- author, dep_name = forge_name.split('/')
- version_string = dependency['version_requirement']
+ version_string = dependency['version_requirement'] || '>= 0.0.0'
- equality, dep_version = version_string ? version_string.split("\s") : [nil, nil]
-
- unless dep_mod = environment.module(dep_name)
- msg = "Missing dependency `#{dep_name}`:\n"
- msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n"
- unmet_dependencies << { :name => forge_name, :error => msg }
- next
+ dep_mod = begin
+ environment.module_by_forge_name(forge_name)
+ rescue => e
+ nil
end
- if dep_version && !dep_mod.version
- msg = "Unversioned dependency `#{dep_mod.name}`:\n"
- msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n"
- unmet_dependencies << { :name => forge_name, :error => msg }
+ error_details = {
+ :name => forge_name,
+ :version_constraint => version_string.gsub(/^(?=\d)/, "v"),
+ :parent => {
+ :name => self.forge_name,
+ :version => self.version.gsub(/^(?=\d)/, "v")
+ },
+ :mod_details => {
+ :installed_version => dep_mod.nil? ? nil : dep_mod.version
+ }
+ }
+
+ unless dep_mod
+ error_details[:reason] = :missing
+ unmet_dependencies << error_details
next
end
- if dep_version
+ if version_string
begin
- required_version_semver = SemVer.new(dep_version)
+ required_version_semver_range = SemVer[version_string]
actual_version_semver = SemVer.new(dep_mod.version)
rescue ArgumentError
- msg = "Non semantic version dependency `#{dep_mod.name}` (#{dep_mod.version}):\n"
- msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n"
- unmet_dependencies << { :name => forge_name, :error => msg }
+ error_details[:reason] = :non_semantic_version
+ unmet_dependencies << error_details
next
end
- if !actual_version_semver.send(equality, required_version_semver)
- msg = "Version dependency mismatch `#{dep_mod.name}` (#{dep_mod.version}):\n"
- msg += " `#{self.name}` (#{self.version}) requires `#{forge_name}` (#{version_string})\n"
- unmet_dependencies << { :name => forge_name, :error => msg }
+ unless required_version_semver_range.include? actual_version_semver
+ error_details[:reason] = :version_mismatch
+ unmet_dependencies << error_details
next
end
end
end
+
unmet_dependencies
end
def validate_puppet_version
return unless puppetversion and puppetversion != Puppet.version
raise IncompatibleModule, "Module #{self.name} is only compatible with Puppet version #{puppetversion}, not #{Puppet.version}"
end
private
def subpath(type)
return File.join(path, type) unless type.to_s == "plugins"
backward_compatible_plugins_dir
end
def backward_compatible_plugins_dir
if dir = File.join(path, "plugins") and FileTest.exist?(dir)
Puppet.deprecation_warning "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'"
return dir
else
return File.join(path, "lib")
end
end
def assert_validity
raise InvalidName, "Invalid module name #{name}; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/
end
def ==(other)
self.name == other.name &&
- self.version == other.version &&
- self.path == other.path &&
- self.environment == other.environment
+ self.version == other.version &&
+ self.path == other.path &&
+ self.environment == other.environment
end
end
diff --git a/lib/puppet/module_tool.rb b/lib/puppet/module_tool.rb
index 6d8d4dbd3..a05ac8e3a 100644
--- a/lib/puppet/module_tool.rb
+++ b/lib/puppet/module_tool.rb
@@ -1,61 +1,101 @@
+# encoding: UTF-8
# Load standard libraries
require 'pathname'
require 'fileutils'
-require 'puppet/module_tool/utils'
+require 'puppet/util/colors'
# Define tool
module Puppet
class Module
module Tool
+ extend Puppet::Util::Colors
- # Directory names that should not be checksummed.
- ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage']
+ # Directory and names that should not be checksummed.
+ ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage', 'metadata.json', 'REVISION']
FULL_MODULE_NAME_PATTERN = /\A([^-\/|.]+)[-|\/](.+)\z/
REPOSITORY_URL = Puppet.settings[:module_repository]
# Is this a directory that shouldn't be checksummed?
#
# TODO: Should this be part of Checksums?
# TODO: Rename this method to reflect it's purpose?
# TODO: Shouldn't this be used when building packages too?
def self.artifact?(path)
case File.basename(path)
when *ARTIFACTS
true
else
false
end
end
# Return the +username+ and +modname+ for a given +full_module_name+, or raise an
# ArgumentError if the argument isn't parseable.
def self.username_and_modname_from(full_module_name)
if matcher = full_module_name.match(FULL_MODULE_NAME_PATTERN)
return matcher.captures
else
raise ArgumentError, "Not a valid full name: #{full_module_name}"
end
end
def self.find_module_root(path)
for dir in [path, Dir.pwd].compact
if File.exist?(File.join(dir, 'Modulefile'))
return dir
end
end
raise ArgumentError, "Could not find a valid module at #{path ? path.inspect : 'current directory'}"
end
+
+ # Builds a formatted tree from a list of node hashes containing +:text+
+ # and +:dependencies+ keys.
+ def self.format_tree(nodes, level = 0)
+ str = ''
+ nodes.each_with_index do |node, i|
+ last_node = nodes.length - 1 == i
+ deps = node[:dependencies] || []
+
+ str << (indent = " " * level)
+ str << (last_node ? "└" : "├")
+ str << "─"
+ str << (deps.empty? ? "─" : "┬")
+ str << " #{node[:text]}\n"
+
+ branch = format_tree(deps, level + 1)
+ branch.gsub!(/^#{indent} /, indent + '│') unless last_node
+ str << branch
+ end
+
+ return str
+ end
+
+ def self.build_tree(mods, dir)
+ mods.each do |mod|
+ version_string = mod[:version][:vstring].sub(/^(?!v)/, 'v')
+
+ if mod[:action] == :upgrade
+ previous_version = mod[:previous_version].sub(/^(?!v)/, 'v')
+ version_string = "#{previous_version} -> #{version_string}"
+ end
+
+ mod[:text] = "#{mod[:module]} (#{colorize(:cyan, version_string)})"
+ mod[:text] += " [#{mod[:path]}]" unless mod[:path] == dir
+ build_tree(mod[:dependencies], dir)
+ end
+ end
end
end
end
# Load remaining libraries
+require 'puppet/module_tool/errors'
require 'puppet/module_tool/applications'
require 'puppet/module_tool/checksums'
require 'puppet/module_tool/contents_description'
require 'puppet/module_tool/dependency'
require 'puppet/module_tool/metadata'
require 'puppet/module_tool/modulefile'
require 'puppet/module_tool/skeleton'
require 'puppet/forge/cache'
require 'puppet/forge'
diff --git a/lib/puppet/module_tool/applications.rb b/lib/puppet/module_tool/applications.rb
index d5eb7f581..c3fb85731 100644
--- a/lib/puppet/module_tool/applications.rb
+++ b/lib/puppet/module_tool/applications.rb
@@ -1,17 +1,17 @@
require 'puppet/module'
class Puppet::Module
module Tool
module Applications
require 'puppet/module_tool/applications/application'
require 'puppet/module_tool/applications/builder'
require 'puppet/module_tool/applications/checksummer'
- require 'puppet/module_tool/applications/cleaner'
require 'puppet/module_tool/applications/generator'
require 'puppet/module_tool/applications/installer'
require 'puppet/module_tool/applications/searcher'
require 'puppet/module_tool/applications/unpacker'
require 'puppet/module_tool/applications/uninstaller'
+ require 'puppet/module_tool/applications/upgrader'
end
end
end
diff --git a/lib/puppet/module_tool/applications/application.rb b/lib/puppet/module_tool/applications/application.rb
index fd398da81..bce0e84b1 100644
--- a/lib/puppet/module_tool/applications/application.rb
+++ b/lib/puppet/module_tool/applications/application.rb
@@ -1,80 +1,82 @@
require 'net/http'
require 'semver'
-require 'puppet/module_tool/utils/interrogation'
+require 'puppet/util/colors'
module Puppet::Module::Tool
module Applications
class Application
- include Puppet::Module::Tool::Utils::Interrogation
+ include Puppet::Util::Colors
def self.run(*args)
new(*args).run
end
attr_accessor :options
def initialize(options = {})
+ if Puppet.features.microsoft_windows?
+ raise Puppet::Error, "`puppet module` actions are currently not supported on Microsoft Windows"
+ end
@options = options
end
def run
raise NotImplementedError, "Should be implemented in child classes."
end
def discuss(response, success, failure)
case response
when Net::HTTPOK, Net::HTTPCreated
Puppet.notice success
else
errors = PSON.parse(response.body)['error'] rescue "HTTP #{response.code}, #{response.body}"
Puppet.warning "#{failure} (#{errors})"
end
end
def metadata(require_modulefile = false)
unless @metadata
unless @path
raise ArgumentError, "Could not determine module path"
end
@metadata = Puppet::Module::Tool::Metadata.new
contents = ContentsDescription.new(@path)
contents.annotate(@metadata)
checksums = Checksums.new(@path)
checksums.annotate(@metadata)
modulefile_path = File.join(@path, 'Modulefile')
if File.file?(modulefile_path)
Puppet::Module::Tool::ModulefileReader.evaluate(@metadata, modulefile_path)
elsif require_modulefile
raise ArgumentError, "No Modulefile found."
end
end
@metadata
end
def load_modulefile!
@metadata = nil
metadata(true)
end
- # Use to extract and validate a module name and version from a
- # filename
- # Note: Must have @filename set to use this
- def parse_filename!
- @release_name = File.basename(@filename,'.tar.gz')
- match = /^(.*?)-(.*?)-(\d+\.\d+\.\d+.*?)$/.match(@release_name)
- if match then
- @username, @module_name, @version = match.captures
+ def parse_filename(filename)
+ if match = /^((.*?)-(.*?))-(\d+\.\d+\.\d+.*?)$/.match(File.basename(filename,'.tar.gz'))
+ module_name, author, shortname, version = match.captures
else
raise ArgumentError, "Could not parse filename to obtain the username, module name and version. (#{@release_name})"
end
- @full_module_name = [@username, @module_name].join('-')
- unless @username && @module_name
- raise ArgumentError, "Username and Module name not provided"
- end
- unless SemVer.valid?(@version)
- raise ArgumentError, "Invalid version format: #{@version} (Semantic Versions are acceptable: http://semver.org)"
+
+ unless SemVer.valid?(version)
+ raise ArgumentError, "Invalid version format: #{version} (Semantic Versions are acceptable: http://semver.org)"
end
+
+ return {
+ :module_name => module_name,
+ :author => author,
+ :dir_name => shortname,
+ :version => version
+ }
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/checksummer.rb b/lib/puppet/module_tool/applications/checksummer.rb
index 2ea1ef587..f0c3a7130 100644
--- a/lib/puppet/module_tool/applications/checksummer.rb
+++ b/lib/puppet/module_tool/applications/checksummer.rb
@@ -1,47 +1,56 @@
+require 'puppet/module_tool/checksums'
+
module Puppet::Module::Tool
module Applications
class Checksummer < Application
def initialize(path, options = {})
@path = Pathname.new(path)
super(options)
end
def run
changes = []
if metadata_file.exist?
- sums = Checksums.new(@path)
+ sums = Puppet::Module::Tool::Checksums.new(@path)
(metadata['checksums'] || {}).each do |child_path, canonical_checksum|
+
+ # Work around an issue where modules built with an older version
+ # of PMT would include the metadata.json file in the list of files
+ # checksummed. This causes metadata.json to always report local
+ # changes.
+ next if File.basename(child_path) == "metadata.json"
+
path = @path + child_path
if canonical_checksum != sums.checksum(path)
changes << child_path
end
end
else
raise ArgumentError, "No metadata.json found."
end
# Return an Array of strings representing file paths of files that have
# been modified since this module was installed. All paths are relative
# to the installed module directory. This return value is used by the
# module_tool face changes action, and displayed on the console.
#
# Example return value:
#
- # [ "REVISION", "metadata.json", "manifests/init.pp"]
+ # [ "REVISION", "manifests/init.pp"]
#
changes
end
private
def metadata
PSON.parse(metadata_file.read)
end
def metadata_file
(@path + 'metadata.json')
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/cleaner.rb b/lib/puppet/module_tool/applications/cleaner.rb
deleted file mode 100644
index b811983d7..000000000
--- a/lib/puppet/module_tool/applications/cleaner.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module Puppet::Module::Tool
- module Applications
- class Cleaner < Application
- def run
- Puppet::Forge::Cache.clean
-
- # Return a status Hash containing the status of the clean command
- # and a status message. This return value is used by the module_tool
- # face clean action, and the status message, return_value[:msg], is
- # displayed on the console.
- #
- { :status => "success", :msg => "Cleaned module cache." }
- end
- end
- end
-end
diff --git a/lib/puppet/module_tool/applications/installer.rb b/lib/puppet/module_tool/applications/installer.rb
index d76e0e308..78614930c 100644
--- a/lib/puppet/module_tool/applications/installer.rb
+++ b/lib/puppet/module_tool/applications/installer.rb
@@ -1,54 +1,183 @@
require 'open-uri'
require 'pathname'
require 'tmpdir'
+require 'semver'
+require 'puppet/forge'
+require 'puppet/module_tool'
+require 'puppet/module_tool/shared_behaviors'
module Puppet::Module::Tool
module Applications
class Installer < Application
+ include Puppet::Module::Tool::Errors
+
def initialize(name, options = {})
- @forge = Puppet::Forge::Forge.new
- @install_params = {}
+ @action = :install
+ @environment = Puppet::Node::Environment.new(Puppet.settings[:environment])
+ @force = options[:force]
+ @ignore_dependencies = options[:force] || options[:ignore_dependencies]
+ @name = name
+ super(options)
+ end
+
+ def run
+ begin
+ if is_module_package?(@name)
+ @source = :filesystem
+ @filename = File.expand_path(@name)
+ raise MissingPackageError, :requested_package => @filename unless File.exist?(@filename)
- if File.exist?(name)
- if File.directory?(name)
- # TODO Unify this handling with that of Unpacker#check_clobber!
- raise ArgumentError, "Module already installed: #{name}"
+ parsed = parse_filename(@filename)
+ @module_name = parsed[:module_name]
+ @version = parsed[:version]
+ else
+ @source = :repository
+ @module_name = @name.gsub('/', '-')
+ @version = options[:version]
end
- @filename = File.expand_path(name)
- @install_params[:source] = :filesystem
- @install_params[:filename] = @filename
- parse_filename!
- else
- @install_params[:source] = :repository
- begin
- @install_params[:author], @install_params[:modname] = Puppet::Module::Tool::username_and_modname_from(name)
- rescue ArgumentError
- raise "Could not install module with invalid name: #{name}"
+
+ results = {
+ :module_name => @module_name,
+ :module_version => @version,
+ :install_dir => options[:target_dir],
+ }
+
+ unless File.directory? options[:target_dir]
+ raise MissingInstallDirectoryError,
+ :requested_module => @module_name,
+ :requested_version => @version || 'latest',
+ :directory => options[:target_dir]
end
- @install_params[:version_requirement] = options[:version]
+
+ cached_paths = get_release_packages
+
+ unless @graph.empty?
+ Puppet.notice 'Installing -- do not interrupt ...'
+ cached_paths.each do |hash|
+ hash.each do |dir, path|
+ Unpacker.new(path, @options.merge(:target_dir => dir)).run
+ end
+ end
+ end
+ rescue ModuleToolError => err
+ results[:error] = {
+ :oneline => err.message,
+ :multiline => err.multiline,
+ }
+ else
+ results[:result] = :success
+ results[:installed_modules] = @graph
+ ensure
+ results[:result] ||= :failure
end
- super(options)
+
+ results
+ end
+
+ private
+
+ include Puppet::Module::Tool::Shared
+
+ # Return a Pathname object representing the path to the module
+ # release package in the `Puppet.settings[:module_working_dir]`.
+ def get_release_packages
+ get_local_constraints
+
+ if !@force && @installed.include?(@module_name)
+
+ raise AlreadyInstalledError,
+ :module_name => @module_name,
+ :installed_version => @installed[@module_name].first.version,
+ :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best),
+ :local_changes => @installed[@module_name].first.local_changes
+ end
+
+ if @ignore_dependencies && @source == :filesystem
+ @urls = {}
+ @remote = { "#{@module_name}@#{@version}" => { } }
+ @versions = {
+ @module_name => [
+ { :vstring => @version, :semver => SemVer.new(@version) }
+ ]
+ }
+ else
+ get_remote_constraints
+ end
+
+ @graph = resolve_constraints({ @module_name => @version })
+ @graph.first[:tarball] = @filename if @source == :filesystem
+ resolve_install_conflicts(@graph) unless @force
+
+ # This clean call means we never "cache" the module we're installing, but this
+ # is desired since module authors can easily rerelease modules different content but the same
+ # version number, meaning someone with the old content cached will be very confused as to why
+ # they can't get new content.
+ # Long term we should just get rid of this caching behavior and cleanup downloaded modules after they install
+ # but for now this is a quick fix to disable caching
+ Puppet::Forge::Cache.clean
+ download_tarballs(@graph, @graph.last[:path])
end
- def force?
- options[:force]
+ #
+ # Resolve installation conflicts by checking if the requested module
+ # or one of it's dependencies conflicts with an installed module.
+ #
+ # Conflicts occur under the following conditions:
+ #
+ # When installing 'puppetlabs-foo' and an existing directory in the
+ # target install path contains a 'foo' directory and we cannot determine
+ # the "full name" of the installed module.
+ #
+ # When installing 'puppetlabs-foo' and 'pete-foo' is already installed.
+ # This is considered a conflict because 'puppetlabs-foo' and 'pete-foo'
+ # install into the same directory 'foo'.
+ #
+ def resolve_install_conflicts(graph, is_dependency = false)
+ graph.each do |release|
+ @environment.modules_by_path[options[:target_dir]].each do |mod|
+ if mod.has_metadata?
+ metadata = {
+ :name => mod.forge_name.gsub('/', '-'),
+ :version => mod.version
+ }
+ next if release[:module] == metadata[:name]
+ else
+ metadata = nil
+ end
+
+ if release[:module] =~ /-#{mod.name}$/
+ dependency_info = {
+ :name => release[:module],
+ :version => release[:version][:vstring]
+ }
+ dependency = is_dependency ? dependency_info : nil
+ latest_version = @versions["#{@module_name}"].sort_by { |h| h[:semver] }.last[:vstring]
+
+ raise InstallConflictError,
+ :requested_module => @module_name,
+ :requested_version => @version || "latest: v#{latest_version}",
+ :dependency => dependency,
+ :directory => mod.path,
+ :metadata => metadata
+ end
+
+ resolve_install_conflicts(release[:dependencies], true)
+ end
+ end
end
- def run
- cache_path = @forge.get_release_package(@install_params)
-
- module_dir = Unpacker.run(cache_path, options)
- # Return the Pathname object representing the path to the installed
- # module. This return value is used by the module_tool face install
- # action, and displayed to on the console.
- #
- # Example return value:
- #
- # "/etc/puppet/modules/apache"
- #
- module_dir
+ #
+ # Check if a file is a vaild module package.
+ # ---
+ # FIXME: Checking for a valid module package should be more robust and
+ # use the acutal metadata contained in the package. 03132012 - Hightower
+ # +++
+ #
+ def is_module_package?(name)
+ filename = File.expand_path(name)
+ filename =~ /.tar.gz$/
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/searcher.rb b/lib/puppet/module_tool/applications/searcher.rb
index 97028cd44..923aaf92d 100644
--- a/lib/puppet/module_tool/applications/searcher.rb
+++ b/lib/puppet/module_tool/applications/searcher.rb
@@ -1,16 +1,15 @@
module Puppet::Module::Tool
module Applications
class Searcher < Application
def initialize(term, options = {})
@term = term
- @forge = Puppet::Forge::Forge.new
super(options)
end
def run
- @forge.search(@term)
+ Puppet::Forge.search(@term)
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/uninstaller.rb b/lib/puppet/module_tool/applications/uninstaller.rb
index 4769e56ff..2ee9b9818 100644
--- a/lib/puppet/module_tool/applications/uninstaller.rb
+++ b/lib/puppet/module_tool/applications/uninstaller.rb
@@ -1,59 +1,107 @@
module Puppet::Module::Tool
module Applications
class Uninstaller < Application
+ include Puppet::Module::Tool::Errors
def initialize(name, options)
- @name = name
- @options = options
- @errors = Hash.new {|h, k| h[k] = []}
- @removed_mods = []
+ @name = name
+ @options = options
+ @errors = Hash.new {|h, k| h[k] = {}}
+ @unfiltered = []
+ @installed = []
+ @suggestions = []
@environment = Puppet::Node::Environment.new(options[:environment])
end
def run
- if module_installed?
- uninstall
- else
- @errors[@name] << "Module #{@name} is not installed"
+ results = {
+ :module_name => @name,
+ :requested_version => @version,
+ }
+
+ begin
+ find_installed_module
+ validate_module
+ FileUtils.rm_rf(@installed.first.path)
+
+ results[:affected_modules] = @installed
+ results[:result] = :success
+ rescue ModuleToolError => err
+ results[:error] = {
+ :oneline => err.message,
+ :multiline => err.multiline,
+ }
+ rescue => e
+ results[:error] = {
+ :oneline => e.message,
+ :multiline => e.respond_to?(:multiline) ? e.multiline : [e.to_s, e.backtrace].join("\n")
+ }
+ ensure
+ results[:result] ||= :failure
end
- { :removed_mods => @removed_mods, :errors => @errors, :options => @options }
+
+ results
end
private
- def version_match?(mod)
- if @options[:version]
- mod.version == @options[:version]
- else
- true
+ def find_installed_module
+ @environment.modules_by_path.values.flatten.each do |mod|
+ mod_name = (mod.forge_name || mod.name).gsub('/', '-')
+ if mod_name == @name
+ @unfiltered << {
+ :name => mod_name,
+ :version => mod.version,
+ :path => mod.modulepath,
+ }
+ if @options[:version] && mod.version
+ next unless SemVer[@options[:version]].include?(SemVer.new(mod.version))
+ end
+ @installed << mod
+ elsif mod_name =~ /#{@name}/
+ @suggestions << mod_name
+ end
end
- end
- def module_installed?
- @environment.module(@name)
+ if @installed.length > 1
+ raise MultipleInstalledError,
+ :action => :uninstall,
+ :module_name => @name,
+ :installed_modules => @installed.sort_by { |mod| @environment.modulepath.index(mod.modulepath) }
+ elsif @installed.empty?
+ if @unfiltered.empty?
+ raise NotInstalledError,
+ :action => :uninstall,
+ :suggestions => @suggestions,
+ :module_name => @name
+ else
+ raise NoVersionMatchesError,
+ :installed_modules => @unfiltered.sort_by { |mod| @environment.modulepath.index(mod[:path]) },
+ :version_range => @options[:version],
+ :module_name => @name
+ end
+ end
end
- def has_changes?
- Puppet::Module::Tool::Applications::Checksummer.run(@module.path)
- end
+ def validate_module
+ mod = @installed.first
- def uninstall
- # TODO: #11803 Check for broken dependencies before uninstalling modules.
- @environment.modules_by_path.each do |path, modules|
- modules.each do |mod|
- if mod.name == @name
- unless version_match?(mod)
- @errors[@name] << "Installed version of #{mod.name} (v#{mod.version}) does not match version range"
- end
-
- if @errors[@name].empty?
- FileUtils.rm_rf(mod.path)
- @removed_mods << mod
- end
- end
- end
+ if !@options[:force] && mod.has_metadata? && mod.has_local_changes?
+ raise LocalChangesError,
+ :action => :uninstall,
+ :module_name => (mod.forge_name || mod.name).gsub('/', '-'),
+ :requested_version => @options[:version],
+ :installed_version => mod.version
+ end
+
+ if !@options[:force] && !mod.required_by.empty?
+ raise ModuleIsRequiredError,
+ :module_name => (mod.forge_name || mod.name).gsub('/', '-'),
+ :required_by => mod.required_by,
+ :requested_version => @options[:version],
+ :installed_version => mod.version
end
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/unpacker.rb b/lib/puppet/module_tool/applications/unpacker.rb
index 119eaf323..f06c62d55 100644
--- a/lib/puppet/module_tool/applications/unpacker.rb
+++ b/lib/puppet/module_tool/applications/unpacker.rb
@@ -1,70 +1,48 @@
require 'pathname'
require 'tmpdir'
module Puppet::Module::Tool
module Applications
class Unpacker < Application
def initialize(filename, options = {})
@filename = Pathname.new(filename)
- parse_filename!
+ parsed = parse_filename(filename)
super(options)
- @module_dir = Pathname.new(options[:install_dir]) + @module_name
+ @module_dir = Pathname.new(options[:target_dir]) + parsed[:dir_name]
end
def run
extract_module_to_install_dir
- tag_revision
# Return the Pathname object representing the directory where the
# module release archive was unpacked the to, and the module release
# name.
@module_dir
end
private
-
- def tag_revision
- File.open("#{@module_dir}/REVISION", 'w') do |f|
- f.puts "module: #{@username}/#{@module_name}"
- f.puts "version: #{@version}"
- f.puts "url: file://#{@filename.expand_path}"
- f.puts "installed: #{Time.now}"
- end
- end
-
def extract_module_to_install_dir
delete_existing_installation_or_abort!
build_dir = Puppet::Forge::Cache.base_path + "tmp-unpacker-#{Digest::SHA1.hexdigest(@filename.basename.to_s)}"
build_dir.mkpath
begin
- Puppet.notice "Installing #{@filename.basename} to #{@module_dir.expand_path}"
unless system "tar xzf #{@filename} -C #{build_dir}"
raise RuntimeError, "Could not extract contents of module archive."
end
# grab the first directory
extracted = build_dir.children.detect { |c| c.directory? }
FileUtils.mv extracted, @module_dir
ensure
build_dir.rmtree
end
end
def delete_existing_installation_or_abort!
return unless @module_dir.exist?
-
- if !options[:force]
- Puppet.warning "Existing module '#{@module_dir.expand_path}' found"
- response = prompt "Overwrite module installed at #{@module_dir.expand_path}? [y/N]"
- unless response =~ /y/i
- raise RuntimeError, "Aborted installation."
- end
- end
-
- Puppet.warning "Deleting #{@module_dir.expand_path}"
FileUtils.rm_rf @module_dir
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/upgrader.rb b/lib/puppet/module_tool/applications/upgrader.rb
new file mode 100644
index 000000000..1a56a6deb
--- /dev/null
+++ b/lib/puppet/module_tool/applications/upgrader.rb
@@ -0,0 +1,109 @@
+module Puppet::Module::Tool
+ module Applications
+ class Upgrader < Application
+
+ include Puppet::Module::Tool::Errors
+
+ def initialize(name, options)
+ @action = :upgrade
+ @environment = Puppet::Node::Environment.new(Puppet.settings[:environment])
+ @module_name = name
+ @options = options
+ @force = options[:force]
+ @ignore_dependencies = options[:force] || options[:ignore_dependencies]
+ @version = options[:version]
+ end
+
+ def run
+ begin
+ results = { :module_name => @module_name }
+
+ get_local_constraints
+
+ if @installed[@module_name].length > 1
+ raise MultipleInstalledError,
+ :action => :upgrade,
+ :module_name => @module_name,
+ :installed_modules => @installed[@module_name].sort_by { |mod| @environment.modulepath.index(mod.modulepath) }
+ elsif @installed[@module_name].empty?
+ raise NotInstalledError,
+ :action => :upgrade,
+ :module_name => @module_name
+ end
+
+ @module = @installed[@module_name].last
+ results[:installed_version] = @module.version ? @module.version.sub(/^(?=\d)/, 'v') : nil
+ results[:requested_version] = @version || (@conditions[@module_name].empty? ? :latest : :best)
+ dir = @module.modulepath
+
+ Puppet.notice "Found '#{@module_name}' (#{colorize(:cyan, results[:installed_version] || '???')}) in #{dir} ..."
+ if !@options[:force] && @module.has_metadata? && @module.has_local_changes?
+ raise LocalChangesError,
+ :action => :upgrade,
+ :module_name => @module_name,
+ :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best),
+ :installed_version => @module.version
+ end
+
+ begin
+ get_remote_constraints
+ rescue => e
+ raise UnknownModuleError, results.merge(:repository => Puppet::Forge.repository.uri)
+ else
+ raise UnknownVersionError, results.merge(:repository => Puppet::Forge.repository.uri) if @remote.empty?
+ end
+
+ if !@options[:force] && @versions["#{@module_name}"].last[:vstring].sub(/^(?=\d)/, 'v') == (@module.version || '0.0.0').sub(/^(?=\d)/, 'v')
+ raise VersionAlreadyInstalledError,
+ :module_name => @module_name,
+ :requested_version => @version || ((@conditions[@module_name].empty? ? 'latest' : 'best') + ": #{@versions["#{@module_name}"].last[:vstring].sub(/^(?=\d)/, 'v')}"),
+ :installed_version => @installed[@module_name].last.version,
+ :conditions => @conditions[@module_name] + [{ :module => :you, :version => @version }]
+ end
+
+ @graph = resolve_constraints({ @module_name => @version })
+
+ # This clean call means we never "cache" the module we're installing, but this
+ # is desired since module authors can easily rerelease modules different content but the same
+ # version number, meaning someone with the old content cached will be very confused as to why
+ # they can't get new content.
+ # Long term we should just get rid of this caching behavior and cleanup downloaded modules after they install
+ # but for now this is a quick fix to disable caching
+ Puppet::Forge::Cache.clean
+ tarballs = download_tarballs(@graph, @graph.last[:path])
+
+ unless @graph.empty?
+ Puppet.notice 'Upgrading -- do not interrupt ...'
+ tarballs.each do |hash|
+ hash.each do |dir, path|
+ Unpacker.new(path, @options.merge(:target_dir => dir)).run
+ end
+ end
+ end
+
+ results[:result] = :success
+ results[:base_dir] = @graph.first[:path]
+ results[:affected_modules] = @graph
+ rescue VersionAlreadyInstalledError => e
+ results[:result] = :noop
+ results[:error] = {
+ :oneline => e.message,
+ :multiline => e.multiline
+ }
+ rescue => e
+ results[:error] = {
+ :oneline => e.message,
+ :multiline => e.respond_to?(:multiline) ? e.multiline : [e.to_s, e.backtrace].join("\n")
+ }
+ ensure
+ results[:result] ||= :failure
+ end
+
+ return results
+ end
+
+ private
+ include Puppet::Module::Tool::Shared
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors.rb b/lib/puppet/module_tool/errors.rb
new file mode 100644
index 000000000..b90e5ba97
--- /dev/null
+++ b/lib/puppet/module_tool/errors.rb
@@ -0,0 +1,9 @@
+module Puppet::Module::Tool
+ module Errors
+ require 'puppet/module_tool/errors/base'
+ require 'puppet/module_tool/errors/installer'
+ require 'puppet/module_tool/errors/uninstaller'
+ require 'puppet/module_tool/errors/upgrader'
+ require 'puppet/module_tool/errors/shared'
+ end
+end
diff --git a/lib/puppet/module_tool/errors/base.rb b/lib/puppet/module_tool/errors/base.rb
new file mode 100644
index 000000000..8ec3e7599
--- /dev/null
+++ b/lib/puppet/module_tool/errors/base.rb
@@ -0,0 +1,15 @@
+module Puppet::Module::Tool::Errors
+ class ModuleToolError < StandardError
+ def v(version)
+ (version || '???').to_s.sub(/^(?=\d)/, 'v')
+ end
+
+ def vstring
+ if @action == :upgrade
+ "#{v(@installed_version)} -> #{v(@requested_version)}"
+ else
+ "#{v(@installed_version || @requested_version)}"
+ end
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors/installer.rb b/lib/puppet/module_tool/errors/installer.rb
new file mode 100644
index 000000000..48b3b77bf
--- /dev/null
+++ b/lib/puppet/module_tool/errors/installer.rb
@@ -0,0 +1,90 @@
+module Puppet::Module::Tool::Errors
+
+ class InstallError < ModuleToolError; end
+
+ class AlreadyInstalledError < InstallError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @installed_version = v(options[:installed_version])
+ @requested_version = v(options[:requested_version])
+ @local_changes = options[:local_changes]
+ super "'#{@module_name}' (#{@requested_version}) requested; '#{@module_name}' (#{@installed_version}) already installed"
+ end
+
+ def multiline
+ message = []
+ message << "Could not install module '#{@module_name}' (#{@requested_version})"
+ message << " Module '#{@module_name}' (#{@installed_version}) is already installed"
+ message << " Installed module has had changes made locally" unless @local_changes.empty?
+ message << " Use `puppet module upgrade` to install a different version"
+ message << " Use `puppet module install --force` to re-install only this module"
+ message.join("\n")
+ end
+ end
+
+ class InstallConflictError < InstallError
+ def initialize(options)
+ @requested_module = options[:requested_module]
+ @requested_version = v(options[:requested_version])
+ @dependency = options[:dependency]
+ @directory = options[:directory]
+ @metadata = options[:metadata]
+ super "'#{@requested_module}' (#{@requested_version}) requested; Installation conflict"
+ end
+
+ def multiline
+ message = []
+ message << "Could not install module '#{@requested_module}' (#{@requested_version})"
+
+ if @dependency
+ message << " Dependency '#{@dependency[:name]}' (#{v(@dependency[:version])}) would overwrite #{@directory}"
+ else
+ message << " Installation would overwrite #{@directory}"
+ end
+
+ if @metadata
+ message << " Currently, '#{@metadata[:name]}' (#{v(@metadata[:version])}) is installed to that directory"
+ end
+
+ message << " Use `puppet module install --dir ` to install modules elsewhere"
+
+ if @dependency
+ message << " Use `puppet module install --ignore-dependencies` to install only this module"
+ else
+ message << " Use `puppet module install --force` to install this module anyway"
+ end
+
+ message.join("\n")
+ end
+ end
+
+ class MissingPackageError < InstallError
+ def initialize(options)
+ @requested_package = options[:requested_package]
+ super "#{@requested_package} requested; Package #{@requested_package} does not exist"
+ end
+
+ def multiline
+ <<-MSG.strip
+Could not install package #{@requested_package}
+ Package #{@requested_package} does not exist
+ MSG
+ end
+ end
+
+ class MissingInstallDirectoryError < InstallError
+ def initialize(options)
+ @requested_module = options[:requested_module]
+ @requested_version = options[:requested_version]
+ @directory = options[:directory]
+ super "'#{@requested_module}' (#{@requested_version}) requested; Directory #{@directory} does not exist"
+ end
+
+ def multiline
+ <<-MSG.strip
+Could not install module '#{@requested_module}' (#{@requested_version})
+ Directory #{@directory} does not exist
+ MSG
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors/shared.rb b/lib/puppet/module_tool/errors/shared.rb
new file mode 100644
index 000000000..f24d16bfe
--- /dev/null
+++ b/lib/puppet/module_tool/errors/shared.rb
@@ -0,0 +1,115 @@
+module Puppet::Module::Tool::Errors
+
+ class NoVersionsSatisfyError < ModuleToolError
+ def initialize(options)
+ @requested_name = options[:requested_name]
+ @requested_version = options[:requested_version]
+ @installed_version = options[:installed_version]
+ @dependency_name = options[:dependency_name]
+ @conditions = options[:conditions]
+ @action = options[:action]
+
+ super "Could not #{@action} '#{@requested_name}' (#{vstring}); module '#{@dependency_name}' cannot satisfy dependencies"
+ end
+
+ def multiline
+ same_mod = @requested_name == @dependency_name
+
+ message = []
+ message << "Could not #{@action} module '#{@requested_name}' (#{vstring})"
+ message << " No version of '#{@dependency_name}' will satisfy dependencies"
+ message << " You specified '#{@requested_name}' (#{v(@requested_version)})" if same_mod
+ message += @conditions.select { |c| c[:module] != :you }.sort_by { |c| c[:module] }.map do |c|
+ " '#{c[:module]}' (#{v(c[:version])}) requires '#{@dependency_name}' (#{v(c[:dependency])})"
+ end
+ message << " Use `puppet module #{@action} --force` to #{@action} this module anyway" if same_mod
+ message << " Use `puppet module #{@action} --ignore-dependencies` to #{@action} only this module" unless same_mod
+
+ message.join("\n")
+ end
+ end
+
+ class InvalidDependencyCycleError < ModuleToolError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @requested_module = options[:requested_module]
+ @requested_version = options[:requested_version]
+ @conditions = options[:conditions]
+ @source = options[:source][1..-1]
+
+ super "'#{@requested_module}' (#{v(@requested_version)}) requested; Invalid dependency cycle"
+ end
+
+ def multiline
+ trace = []
+ trace << "You specified '#{@source.first[:name]}' (#{v(@requested_version)})"
+ trace += @source[1..-1].map { |m| "which depends on '#{m[:name]}' (#{v(m[:version])})" }
+
+ message = []
+ message << "Could not install module '#{@requested_module}' (#{v(@requested_version)})"
+ message << " No version of '#{@module_name}' will satisfy dependencies"
+ message << trace.map { |s| " #{s}" }.join(",\n")
+ message << " Use `puppet module install --force` to install this module anyway"
+
+ message.join("\n")
+ end
+ end
+
+ class NotInstalledError < ModuleToolError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @suggestions = options[:suggestions] || []
+ @action = options[:action]
+ super "Could not #{@action} '#{@module_name}'; module is not installed"
+ end
+
+ def multiline
+ message = []
+ message << "Could not #{@action} module '#{@module_name}'"
+ message << " Module '#{@module_name}' is not installed"
+ message += @suggestions.map do |suggestion|
+ " You may have meant `puppet module #{@action} #{suggestion}`"
+ end
+ message << " Use `puppet module install` to install this module" if @action == :upgrade
+ message.join("\n")
+ end
+ end
+
+ class MultipleInstalledError < ModuleToolError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @modules = options[:installed_modules]
+ @action = options[:action]
+ super "Could not #{@action} '#{@module_name}'; module appears in multiple places in the module path"
+ end
+
+ def multiline
+ message = []
+ message << "Could not #{@action} module '#{@module_name}'"
+ message << " Module '#{@module_name}' appears multiple places in the module path"
+ message += @modules.map do |mod|
+ " '#{@module_name}' (#{v(mod.version)}) was found in #{mod.modulepath}"
+ end
+ message << " Use the `--modulepath` option to limit the search to specific directories"
+ message.join("\n")
+ end
+ end
+
+ class LocalChangesError < ModuleToolError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @requested_version = options[:requested_version]
+ @installed_version = options[:installed_version]
+ @action = options[:action]
+ super "Could not #{@action} '#{@module_name}'; module is not installed"
+ end
+
+ def multiline
+ message = []
+ message << "Could not #{@action} module '#{@module_name}' (#{vstring})"
+ message << " Installed module has had changes made locally"
+ message << " Use `puppet module #{@action} --force` to #{@action} this module anyway"
+ message.join("\n")
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors/uninstaller.rb b/lib/puppet/module_tool/errors/uninstaller.rb
new file mode 100644
index 000000000..3beee7872
--- /dev/null
+++ b/lib/puppet/module_tool/errors/uninstaller.rb
@@ -0,0 +1,45 @@
+module Puppet::Module::Tool::Errors
+
+ class UninstallError < ModuleToolError; end
+
+ class NoVersionMatchesError < UninstallError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @modules = options[:installed_modules]
+ @version = options[:version_range]
+ super "Could not uninstall '#{@module_name}'; no installed version matches"
+ end
+
+ def multiline
+ message = []
+ message << "Could not uninstall module '#{@module_name}' (#{v(@version)})"
+ message << " No installed version of '#{@module_name}' matches (#{v(@version)})"
+ message += @modules.map do |mod|
+ " '#{mod[:name]}' (#{v(mod[:version])}) is installed in #{mod[:path]}"
+ end
+ message.join("\n")
+ end
+ end
+
+ class ModuleIsRequiredError < UninstallError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @required_by = options[:required_by]
+ @requested_version = options[:requested_version]
+ @installed_version = options[:installed_version]
+
+ super "Could not uninstall '#{@module_name}'; installed modules still depend upon it"
+ end
+
+ def multiline
+ message = []
+ message << ("Could not uninstall module '#{@module_name}'" << (@requested_version ? " (#{v(@requested_version)})" : ''))
+ message << " Other installed modules have dependencies on '#{@module_name}' (#{v(@installed_version)})"
+ message += @required_by.map do |mod|
+ " '#{mod['name']}' (#{v(mod['version'])}) requires '#{@module_name}' (#{v(mod['version_requirement'])})"
+ end
+ message << " Use `puppet module uninstall --force` to uninstall this module anyway"
+ message.join("\n")
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/errors/upgrader.rb b/lib/puppet/module_tool/errors/upgrader.rb
new file mode 100644
index 000000000..abc8375b8
--- /dev/null
+++ b/lib/puppet/module_tool/errors/upgrader.rb
@@ -0,0 +1,72 @@
+module Puppet::Module::Tool::Errors
+
+ class UpgradeError < ModuleToolError
+ def initialize(msg)
+ @action = :upgrade
+ super
+ end
+ end
+
+ class VersionAlreadyInstalledError < UpgradeError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @requested_version = options[:requested_version]
+ @installed_version = options[:installed_version]
+ @dependency_name = options[:dependency_name]
+ @conditions = options[:conditions]
+ super "Could not upgrade '#{@module_name}'; module is not installed"
+ end
+
+ def multiline
+ message = []
+ message << "Could not upgrade module '#{@module_name}' (#{vstring})"
+ if @conditions.length == 1 && @conditions.last[:version].nil?
+ message << " The installed version is already the latest version"
+ else
+ message << " The installed version is already the best fit for the current dependencies"
+ message += @conditions.select { |c| c[:module] == :you && c[:version] }.map do |c|
+ " You specified '#{@module_name}' (#{v(c[:version])})"
+ end
+ message += @conditions.select { |c| c[:module] != :you }.sort_by { |c| c[:module] }.map do |c|
+ " '#{c[:module]}' (#{v(c[:version])}) requires '#{@module_name}' (#{v(c[:dependency])})"
+ end
+ end
+ message << " Use `puppet module install --force` to re-install this module"
+ message.join("\n")
+ end
+ end
+
+ class UnknownModuleError < UpgradeError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @installed_version = options[:installed_version]
+ @requested_version = options[:requested_version]
+ @repository = options[:repository]
+ super "Could not upgrade '#{@module_name}'; module is unknown to #{@repository}"
+ end
+
+ def multiline
+ message = []
+ message << "Could not upgrade module '#{@module_name}' (#{vstring})"
+ message << " Module '#{@module_name}' does not exist on #{@repository}"
+ message.join("\n")
+ end
+ end
+
+ class UnknownVersionError < UpgradeError
+ def initialize(options)
+ @module_name = options[:module_name]
+ @installed_version = options[:installed_version]
+ @requested_version = options[:requested_version]
+ @repository = options[:repository]
+ super "Could not upgrade '#{@module_name}' (#{vstring}); module has no versions #{ @requested_version && "matching #{v(@requested_version)} "}published on #{@repository}"
+ end
+
+ def multiline
+ message = []
+ message << "Could not upgrade module '#{@module_name}' (#{vstring})"
+ message << " No version matching '#{@requested_version || ">= 0.0.0"}' exists on #{@repository}"
+ message.join("\n")
+ end
+ end
+end
diff --git a/lib/puppet/module_tool/shared_behaviors.rb b/lib/puppet/module_tool/shared_behaviors.rb
new file mode 100644
index 000000000..dae588e34
--- /dev/null
+++ b/lib/puppet/module_tool/shared_behaviors.rb
@@ -0,0 +1,161 @@
+module Puppet::Module::Tool::Shared
+
+ include Puppet::Module::Tool::Errors
+
+ def get_local_constraints
+ @local = Hash.new { |h,k| h[k] = { } }
+ @conditions = Hash.new { |h,k| h[k] = [] }
+ @installed = Hash.new { |h,k| h[k] = [] }
+
+ @environment.modules_by_path.values.flatten.each do |mod|
+ mod_name = (mod.forge_name || mod.name).gsub('/', '-')
+ @installed[mod_name] << mod
+ d = @local["#{mod_name}@#{mod.version}"]
+ (mod.dependencies || []).each do |hash|
+ name, conditions = hash['name'], hash['version_requirement']
+ name = name.gsub('/', '-')
+ d[name] = conditions
+ @conditions[name] << {
+ :module => mod_name,
+ :version => mod.version,
+ :dependency => conditions
+ }
+ end
+ end
+ end
+
+ def get_remote_constraints
+ @remote = Hash.new { |h,k| h[k] = { } }
+ @urls = {}
+ @versions = Hash.new { |h,k| h[k] = [] }
+
+ Puppet.notice "Downloading from #{Puppet::Forge.repository.uri} ..."
+ author, modname = Puppet::Module::Tool.username_and_modname_from(@module_name)
+ info = Puppet::Forge.remote_dependency_info(author, modname, @options[:version])
+ info.each do |pair|
+ mod_name, releases = pair
+ mod_name = mod_name.gsub('/', '-')
+ releases.each do |rel|
+ semver = SemVer.new(rel['version'] || '0.0.0') rescue SemVer.MIN
+ @versions[mod_name] << { :vstring => rel['version'], :semver => semver }
+ @versions[mod_name].sort! { |a, b| a[:semver] <=> b[:semver] }
+ @urls["#{mod_name}@#{rel['version']}"] = rel['file']
+ d = @remote["#{mod_name}@#{rel['version']}"]
+ (rel['dependencies'] || []).each do |name, conditions|
+ d[name.gsub('/', '-')] = conditions
+ end
+ end
+ end
+ end
+
+ def implicit_version(mod)
+ return :latest if @conditions[mod].empty?
+ if @conditions[mod].all? { |c| c[:queued] || c[:module] == :you }
+ return :latest
+ end
+ return :best
+ end
+
+ def annotated_version(mod, versions)
+ if versions.empty?
+ return implicit_version(mod)
+ else
+ return "#{implicit_version(mod)}: #{versions.last}"
+ end
+ end
+
+ def resolve_constraints(dependencies, source = [{:name => :you}], seen = {}, action = @action)
+ dependencies = dependencies.map do |mod, range|
+ source.last[:dependency] = range
+
+ @conditions[mod] << {
+ :module => source.last[:name],
+ :version => source.last[:version],
+ :dependency => range,
+ :queued => true
+ }
+
+ if @force
+ range = SemVer[@version] rescue SemVer['>= 0.0.0']
+ else
+ range = (@conditions[mod]).map do |r|
+ SemVer[r[:dependency]] rescue SemVer['>= 0.0.0']
+ end.inject(&:&)
+ end
+
+ if @action == :install && seen.include?(mod)
+ next if range === seen[mod][:semver]
+
+ req_module = @module_name
+ req_versions = @versions["#{@module_name}"].map { |v| v[:semver] }
+ raise InvalidDependencyCycleError,
+ :module_name => mod,
+ :source => (source + [{ :name => mod, :version => source.last[:dependency] }]),
+ :requested_module => req_module,
+ :requested_version => @version || annotated_version(req_module, req_versions),
+ :conditions => @conditions
+ end
+
+ if !(@force || @installed[mod].empty? || source.last[:name] == :you)
+ next if range === SemVer.new(@installed[mod].first.version)
+ action = :upgrade
+ elsif @installed[mod].empty?
+ action = :install
+ end
+
+ if action == :upgrade
+ @conditions.each { |_, conds| conds.delete_if { |c| c[:module] == mod } }
+ end
+
+ valid_versions = @versions["#{mod}"].select { |h| range === h[:semver] }
+
+ unless version = valid_versions.last
+ req_module = @module_name
+ req_versions = @versions["#{@module_name}"].map { |v| v[:semver] }
+ raise NoVersionsSatisfyError,
+ :requested_name => req_module,
+ :requested_version => @version || annotated_version(req_module, req_versions),
+ :installed_version => @installed[@module_name].empty? ? nil : @installed[@module_name].first.version,
+ :dependency_name => mod,
+ :conditions => @conditions[mod],
+ :action => @action
+ end
+
+ seen[mod] = version
+
+ {
+ :module => mod,
+ :version => version,
+ :action => action,
+ :previous_version => @installed[mod].empty? ? nil : @installed[mod].first.version,
+ :file => @urls["#{mod}@#{version[:vstring]}"],
+ :path => action == :install ? @options[:target_dir] : (@installed[mod].empty? ? @options[:target_dir] : @installed[mod].first.modulepath),
+ :dependencies => []
+ }
+ end.compact
+ dependencies.each do |mod|
+ deps = @remote["#{mod[:module]}@#{mod[:version][:vstring]}"].sort_by(&:first)
+ mod[:dependencies] = resolve_constraints(deps, source + [{ :name => mod[:module], :version => mod[:version][:vstring] }], seen, :install)
+ end unless @ignore_dependencies
+ return dependencies
+ end
+
+ def download_tarballs(graph, default_path)
+ graph.map do |release|
+ begin
+ if release[:tarball]
+ cache_path = Pathname(release[:tarball])
+ else
+ cache_path = Puppet::Forge.repository.retrieve(release[:file])
+ end
+ rescue OpenURI::HTTPError => e
+ raise RuntimeError, "Could not download module: #{e.message}"
+ end
+
+ [
+ { (release[:path] ||= default_path) => cache_path},
+ *download_tarballs(release[:dependencies], default_path)
+ ]
+ end.flatten
+ end
+end
diff --git a/lib/puppet/module_tool/skeleton/templates/generator/metadata.json b/lib/puppet/module_tool/skeleton/templates/generator/metadata.json
deleted file mode 100644
index 8ce7797ff..000000000
--- a/lib/puppet/module_tool/skeleton/templates/generator/metadata.json
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
-+-----------------------------------------------------------------------+
-| |
-| ==> DO NOT EDIT THIS FILE! <== |
-| |
-| You should edit the `Modulefile` and run `puppet-module build` |
-| to generate the `metadata.json` file for your releases. |
-| |
-+-----------------------------------------------------------------------+
-*/
-
-{}
diff --git a/lib/puppet/module_tool/utils.rb b/lib/puppet/module_tool/utils.rb
deleted file mode 100644
index 85f57c973..000000000
--- a/lib/puppet/module_tool/utils.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Puppet::Module::Tool
- module Utils
- require 'puppet/module_tool/utils/interrogation'
- end
-end
diff --git a/lib/puppet/module_tool/utils/interrogation.rb b/lib/puppet/module_tool/utils/interrogation.rb
deleted file mode 100644
index 19450dedd..000000000
--- a/lib/puppet/module_tool/utils/interrogation.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Puppet::Module::Tool
- module Utils
-
- # = Interrogation
- #
- # This module contains methods to emit questions to the console.
- module Interrogation
- def confirms?(question)
- $stderr.print "#{question} [y/N]: "
- $stdin.gets =~ /y/i
- end
-
- def prompt(question, quiet = false)
- $stderr.print "#{question}: "
- system 'stty -echo' if quiet
- $stdin.gets.strip
- ensure
- if quiet
- system 'stty echo'
- say "\n---------"
- end
- end
- end
- end
-end
diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb
index fc36c357d..aca3c6bfb 100644
--- a/lib/puppet/node/environment.rb
+++ b/lib/puppet/node/environment.rb
@@ -1,223 +1,225 @@
require 'puppet/util'
require 'puppet/util/cacher'
require 'monitor'
# Just define it, so this class has fewer load dependencies.
class Puppet::Node
end
# Model the environment that a node can operate in. This class just
# provides a simple wrapper for the functionality around environments.
class Puppet::Node::Environment
module Helper
def environment
Puppet::Node::Environment.new(@environment)
end
def environment=(env)
if env.is_a?(String) or env.is_a?(Symbol)
@environment = env
else
@environment = env.name
end
end
end
include Puppet::Util::Cacher
@seen = {}
# Return an existing environment instance, or create a new one.
def self.new(name = nil)
return name if name.is_a?(self)
name ||= Puppet.settings.value(:environment)
raise ArgumentError, "Environment name must be specified" unless name
symbol = name.to_sym
return @seen[symbol] if @seen[symbol]
obj = self.allocate
obj.send :initialize, symbol
@seen[symbol] = obj
end
def self.current
Thread.current[:environment] || root
end
def self.current=(env)
Thread.current[:environment] = new(env)
end
def self.root
@root
end
def self.clear
@seen.clear
end
attr_reader :name
# Return an environment-specific setting.
def [](param)
Puppet.settings.value(param, self.name)
end
def initialize(name)
@name = name
extend MonitorMixin
end
def known_resource_types
# This makes use of short circuit evaluation to get the right thread-safe
# per environment semantics with an efficient most common cases; we almost
# always just return our thread's known-resource types. Only at the start
# of a compilation (after our thread var has been set to nil) or when the
# environment has changed do we delve deeper.
Thread.current[:known_resource_types] = nil if (krt = Thread.current[:known_resource_types]) && krt.environment != self
Thread.current[:known_resource_types] ||= synchronize {
if @known_resource_types.nil? or @known_resource_types.require_reparse?
@known_resource_types = Puppet::Resource::TypeCollection.new(self)
@known_resource_types.import_ast(perform_initial_import, '')
end
@known_resource_types
}
end
def module(name)
mod = Puppet::Module.new(name, :environment => self)
return nil unless mod.exist?
mod
end
def module_by_forge_name(forge_name)
author, modname = forge_name.split('/')
found_mod = self.module(modname)
found_mod and found_mod.forge_name == forge_name ?
found_mod :
nil
end
# Cache the modulepath, so that we aren't searching through
# all known directories all the time.
cached_attr(:modulepath, Puppet[:filetimeout]) do
dirs = self[:modulepath].split(File::PATH_SEPARATOR)
dirs = ENV["PUPPETLIB"].split(File::PATH_SEPARATOR) + dirs if ENV["PUPPETLIB"]
validate_dirs(dirs)
end
# Return all modules from this environment.
# Cache the list, because it can be expensive to create.
cached_attr(:modules, Puppet[:filetimeout]) do
module_names =
modulepath.collect do |path|
module_names = Dir.entries(path)
Puppet.debug("Warning: Found directory named 'lib' in module path ('#{path}/lib'); unless " +
"you are expecting to load a module named 'lib', your module path may be set " +
"incorrectly.") if module_names.include?("lib")
module_names
end .flatten.uniq
module_names.collect do |path|
begin
Puppet::Module.new(path, :environment => self)
rescue Puppet::Module::Error => e
nil
end
end.compact
end
# Modules broken out by directory in the modulepath
def modules_by_path
modules_by_path = {}
modulepath.each do |path|
Dir.chdir(path) do
- module_names = Dir.glob('*').select { |d| FileTest.directory? d }
- modules_by_path[path] = module_names.map do |name|
+ module_names = Dir.glob('*').select do |d|
+ FileTest.directory?(d) && (File.basename(d) =~ /^[-\w]+$/)
+ end
+ modules_by_path[path] = module_names.sort.map do |name|
Puppet::Module.new(name, :environment => self, :path => File.join(path, name))
end
end
end
modules_by_path
end
def module_requirements
deps = {}
modules.each do |mod|
next unless mod.forge_name
deps[mod.forge_name] ||= []
mod.dependencies and mod.dependencies.each do |mod_dep|
deps[mod_dep['name']] ||= []
dep_details = {
'name' => mod.forge_name,
'version' => mod.version,
'version_requirement' => mod_dep['version_requirement']
}
deps[mod_dep['name']] << dep_details
end
end
deps.each do |mod, mod_deps|
deps[mod] = mod_deps.sort_by {|d| d['name']}
end
deps
end
def to_s
name.to_s
end
def to_sym
to_s.to_sym
end
# The only thing we care about when serializing an environment is its
# identity; everything else is ephemeral and should not be stored or
# transmitted.
def to_zaml(z)
self.to_s.to_zaml(z)
end
def validate_dirs(dirs)
dirs.collect do |dir|
unless Puppet::Util.absolute_path?(dir)
File.expand_path(File.join(Dir.getwd, dir))
else
dir
end
end.find_all do |p|
Puppet::Util.absolute_path?(p) && FileTest.directory?(p)
end
end
private
def perform_initial_import
return empty_parse_result if Puppet.settings[:ignoreimport]
parser = Puppet::Parser::Parser.new(self)
if code = Puppet.settings.uninterpolated_value(:code, name.to_s) and code != ""
parser.string = code
else
file = Puppet.settings.value(:manifest, name.to_s)
parser.file = file
end
parser.parse
rescue => detail
known_resource_types.parse_failed = true
msg = "Could not parse for environment #{self}: #{detail}"
error = Puppet::Error.new(msg)
error.set_backtrace(detail.backtrace)
raise error
end
def empty_parse_result
# Return an empty toplevel hostclass to use as the result of
# perform_initial_import when no file was actually loaded.
return Puppet::Parser::AST::Hostclass.new('')
end
@root = new(:'*root*')
end
diff --git a/lib/puppet/parser/functions/create_resources.rb b/lib/puppet/parser/functions/create_resources.rb
index 3c91b4111..9a5304dec 100644
--- a/lib/puppet/parser/functions/create_resources.rb
+++ b/lib/puppet/parser/functions/create_resources.rb
@@ -1,75 +1,76 @@
Puppet::Parser::Functions::newfunction(:create_resources, :doc => <<-'ENDHEREDOC') do |args|
Converts a hash into a set of resources and adds them to the catalog.
This function takes two mandatory arguments: a resource type, and a hash describing
a set of resources. The hash should be in the form `{title => {parameters} }`:
# A hash of user resources:
$myusers = {
'nick' => { uid => '1330',
group => allstaff,
groups => ['developers', 'operations', 'release'], }
'dan' => { uid => '1308',
group => allstaff,
groups => ['developers', 'prosvc', 'release'], }
}
create_resources(user, $myusers)
A third, optional parameter may be given, also as a hash:
$defaults => {
'ensure' => present,
'provider' => 'ldap',
}
create_resources(user, $myusers, $defaults)
The values given on the third argument are added to the parameters of each resource
present in the set given on the second argument. If a parameter is present on both
the second and third arguments, the one on the second argument takes precedence.
This function can be used to create defined resources and classes, as well
as native resources.
ENDHEREDOC
raise ArgumentError, ("create_resources(): wrong number of arguments (#{args.length}; must be 2 or 3)") if args.length < 2 || args.length > 3
# figure out what kind of resource we are
type_of_resource = nil
type_name = args[0].downcase
if type_name == 'class'
type_of_resource = :class
else
if resource = Puppet::Type.type(type_name.to_sym)
type_of_resource = :type
elsif resource = find_definition(type_name.downcase)
type_of_resource = :define
else
raise ArgumentError, "could not create resource of unknown type #{type_name}"
end
end
# iterate through the resources to create
defaults = args[2] || {}
args[1].each do |title, params|
- raise ArgumentError, 'params should not contain title' if(params['title'])
params = defaults.merge(params)
+ Puppet::Util.symbolizehash!(params)
+ raise ArgumentError, 'params should not contain title' if(params[:title])
case type_of_resource
# JJM The only difference between a type and a define is the call to instantiate_resource
# for a defined type.
when :type, :define
p_resource = Puppet::Parser::Resource.new(type_name, title, :scope => self, :source => resource)
- params.merge(:name => title).each do |k,v|
+ {:name => title}.merge(params).each do |k,v|
p_resource.set_parameter(k,v)
end
if type_of_resource == :define then
resource.instantiate_resource(self, p_resource)
end
compiler.add_resource(self, p_resource)
when :class
klass = find_hostclass(title)
raise ArgumentError, "could not find hostclass #{title}" unless klass
klass.ensure_in_catalog(self, params)
compiler.catalog.add_class(title)
end
end
end
diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb
index 083c9a60e..5281d59a3 100644
--- a/lib/puppet/provider/nameservice/directoryservice.rb
+++ b/lib/puppet/provider/nameservice/directoryservice.rb
@@ -1,592 +1,598 @@
require 'puppet'
require 'puppet/provider/nameservice'
require 'facter/util/plist'
require 'fileutils'
class Puppet::Provider::NameService
class DirectoryService < Puppet::Provider::NameService
# JJM: Dive into the singleton_class
class << self
# JJM: This allows us to pass information when calling
# Puppet::Type.type
# e.g. Puppet::Type.type(:user).provide :directoryservice, :ds_path => "Users"
# This is referenced in the get_ds_path class method
attr_writer :ds_path
attr_writer :macosx_version_major
end
initvars
commands :dscl => "/usr/bin/dscl"
commands :dseditgroup => "/usr/sbin/dseditgroup"
commands :sw_vers => "/usr/bin/sw_vers"
commands :plutil => '/usr/bin/plutil'
confine :operatingsystem => :darwin
defaultfor :operatingsystem => :darwin
# JJM 2007-07-25: This map is used to map NameService attributes to their
# corresponding DirectoryService attribute names.
# See: http://images.apple.com/server/docs.Open_Directory_v10.4.pdf
# JJM: Note, this is de-coupled from the Puppet::Type, and must
# be actively maintained. There may also be collisions with different
# types (Users, Groups, Mounts, Hosts, etc...)
def ds_to_ns_attribute_map; self.class.ds_to_ns_attribute_map; end
def self.ds_to_ns_attribute_map
{
'RecordName' => :name,
'PrimaryGroupID' => :gid,
'NFSHomeDirectory' => :home,
'UserShell' => :shell,
'UniqueID' => :uid,
'RealName' => :comment,
'Password' => :password,
'GeneratedUID' => :guid,
'IPAddress' => :ip_address,
'ENetAddress' => :en_address,
'GroupMembership' => :members,
}
end
# JJM The same table as above, inverted.
def ns_to_ds_attribute_map; self.class.ns_to_ds_attribute_map end
def self.ns_to_ds_attribute_map
@ns_to_ds_attribute_map ||= ds_to_ns_attribute_map.invert
end
def self.password_hash_dir
'/var/db/shadow/hash'
end
def self.users_plist_dir
'/var/db/dslocal/nodes/Default/users'
end
def self.instances
# JJM Class method that provides an array of instance objects of this
# type.
# JJM: Properties are dependent on the Puppet::Type we're managine.
type_property_array = [:name] + @resource_type.validproperties
# Create a new instance of this Puppet::Type for each object present
# on the system.
list_all_present.collect do |name_string|
self.new(single_report(name_string, *type_property_array))
end
end
def self.get_ds_path
# JJM: 2007-07-24 This method dynamically returns the DS path we're concerned with.
# For example, if we're working with an user type, this will be /Users
# with a group type, this will be /Groups.
# @ds_path is an attribute of the class itself.
return @ds_path if defined?(@ds_path)
# JJM: "Users" or "Groups" etc ... (Based on the Puppet::Type)
# Remember this is a class method, so self.class is Class
# Also, @resource_type seems to be the reference to the
# Puppet::Type this class object is providing for.
@resource_type.name.to_s.capitalize + "s"
end
def self.get_macosx_version_major
return @macosx_version_major if defined?(@macosx_version_major)
begin
# Make sure we've loaded all of the facts
Facter.loadfacts
if Facter.value(:macosx_productversion_major)
product_version_major = Facter.value(:macosx_productversion_major)
else
# TODO: remove this code chunk once we require Facter 1.5.5 or higher.
Puppet.warning("DEPRECATION WARNING: Future versions of the directoryservice provider will require Facter 1.5.5 or newer.")
product_version = Facter.value(:macosx_productversion)
fail("Could not determine OS X version from Facter") if product_version.nil?
product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".")
end
fail("#{product_version_major} is not supported by the directoryservice provider") if %w{10.0 10.1 10.2 10.3 10.4}.include?(product_version_major)
@macosx_version_major = product_version_major
return @macosx_version_major
rescue Puppet::ExecutionFailure => detail
fail("Could not determine OS X version: #{detail}")
end
end
def self.list_all_present
# JJM: List all objects of this Puppet::Type already present on the system.
begin
dscl_output = execute(get_exec_preamble("-list"))
rescue Puppet::ExecutionFailure => detail
fail("Could not get #{@resource_type.name} list from DirectoryService")
end
dscl_output.split("\n")
end
def self.parse_dscl_plist_data(dscl_output)
Plist.parse_xml(dscl_output)
end
def self.generate_attribute_hash(input_hash, *type_properties)
attribute_hash = {}
input_hash.keys.each do |key|
ds_attribute = key.sub("dsAttrTypeStandard:", "")
next unless (ds_to_ns_attribute_map.keys.include?(ds_attribute) and type_properties.include? ds_to_ns_attribute_map[ds_attribute])
ds_value = input_hash[key]
case ds_to_ns_attribute_map[ds_attribute]
when :members
ds_value = ds_value # only members uses arrays so far
when :gid, :uid
# OS X stores objects like uid/gid as strings.
# Try casting to an integer for these cases to be
# consistent with the other providers and the group type
# validation
begin
ds_value = Integer(ds_value[0])
rescue ArgumentError
ds_value = ds_value[0]
end
else ds_value = ds_value[0]
end
attribute_hash[ds_to_ns_attribute_map[ds_attribute]] = ds_value
end
# NBK: need to read the existing password here as it's not actually
# stored in the user record. It is stored at a path that involves the
# UUID of the user record for non-Mobile local acccounts.
# Mobile Accounts are out of scope for this provider for now
attribute_hash[:password] = self.get_password(attribute_hash[:guid], attribute_hash[:name]) if @resource_type.validproperties.include?(:password) and Puppet.features.root?
attribute_hash
end
def self.single_report(resource_name, *type_properties)
# JJM 2007-07-24:
# Given a the name of an object and a list of properties of that
# object, return all property values in a hash.
#
# This class method returns nil if the object doesn't exist
# Otherwise, it returns a hash of the object properties.
all_present_str_array = list_all_present
# NBK: shortcut the process if the resource is missing
return nil unless all_present_str_array.include? resource_name
dscl_vector = get_exec_preamble("-read", resource_name)
begin
dscl_output = execute(dscl_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not get report. command execution failed.")
end
# (#11593) Remove support for OS X 10.4 and earlier
fail_if_wrong_version
dscl_plist = self.parse_dscl_plist_data(dscl_output)
self.generate_attribute_hash(dscl_plist, *type_properties)
end
def self.fail_if_wrong_version
fail("Puppet does not support OS X versions < 10.5") unless self.get_macosx_version_major >= "10.5"
end
def self.get_exec_preamble(ds_action, resource_name = nil)
# JJM 2007-07-24
# DSCL commands are often repetitive and contain the same positional
# arguments over and over. See http://developer.apple.com/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/chapter_10_section_9.html
# for an example of what I mean.
# This method spits out proper DSCL commands for us.
# We EXPECT name to be @resource[:name] when called from an instance object.
# (#11593) Remove support for OS X 10.4 and earlier
fail_if_wrong_version
command_vector = [ command(:dscl), "-plist", "." ]
# JJM: The actual action to perform. See "man dscl"
# Common actiosn: -create, -delete, -merge, -append, -passwd
command_vector << ds_action
# JJM: get_ds_path will spit back "Users" or "Groups",
# etc... Depending on the Puppet::Type of our self.
if resource_name
command_vector << "/#{get_ds_path}/#{resource_name}"
else
command_vector << "/#{get_ds_path}"
end
# JJM: This returns most of the preamble of the command.
# e.g. 'dscl / -create /Users/mccune'
command_vector
end
def self.set_password(resource_name, guid, password_hash)
# Use Puppet::Util::Package.versioncmp() to catch the scenario where a
# version '10.10' would be < '10.7' with simple string comparison. This
# if-statement only executes if the current version is less-than 10.7
if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1)
password_hash_file = "#{password_hash_dir}/#{guid}"
begin
File.open(password_hash_file, 'w') { |f| f.write(password_hash)}
rescue Errno::EACCES => detail
fail("Could not write to password hash file: #{detail}")
end
# NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of
# ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it
# will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if
# missing. Thus we make sure we only set ;ShadowHash; if it is missing, and
# we can do this with the merge command. This allows people to continue to
# use other custom AuthenticationAuthority attributes without stomping on them.
#
# There is a potential problem here in that we're only doing this when setting
# the password, and the attribute could get modified at other times while the
# hash doesn't change and so this doesn't get called at all... but
# without switching all the other attributes to merge instead of create I can't
# see a simple enough solution for this that doesn't modify the user record
# every single time. This should be a rather rare edge case. (famous last words)
dscl_vector = self.get_exec_preamble("-merge", resource_name)
dscl_vector << "AuthenticationAuthority" << ";ShadowHash;"
begin
dscl_output = execute(dscl_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not set AuthenticationAuthority.")
end
else
# 10.7 uses salted SHA512 password hashes which are 128 characters plus
# an 8 character salt. Previous versions used a SHA1 hash padded with
# zeroes. If someone attempts to use a password hash that worked with
# a previous version of OX X, we will fail early and warn them.
if password_hash.length != 136
fail("OS X 10.7 requires a Salted SHA512 hash password of 136 characters. \
Please check your password and try again.")
end
if File.exists?("#{users_plist_dir}/#{resource_name}.plist")
# If a plist already exists in /var/db/dslocal/nodes/Default/users, then
# we will need to extract the binary plist from the 'ShadowHashData'
# key, log the new password into the resultant plist's 'SALTED-SHA512'
# key, and then save the entire structure back.
users_plist = Plist::parse_xml(plutil( '-convert', 'xml1', '-o', '/dev/stdout', \
"#{users_plist_dir}/#{resource_name}.plist"))
# users_plist['ShadowHashData'][0].string is actually a binary plist
# that's nested INSIDE the user's plist (which itself is a binary
- # plist).
- password_hash_plist = users_plist['ShadowHashData'][0].string
- converted_hash_plist = convert_binary_to_xml(password_hash_plist)
+ # plist). If we encounter a user plist that DOESN'T have a
+ # ShadowHashData field, create one.
+ if users_plist['ShadowHashData']
+ password_hash_plist = users_plist['ShadowHashData'][0].string
+ converted_hash_plist = convert_binary_to_xml(password_hash_plist)
+ else
+ users_plist['ShadowHashData'] = [StringIO.new]
+ converted_hash_plist = {'SALTED-SHA512' => StringIO.new}
+ end
# converted_hash_plist['SALTED-SHA512'].string expects a Base64 encoded
# string. The password_hash provided as a resource attribute is a
# hex value. We need to convert the provided hex value to a Base64
# encoded string to nest it in the converted hash plist.
converted_hash_plist['SALTED-SHA512'].string = \
password_hash.unpack('a2'*(password_hash.size/2)).collect { |i| i.hex.chr }.join
# Finally, we can convert the nested plist back to binary, embed it
# into the user's plist, and convert the resultant plist back to
# a binary plist.
changed_plist = convert_xml_to_binary(converted_hash_plist)
users_plist['ShadowHashData'][0].string = changed_plist
Plist::Emit.save_plist(users_plist, "#{users_plist_dir}/#{resource_name}.plist")
plutil('-convert', 'binary1', "#{users_plist_dir}/#{resource_name}.plist")
end
end
end
def self.get_password(guid, username)
# Use Puppet::Util::Package.versioncmp() to catch the scenario where a
# version '10.10' would be < '10.7' with simple string comparison. This
- # if-statement only executes if the current version is less-than 10.7
+ # if-statement only executes if the current version is less-than 10.7
if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1)
password_hash = nil
password_hash_file = "#{password_hash_dir}/#{guid}"
if File.exists?(password_hash_file) and File.file?(password_hash_file)
fail("Could not read password hash file at #{password_hash_file}") if not File.readable?(password_hash_file)
f = File.new(password_hash_file)
password_hash = f.read
f.close
end
password_hash
else
if File.exists?("#{users_plist_dir}/#{username}.plist")
# If a plist exists in /var/db/dslocal/nodes/Default/users, we will
# extract the binary plist from the 'ShadowHashData' key, decode the
# salted-SHA512 password hash, and then return it.
users_plist = Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{username}.plist"))
if users_plist['ShadowHashData']
# users_plist['ShadowHashData'][0].string is actually a binary plist
# that's nested INSIDE the user's plist (which itself is a binary
# plist).
password_hash_plist = users_plist['ShadowHashData'][0].string
converted_hash_plist = convert_binary_to_xml(password_hash_plist)
# converted_hash_plist['SALTED-SHA512'].string is a Base64 encoded
# string. The password_hash provided as a resource attribute is a
# hex value. We need to convert the Base64 encoded string to a
# hex value and provide it back to Puppet.
password_hash = converted_hash_plist['SALTED-SHA512'].string.unpack("H*")[0]
password_hash
end
end
end
end
# This method will accept a hash that has been returned from Plist::parse_xml
# and convert it to a binary plist (string value).
def self.convert_xml_to_binary(plist_data)
Puppet.debug('Converting XML plist to binary')
Puppet.debug('Executing: \'plutil -convert binary1 -o - -\'')
IO.popen('plutil -convert binary1 -o - -', mode='r+') do |io|
io.write plist_data.to_plist
io.close_write
@converted_plist = io.read
end
@converted_plist
end
# This method will accept a binary plist (as a string) and convert it to a
# hash via Plist::parse_xml.
def self.convert_binary_to_xml(plist_data)
Puppet.debug('Converting binary plist to XML')
Puppet.debug('Executing: \'plutil -convert xml1 -o - -\'')
IO.popen('plutil -convert xml1 -o - -', mode='r+') do |io|
io.write plist_data
io.close_write
@converted_plist = io.read
end
Puppet.debug('Converting XML values to a hash.')
@plist_hash = Plist::parse_xml(@converted_plist)
@plist_hash
end
# Unlike most other *nixes, OS X doesn't provide built in functionality
# for automatically assigning uids and gids to accounts, so we set up these
# methods for consumption by functionality like --mkusers
# By default we restrict to a reasonably sane range for system accounts
def self.next_system_id(id_type, min_id=20)
dscl_args = ['.', '-list']
if id_type == 'uid'
dscl_args << '/Users' << 'uid'
elsif id_type == 'gid'
dscl_args << '/Groups' << 'gid'
else
fail("Invalid id_type #{id_type}. Only 'uid' and 'gid' supported")
end
dscl_out = dscl(dscl_args)
# We're ok with throwing away negative uids here.
ids = dscl_out.split.compact.collect { |l| l.to_i if l.match(/^\d+$/) }
ids.compact!.sort! { |a,b| a.to_f <=> b.to_f }
# We're just looking for an unused id in our sorted array.
ids.each_index do |i|
next_id = ids[i] + 1
return next_id if ids[i+1] != next_id and next_id >= min_id
end
end
def ensure=(ensure_value)
super
# We need to loop over all valid properties for the type we're
# managing and call the method which sets that property value
# dscl can't create everything at once unfortunately.
if ensure_value == :present
@resource.class.validproperties.each do |name|
next if name == :ensure
# LAK: We use property.sync here rather than directly calling
# the settor method because the properties might do some kind
# of conversion. In particular, the user gid property might
# have a string and need to convert it to a number
if @resource.should(name)
@resource.property(name).sync
elsif value = autogen(name)
self.send(name.to_s + "=", value)
else
next
end
end
end
end
def password=(passphrase)
exec_arg_vector = self.class.get_exec_preamble("-read", @resource.name)
exec_arg_vector << ns_to_ds_attribute_map[:guid]
begin
guid_output = execute(exec_arg_vector)
guid_plist = Plist.parse_xml(guid_output)
# Although GeneratedUID like all DirectoryService values can be multi-valued
# according to the schema, in practice user accounts cannot have multiple UUIDs
# otherwise Bad Things Happen, so we just deal with the first value.
guid = guid_plist["dsAttrTypeStandard:#{ns_to_ds_attribute_map[:guid]}"][0]
self.class.set_password(@resource.name, guid, passphrase)
rescue Puppet::ExecutionFailure => detail
fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}")
end
end
# NBK: we override @parent.set as we need to execute a series of commands
# to deal with array values, rather than the single command nameservice.rb
# expects to be returned by modifycmd. Thus we don't bother defining modifycmd.
def set(param, value)
self.class.validate(param, value)
current_members = @property_value_cache_hash[:members]
if param == :members
# If we are meant to be authoritative for the group membership
# then remove all existing members who haven't been specified
# in the manifest.
remove_unwanted_members(current_members, value) if @resource[:auth_membership] and not current_members.nil?
# if they're not a member, make them one.
add_members(current_members, value)
else
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
# JJM: The following line just maps the NS name to the DS name
# e.g. { :uid => 'UniqueID' }
exec_arg_vector << ns_to_ds_attribute_map[symbolize(param)]
# JJM: The following line sends the actual value to set the property to
exec_arg_vector << value.to_s
begin
execute(exec_arg_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}")
end
end
end
# NBK: we override @parent.create as we need to execute a series of commands
# to create objects with dscl, rather than the single command nameservice.rb
# expects to be returned by addcmd. Thus we don't bother defining addcmd.
def create
if exists?
info "already exists"
return nil
end
# NBK: First we create the object with a known guid so we can set the contents
# of the password hash if required
# Shelling out sucks, but for a single use case it doesn't seem worth
# requiring people install a UUID library that doesn't come with the system.
# This should be revisited if Puppet starts managing UUIDs for other platform
# user records.
guid = %x{/usr/bin/uuidgen}.chomp
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
exec_arg_vector << ns_to_ds_attribute_map[:guid] << guid
begin
execute(exec_arg_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not set GeneratedUID for #{@resource.class.name} #{@resource.name}: #{detail}")
end
if value = @resource.should(:password) and value != ""
self.class.set_password(@resource[:name], guid, value)
end
# Now we create all the standard properties
Puppet::Type.type(@resource.class.name).validproperties.each do |property|
next if property == :ensure
value = @resource.should(property)
if property == :gid and value.nil?
value = self.class.next_system_id(id_type='gid')
end
if property == :uid and value.nil?
value = self.class.next_system_id(id_type='uid')
end
if value != "" and not value.nil?
if property == :members
add_members(nil, value)
else
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
exec_arg_vector << ns_to_ds_attribute_map[symbolize(property)]
next if property == :password # skip setting the password here
exec_arg_vector << value.to_s
begin
execute(exec_arg_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not create #{@resource.class.name} #{@resource.name}: #{detail}")
end
end
end
end
end
def remove_unwanted_members(current_members, new_members)
current_members.each do |member|
if not new_members.flatten.include?(member)
cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-d", member, @resource[:name]]
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
# TODO: We're falling back to removing the member using dscl due to rdar://8481241
# This bug causes dseditgroup to fail to remove a member if that member doesn't exist
cmd = [:dscl, ".", "-delete", "/Groups/#{@resource.name}", "GroupMembership", member]
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
fail("Could not remove #{member} from group: #{@resource.name}, #{detail}")
end
end
end
end
end
def add_members(current_members, new_members)
new_members.flatten.each do |new_member|
if current_members.nil? or not current_members.include?(new_member)
cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-a", new_member, @resource[:name]]
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
fail("Could not add #{new_member} to group: #{@resource.name}, #{detail}")
end
end
end
end
def deletecmd
# JJM: Like addcmd, only called when deleting the object itself
# Note, this isn't used to delete properties of the object,
# at least that's how I understand it...
self.class.get_exec_preamble("-delete", @resource[:name])
end
def getinfo(refresh = false)
# JJM 2007-07-24:
# Override the getinfo method, which is also defined in nameservice.rb
# This method returns and sets @infohash
# I'm not re-factoring the name "getinfo" because this method will be
# most likely called by nameservice.rb, which I didn't write.
if refresh or (! defined?(@property_value_cache_hash) or ! @property_value_cache_hash)
# JJM 2007-07-24: OK, there's a bit of magic that's about to
# happen... Let's see how strong my grip has become... =)
#
# self is a provider instance of some Puppet::Type, like
# Puppet::Type::User::ProviderDirectoryservice for the case of the
# user type and this provider.
#
# self.class looks like "user provider directoryservice", if that
# helps you ...
#
# self.class.resource_type is a reference to the Puppet::Type class,
# probably Puppet::Type::User or Puppet::Type::Group, etc...
#
# self.class.resource_type.validproperties is a class method,
# returning an Array of the valid properties of that specific
# Puppet::Type.
#
# So... something like [:comment, :home, :password, :shell, :uid,
# :groups, :ensure, :gid]
#
# Ultimately, we add :name to the list, delete :ensure from the
# list, then report on the remaining list. Pretty whacky, ehh?
type_properties = [:name] + self.class.resource_type.validproperties
type_properties.delete(:ensure) if type_properties.include? :ensure
type_properties << :guid # append GeneratedUID so we just get the report here
@property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties)
[:uid, :gid].each do |param|
@property_value_cache_hash[param] = @property_value_cache_hash[param].to_i if @property_value_cache_hash and @property_value_cache_hash.include?(param)
end
end
@property_value_cache_hash
end
end
end
diff --git a/lib/puppet/reports/tagmail.rb b/lib/puppet/reports/tagmail.rb
index 7960b2ce4..aeb35b450 100644
--- a/lib/puppet/reports/tagmail.rb
+++ b/lib/puppet/reports/tagmail.rb
@@ -1,172 +1,179 @@
require 'puppet'
require 'pp'
require 'net/smtp'
require 'time'
Puppet::Reports.register_report(:tagmail) do
desc "This report sends specific log messages to specific email addresses
based on the tags in the log messages.
See the [documentation on tags](http://projects.puppetlabs.com/projects/puppet/wiki/Using_Tags) for more information.
To use this report, you must create a `tagmail.conf` file in the location
specified by the `tagmap` setting. This is a simple file that maps tags to
email addresses: Any log messages in the report that match the specified
tags will be sent to the specified email addresses.
Lines in the `tagmail.conf` file consist of a comma-separated list
of tags, a colon, and a comma-separated list of email addresses.
Tags can be !negated with a leading exclamation mark, which will
subtract any messages with that tag from the set of events handled
by that line.
Puppet's log levels (`debug`, `info`, `notice`, `warning`, `err`,
`alert`, `emerg`, `crit`, and `verbose`) can also be used as tags,
and there is an `all` tag that will always match all log messages.
An example `tagmail.conf`:
all: me@domain.com
webserver, !mailserver: httpadmins@domain.com
This will send all messages to `me@domain.com`, and all messages from
webservers that are not also from mailservers to `httpadmins@domain.com`.
If you are using anti-spam controls such as grey-listing on your mail
server, you should whitelist the sending email address (controlled by
`reportform` configuration option) to ensure your email is not discarded as spam.
"
# Find all matching messages.
def match(taglists)
matching_logs = []
taglists.each do |emails, pos, neg|
# First find all of the messages matched by our positive tags
messages = nil
if pos.include?("all")
messages = self.logs
else
# Find all of the messages that are tagged with any of our
# tags.
messages = self.logs.find_all do |log|
pos.detect { |tag| log.tagged?(tag) }
end
end
# Now go through and remove any messages that match our negative tags
messages = messages.reject do |log|
true if neg.detect do |tag| log.tagged?(tag) end
end
if messages.empty?
Puppet.info "No messages to report to #{emails.join(",")}"
next
else
matching_logs << [emails, messages.collect { |m| m.to_report }.join("\n")]
end
end
matching_logs
end
# Load the config file
def parse(text)
taglists = []
text.split("\n").each do |line|
taglist = emails = nil
case line.chomp
when /^\s*#/; next
when /^\s*$/; next
when /^\s*(.+)\s*:\s*(.+)\s*$/
taglist = $1
emails = $2.sub(/#.*$/,'')
else
raise ArgumentError, "Invalid tagmail config file"
end
pos = []
neg = []
taglist.sub(/\s+$/,'').split(/\s*,\s*/).each do |tag|
unless tag =~ /^!?[-\w\.]+$/
raise ArgumentError, "Invalid tag #{tag.inspect}"
end
case tag
when /^\w+/; pos << tag
when /^!\w+/; neg << tag.sub("!", '')
else
raise Puppet::Error, "Invalid tag '#{tag}'"
end
end
# Now split the emails
emails = emails.sub(/\s+$/,'').split(/\s*,\s*/)
taglists << [emails, pos, neg]
end
taglists
end
# Process the report. This just calls the other associated messages.
def process
unless FileTest.exists?(Puppet[:tagmap])
Puppet.notice "Cannot send tagmail report; no tagmap file #{Puppet[:tagmap]}"
return
end
+ metrics = raw_summary['resources'] || {} rescue {}
+
+ if metrics['out_of_sync'] == 0 && metrics['changed'] == 0
+ Puppet.notice "Not sending tagmail report; no changes"
+ return
+ end
+
taglists = parse(File.read(Puppet[:tagmap]))
# Now find any appropriately tagged messages.
reports = match(taglists)
send(reports)
end
# Send the email reports.
def send(reports)
pid = fork do
if Puppet[:smtpserver] != "none"
begin
Net::SMTP.start(Puppet[:smtpserver]) do |smtp|
reports.each do |emails, messages|
smtp.open_message_stream(Puppet[:reportfrom], *emails) do |p|
p.puts "From: #{Puppet[:reportfrom]}"
p.puts "Subject: Puppet Report for #{self.host}"
p.puts "To: " + emails.join(", ")
p.puts "Date: #{Time.now.rfc2822}"
p.puts
p.puts messages
end
end
end
rescue => detail
message = "Could not send report emails through smtp: #{detail}"
Puppet.log_exception(detail, message)
raise Puppet::Error, message
end
elsif Puppet[:sendmail] != ""
begin
reports.each do |emails, messages|
# We need to open a separate process for every set of email addresses
IO.popen(Puppet[:sendmail] + " " + emails.join(" "), "w") do |p|
p.puts "From: #{Puppet[:reportfrom]}"
p.puts "Subject: Puppet Report for #{self.host}"
p.puts "To: " + emails.join(", ")
p.puts messages
end
end
rescue => detail
message = "Could not send report emails via sendmail: #{detail}"
Puppet.log_exception(detail, message)
raise Puppet::Error, message
end
else
raise Puppet::Error, "SMTP server is unset and could not find sendmail"
end
end
# Don't bother waiting for the pid to return.
Process.detach(pid)
end
end
diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb
index 6556a249a..f3496a8fa 100755
--- a/lib/puppet/type/user.rb
+++ b/lib/puppet/type/user.rb
@@ -1,523 +1,524 @@
require 'etc'
require 'facter'
require 'puppet/property/list'
require 'puppet/property/ordered_list'
require 'puppet/property/keyvalue'
module Puppet
newtype(:user) do
@doc = "Manage users. This type is mostly built to manage system
users, so it is lacking some features useful for managing normal
users.
This resource type uses the prescribed native tools for creating
groups and generally uses POSIX APIs for retrieving information
about them. It does not directly modify `/etc/passwd` or anything.
**Autorequires:** If Puppet is managing the user's primary group (as
provided in the `gid` attribute), the user resource will autorequire
that group. If Puppet is managing any role accounts corresponding to the
user's roles, the user resource will autorequire those role accounts."
feature :allows_duplicates,
"The provider supports duplicate users with the same UID."
feature :manages_homedir,
"The provider can create and remove home directories."
feature :manages_passwords,
"The provider can modify user passwords, by accepting a password
hash."
feature :manages_password_age,
"The provider can set age requirements and restrictions for
passwords."
feature :manages_solaris_rbac,
"The provider can manage roles and normal users"
feature :manages_expiry,
"The provider can manage the expiry date for a user."
feature :system_users,
"The provider allows you to create system users with lower UIDs."
feature :manages_aix_lam,
"The provider can manage AIX Loadable Authentication Module (LAM) system."
newproperty(:ensure, :parent => Puppet::Property::Ensure) do
newvalue(:present, :event => :user_created) do
provider.create
end
newvalue(:absent, :event => :user_removed) do
provider.delete
end
newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do
provider.create_role
end
desc "The basic state that the object should be in."
# If they're talking about the thing at all, they generally want to
# say it should exist.
defaultto do
if @resource.managed?
:present
else
nil
end
end
def retrieve
if provider.exists?
if provider.respond_to?(:is_role?) and provider.is_role?
return :role
else
return :present
end
else
return :absent
end
end
end
newproperty(:home) do
desc "The home directory of the user. The directory must be created
separately and is not currently checked for existence."
end
newproperty(:uid) do
desc "The user ID; must be specified numerically. If no user ID is
specified when creating a new user, then one will be chosen
automatically. This will likely result in the same user having
different UIDs on different systems, which is not recommended. This is
especially noteworthy when managing the same user on both Darwin and
other platforms, since Puppet does UID generation on Darwin, but
the underlying tools do so on other platforms.
On Windows, this property is read-only and will return the user's
security identifier (SID)."
munge do |value|
case value
when String
if value =~ /^[-0-9]+$/
value = Integer(value)
end
end
return value
end
end
newproperty(:gid) do
desc "The user's primary group. Can be specified numerically or by name.
Note that users on Windows systems do not have a primary group; manage groups
with the `groups` attribute instead."
munge do |value|
if value.is_a?(String) and value =~ /^[-0-9]+$/
Integer(value)
else
value
end
end
def insync?(is)
# We know the 'is' is a number, so we need to convert the 'should' to a number,
# too.
@should.each do |value|
return true if number = Puppet::Util.gid(value) and is == number
end
false
end
def sync
found = false
@should.each do |value|
if number = Puppet::Util.gid(value)
provider.gid = number
found = true
break
end
end
fail "Could not find group(s) #{@should.join(",")}" unless found
# Use the default event.
end
end
newproperty(:comment) do
desc "A description of the user. Generally the user's full name."
end
newproperty(:shell) do
desc "The user's login shell. The shell must exist and be
executable.
This attribute cannot be managed on Windows systems."
end
newproperty(:password, :required_features => :manages_passwords) do
desc %q{The user's password, in whatever encrypted format the local
system requires.
* Most modern Unix-like systems use salted SHA1 password hashes. You can use
Puppet's built-in `sha1` function to generate a hash from a password.
* Mac OS X 10.5 and 10.6 also use salted SHA1 hashes.
* Mac OS X 10.7 (Lion) uses salted SHA512 hashes. The Puppet Labs [stdlib][]
module contains a `str2saltedsha512` function which can generate password
hashes for Lion.
* Windows passwords can only be managed in cleartext, as there is no Windows API
for setting the password hash.
[stdlib]: https://github.com/puppetlabs/puppetlabs-stdlib/
Be sure to enclose any value that includes a dollar sign ($) in single
quotes (') to avoid accidental variable interpolation.}
validate do |value|
raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":")
end
def change_to_s(currentvalue, newvalue)
if currentvalue == :absent
return "created password"
else
return "changed password"
end
end
def is_to_s( currentvalue )
return '[old password hash redacted]'
end
def should_to_s( newvalue )
return '[new password hash redacted]'
end
end
newproperty(:password_min_age, :required_features => :manages_password_age) do
desc "The minimum number of days a password must be used before it may be changed."
munge do |value|
case value
when String
Integer(value)
else
value
end
end
validate do |value|
if value.to_s !~ /^-?\d+$/
raise ArgumentError, "Password minimum age must be provided as a number."
end
end
end
newproperty(:password_max_age, :required_features => :manages_password_age) do
desc "The maximum number of days a password may be used before it must be changed."
munge do |value|
case value
when String
Integer(value)
else
value
end
end
validate do |value|
if value.to_s !~ /^-?\d+$/
raise ArgumentError, "Password maximum age must be provided as a number."
end
end
end
newproperty(:groups, :parent => Puppet::Property::List) do
desc "The groups to which the user belongs. The primary group should
not be listed, and groups should be identified by name rather than by
GID. Multiple groups should be specified as an array."
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Group names must be provided, not GID numbers."
end
raise ArgumentError, "Group names must be provided as an array, not a comma-separated list." if value.include?(",")
+ raise ArgumentError, "Group names must not be empty. If you want to specify \"no groups\" pass an empty array" if value.empty?
end
end
newparam(:name) do
desc "The user name. While naming limitations vary by operating system,
it is advisable to restrict names to the lowest common denominator,
which is a maximum of 8 characters beginning with a letter.
Note that Puppet considers user names to be case-sensitive, regardless
of the platform's own rules; be sure to always use the same case when
referring to a given user."
isnamevar
end
newparam(:membership) do
desc "Whether specified groups should be considered the **complete list**
(`inclusive`) or the **minimum list** (`minimum`) of groups to which
the user belongs. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newparam(:system, :boolean => true) do
desc "Whether the user is a system user, according to the OS's criteria;
on most platforms, a UID less than or equal to 500 indicates a system
user. Defaults to `false`."
newvalues(:true, :false)
defaultto false
end
newparam(:allowdupe, :boolean => true) do
desc "Whether to allow duplicate UIDs. Defaults to `false`."
newvalues(:true, :false)
defaultto false
end
newparam(:managehome, :boolean => true) do
desc "Whether to manage the home directory when managing the user.
Defaults to `false`."
newvalues(:true, :false)
defaultto false
validate do |val|
if val.to_s == "true"
raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" unless provider.class.manages_homedir?
end
end
end
newproperty(:expiry, :required_features => :manages_expiry) do
desc "The expiry date for this user. Must be provided in
a zero-padded YYYY-MM-DD format --- e.g. 2010-02-19."
validate do |value|
if value !~ /^\d{4}-\d{2}-\d{2}$/
raise ArgumentError, "Expiry dates must be YYYY-MM-DD"
end
end
end
# Autorequire the group, if it's around
autorequire(:group) do
autos = []
if obj = @parameters[:gid] and groups = obj.shouldorig
groups = groups.collect { |group|
if group =~ /^\d+$/
Integer(group)
else
group
end
}
groups.each { |group|
case group
when Integer
if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group }
autos << resource
end
else
autos << group
end
}
end
if obj = @parameters[:groups] and groups = obj.should
autos += groups.split(",")
end
autos
end
# Provide an external hook. Yay breaking out of APIs.
def exists?
provider.exists?
end
def retrieve
absent = false
properties.inject({}) { |prophash, property|
current_value = :absent
if absent
prophash[property] = :absent
else
current_value = property.retrieve
prophash[property] = current_value
end
if property.name == :ensure and current_value == :absent
absent = true
end
prophash
}
end
newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do
desc "The roles the user has. Multiple roles should be
specified as an array."
def membership
:role_membership
end
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Role names must be provided, not numbers"
end
raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
#autorequire the roles that the user has
autorequire(:user) do
reqs = []
if roles_property = @parameters[:roles] and roles = roles_property.should
reqs += roles.split(',')
end
reqs
end
newparam(:role_membership) do
desc "Whether specified roles should be considered the **complete list**
(`inclusive`) or the **minimum list** (`minimum`) of roles the user
has. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do
desc "The auths the user has. Multiple auths should be
specified as an array."
def membership
:auth_membership
end
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Auth names must be provided, not numbers"
end
raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
newparam(:auth_membership) do
desc "Whether specified auths should be considered the **complete list**
(`inclusive`) or the **minimum list** (`minimum`) of auths the user
has. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do
desc "The profiles the user has. Multiple profiles should be
specified as an array."
def membership
:profile_membership
end
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Profile names must be provided, not numbers"
end
raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
newparam(:profile_membership) do
desc "Whether specified roles should be treated as the **complete list**
(`inclusive`) or the **minimum list** (`minimum`) of roles
of which the user is a member. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do
desc "Specify user attributes in an array of key = value pairs."
def membership
:key_membership
end
validate do |value|
raise ArgumentError, "Key/value pairs must be separated by an =" unless value.include?("=")
end
end
newparam(:key_membership) do
desc "Whether specified key/value pairs should be considered the
**complete list** (`inclusive`) or the **minimum list** (`minimum`) of
the user's attributes. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:project, :required_features => :manages_solaris_rbac) do
desc "The name of the project associated with a user."
end
newparam(:ia_load_module, :required_features => :manages_aix_lam) do
desc "The name of the I&A module to use to manage this user."
end
newproperty(:attributes, :parent => Puppet::Property::KeyValue, :required_features => :manages_aix_lam) do
desc "Specify AIX attributes for the user in an array of attribute = value pairs."
def membership
:attribute_membership
end
def delimiter
" "
end
validate do |value|
raise ArgumentError, "Attributes value pairs must be separated by an =" unless value.include?("=")
end
end
newparam(:attribute_membership) do
desc "Whether specified attribute value pairs should be treated as the
**complete list** (`inclusive`) or the **minimum list** (`minimum`) of
attribute/value pairs for the user. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
end
end
diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb
index 76bfbe6fd..bb83e0999 100644
--- a/lib/puppet/util.rb
+++ b/lib/puppet/util.rb
@@ -1,529 +1,529 @@
# A module to collect utility functions.
require 'English'
require 'puppet/external/lock'
require 'puppet/error'
require 'puppet/util/execution_stub'
require 'uri'
require 'sync'
require 'monitor'
require 'tempfile'
require 'pathname'
module Puppet
module Util
require 'puppet/util/monkey_patches'
require 'benchmark'
# These are all for backward compatibility -- these are methods that used
# to be in Puppet::Util but have been moved into external modules.
require 'puppet/util/posix'
extend Puppet::Util::POSIX
@@sync_objects = {}.extend MonitorMixin
def self.activerecord_version
if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR))
([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f)
else
0
end
end
# Run some code with a specific environment. Resets the environment back to
# what it was at the end of the code.
def self.withenv(hash)
saved = ENV.to_hash
hash.each do |name, val|
ENV[name.to_s] = val
end
yield
ensure
ENV.clear
saved.each do |name, val|
ENV[name] = val
end
end
# Execute a given chunk of code with a new umask.
def self.withumask(mask)
cur = File.umask(mask)
begin
yield
ensure
File.umask(cur)
end
end
def self.synchronize_on(x,type)
sync_object,users = 0,1
begin
@@sync_objects.synchronize {
(@@sync_objects[x] ||= [Sync.new,0])[users] += 1
}
@@sync_objects[x][sync_object].synchronize(type) { yield }
ensure
@@sync_objects.synchronize {
@@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0
}
end
end
# Change the process to a different user
def self.chuser
if group = Puppet[:group]
begin
Puppet::Util::SUIDManager.change_group(group, true)
rescue => detail
Puppet.warning "could not change to group #{group.inspect}: #{detail}"
$stderr.puts "could not change to group #{group.inspect}"
# Don't exit on failed group changes, since it's
# not fatal
#exit(74)
end
end
if user = Puppet[:user]
begin
Puppet::Util::SUIDManager.change_user(user, true)
rescue => detail
$stderr.puts "Could not change to user #{user}: #{detail}"
exit(74)
end
end
end
# Create instance methods for each of the log levels. This allows
# the messages to be a little richer. Most classes will be calling this
# method.
def self.logmethods(klass, useself = true)
Puppet::Util::Log.eachlevel { |level|
klass.send(:define_method, level, proc { |args|
args = args.join(" ") if args.is_a?(Array)
if useself
Puppet::Util::Log.create(
:level => level,
:source => self,
:message => args
)
else
Puppet::Util::Log.create(
:level => level,
:message => args
)
end
})
}
end
# Proxy a bunch of methods to another object.
def self.classproxy(klass, objmethod, *methods)
classobj = class << klass; self; end
methods.each do |method|
classobj.send(:define_method, method) do |*args|
obj = self.send(objmethod)
obj.send(method, *args)
end
end
end
# Proxy a bunch of methods to another object.
def self.proxy(klass, objmethod, *methods)
methods.each do |method|
klass.send(:define_method, method) do |*args|
obj = self.send(objmethod)
obj.send(method, *args)
end
end
end
def benchmark(*args)
msg = args.pop
level = args.pop
object = nil
if args.empty?
if respond_to?(level)
object = self
else
object = Puppet
end
else
object = args.pop
end
raise Puppet::DevError, "Failed to provide level to :benchmark" unless level
unless level == :none or object.respond_to? level
raise Puppet::DevError, "Benchmarked object does not respond to #{level}"
end
# Only benchmark if our log level is high enough
if level != :none and Puppet::Util::Log.sendlevel?(level)
result = nil
seconds = Benchmark.realtime {
yield
}
object.send(level, msg + (" in %0.2f seconds" % seconds))
return seconds
else
yield
end
end
def which(bin)
if absolute_path?(bin)
return bin if FileTest.file? bin and FileTest.executable? bin
else
ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir|
begin
dest = File.expand_path(File.join(dir, bin))
rescue ArgumentError => e
# if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get
# an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error
# was thrown.
raise e unless ((dir =~ /~/) && ((ENV['HOME'].nil? || ENV['HOME'] == "")))
# if we get here they have a tilde in their PATH. We'll issue a single warning about this and then
# ignore this path element and carry on with our lives.
Puppet::Util::Warnings.warnonce("PATH contains a ~ character, and HOME is not set; ignoring PATH element '#{dir}'.")
next
end
if Puppet.features.microsoft_windows? && File.extname(dest).empty?
exts = ENV['PATHEXT']
exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD]
exts.each do |ext|
destext = File.expand_path(dest + ext)
return destext if FileTest.file? destext and FileTest.executable? destext
end
end
return dest if FileTest.file? dest and FileTest.executable? dest
end
end
nil
end
module_function :which
# Determine in a platform-specific way whether a path is absolute. This
# defaults to the local platform if none is specified.
def absolute_path?(path, platform=nil)
# Escape once for the string literal, and once for the regex.
slash = '[\\\\/]'
name = '[^\\\\/]+'
regexes = {
:windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i,
:posix => %r!^/!,
}
require 'puppet'
platform ||= Puppet.features.microsoft_windows? ? :windows : :posix
!! (path =~ regexes[platform])
end
module_function :absolute_path?
# Convert a path to a file URI
def path_to_uri(path)
return unless path
params = { :scheme => 'file' }
if Puppet.features.microsoft_windows?
path = path.gsub(/\\/, '/')
if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path)
params[:host] = unc[1]
path = unc[2]
elsif path =~ /^[a-z]:\//i
path = '/' + path
end
end
params[:path] = URI.escape(path)
begin
URI::Generic.build(params)
rescue => detail
raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}"
end
end
module_function :path_to_uri
# Get the path component of a URI
def uri_to_path(uri)
return unless uri.is_a?(URI)
path = URI.unescape(uri.path)
if Puppet.features.microsoft_windows? and uri.scheme == 'file'
if uri.host
path = "//#{uri.host}" + path # UNC
else
path.sub!(/^\//, '')
end
end
path
end
module_function :uri_to_path
# Create an exclusive lock.
def threadlock(resource, type = Sync::EX)
Puppet::Util.synchronize_on(resource,type) { yield }
end
module_function :benchmark
def memory
unless defined?(@pmap)
@pmap = which('pmap')
end
if @pmap
%x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i
else
0
end
end
def symbolize(value)
if value.respond_to? :intern
value.intern
else
value
end
end
def symbolizehash(hash)
newhash = {}
hash.each do |name, val|
if name.is_a? String
newhash[name.intern] = val
else
newhash[name] = val
end
end
+ newhash
end
def symbolizehash!(hash)
- hash.each do |name, val|
- if name.is_a? String
- hash[name.intern] = val
- hash.delete(name)
- end
- end
+ # this is not the most memory-friendly way to accomplish this, but the
+ # code re-use and clarity seems worthwhile.
+ newhash = symbolizehash(hash)
+ hash.clear
+ hash.merge!(newhash)
hash
end
module_function :symbolize, :symbolizehash, :symbolizehash!
# Just benchmark, with no logging.
def thinmark
seconds = Benchmark.realtime {
yield
}
seconds
end
module_function :memory, :thinmark
# Because IO#binread is only available in 1.9
def binread(file)
File.open(file, 'rb') { |f| f.read }
end
module_function :binread
# utility method to get the current call stack and format it to a human-readable string (which some IDEs/editors
# will recognize as links to the line numbers in the trace)
def self.pretty_backtrace(backtrace = caller(1))
backtrace.collect do |line|
file_path, line_num = line.split(":")
file_path = expand_symlinks(File.expand_path(file_path))
file_path + ":" + line_num
end .join("\n")
end
# utility method that takes a path as input, checks each component of the path to see if it is a symlink, and expands
# it if it is. returns the expanded path.
def self.expand_symlinks(file_path)
file_path.split("/").inject do |full_path, next_dir|
next_path = full_path + "/" + next_dir
if File.symlink?(next_path) then
link = File.readlink(next_path)
next_path =
case link
when /^\// then link
else
File.expand_path(full_path + "/" + link)
end
end
next_path
end
end
# Replace a file, securely. This takes a block, and passes it the file
# handle of a file open for writing. Write the replacement content inside
# the block and it will safely replace the target file.
#
# This method will make no changes to the target file until the content is
# successfully written and the block returns without raising an error.
#
# As far as possible the state of the existing file, such as mode, is
# preserved. This works hard to avoid loss of any metadata, but will result
# in an inode change for the file.
#
# Arguments: `filename`, `default_mode`
#
# The filename is the file we are going to replace.
#
# The default_mode is the mode to use when the target file doesn't already
# exist; if the file is present we copy the existing mode/owner/group values
# across.
def replace_file(file, default_mode, &block)
raise Puppet::DevError, "replace_file requires a block" unless block_given?
file = Pathname(file)
tempfile = Tempfile.new(file.basename.to_s, file.dirname.to_s)
file_exists = file.exist?
# If the file exists, use its current mode/owner/group. If it doesn't, use
# the supplied mode, and default to current user/group.
if file_exists
if Puppet.features.microsoft_windows?
mode = Puppet::Util::Windows::Security.get_mode(file.to_s)
uid = Puppet::Util::Windows::Security.get_owner(file.to_s)
gid = Puppet::Util::Windows::Security.get_owner(file.to_s)
else
stat = file.lstat
mode = stat.mode
uid = stat.uid
gid = stat.gid
end
# We only care about the four lowest-order octets. Higher octets are
# filesystem-specific.
mode &= 07777
else
mode = default_mode
uid = Process.euid
gid = Process.egid
end
# Set properties of the temporary file before we write the content, because
# Tempfile doesn't promise to be safe from reading by other people, just
# that it avoids races around creating the file.
if Puppet.features.microsoft_windows?
Puppet::Util::Windows::Security.set_mode(mode, tempfile.path)
Puppet::Util::Windows::Security.set_owner(uid, tempfile.path)
Puppet::Util::Windows::Security.set_group(gid, tempfile.path)
else
tempfile.chmod(mode)
tempfile.chown(uid, gid)
end
# OK, now allow the caller to write the content of the file.
yield tempfile
# Now, make sure the data (which includes the mode) is safe on disk.
tempfile.flush
begin
tempfile.fsync
rescue NotImplementedError
# fsync may not be implemented by Ruby on all platforms, but
# there is absolutely no recovery path if we detect that. So, we just
# ignore the return code.
#
# However, don't be fooled: that is accepting that we are running in
# an unsafe fashion. If you are porting to a new platform don't stub
# that out.
end
tempfile.close
File.rename(tempfile.path, file)
# Ideally, we would now fsync the directory as well, but Ruby doesn't
# have support for that, and it doesn't matter /that/ much...
# Return something true, and possibly useful.
file
end
module_function :replace_file
# Executes a block of code, wrapped with some special exception handling. Causes the ruby interpreter to
# exit if the block throws an exception.
#
# @param [String] message a message to log if the block fails
# @param [Integer] code the exit code that the ruby interpreter should return if the block fails
# @yield
def exit_on_fail(message, code = 1)
yield
# First, we need to check and see if we are catching a SystemExit error. These will be raised
# when we daemonize/fork, and they do not necessarily indicate a failure case.
rescue SystemExit => err
raise err
# Now we need to catch *any* other kind of exception, because we may be calling third-party
# code (e.g. webrick), and we have no idea what they might throw.
rescue Exception => err
Puppet.log_exception(err, "Could not #{message}: #{err}")
exit(code)
end
module_function :exit_on_fail
#######################################################################################################
# Deprecated methods relating to process execution; these have been moved to Puppet::Util::Execution
#######################################################################################################
def execpipe(command, failonfail = true, &block)
Puppet.deprecation_warning("Puppet::Util.execpipe is deprecated; please use Puppet::Util::Execution.execpipe")
Puppet::Util::Execution.execpipe(command, failonfail, &block)
end
module_function :execpipe
def execfail(command, exception)
Puppet.deprecation_warning("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail")
Puppet::Util::Execution.execfail(command, exception)
end
module_function :execfail
def execute(command, arguments = {})
Puppet.deprecation_warning("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute")
Puppet::Util::Execution.execute(command, arguments)
end
module_function :execute
end
end
require 'puppet/util/errors'
require 'puppet/util/methodhelper'
require 'puppet/util/metaid'
require 'puppet/util/classgen'
require 'puppet/util/docs'
require 'puppet/util/execution'
require 'puppet/util/logging'
require 'puppet/util/package'
require 'puppet/util/warnings'
diff --git a/lib/puppet/util/monkey_patches.rb b/lib/puppet/util/monkey_patches.rb
index 952aa2790..87ffc617e 100644
--- a/lib/puppet/util/monkey_patches.rb
+++ b/lib/puppet/util/monkey_patches.rb
@@ -1,230 +1,275 @@
require 'puppet/util'
module Puppet::Util::MonkeyPatches
end
unless defined? JRUBY_VERSION
Process.maxgroups = 1024
end
module RDoc
def self.caller(skip=nil)
in_gem_wrapper = false
Kernel.caller.reject { |call|
in_gem_wrapper ||= call =~ /#{Regexp.escape $0}:\d+:in `load'/
}
end
end
require "yaml"
require "puppet/util/zaml.rb"
class Symbol
def to_zaml(z)
z.emit("!ruby/sym ")
to_s.to_zaml(z)
end
def <=> (other)
self.to_s <=> other.to_s
end unless method_defined? "<=>"
end
[Object, Exception, Integer, Struct, Date, Time, Range, Regexp, Hash, Array, Float, String, FalseClass, TrueClass, Symbol, NilClass, Class].each { |cls|
cls.class_eval do
def to_yaml(ignored=nil)
ZAML.dump(self)
end
end
}
def YAML.dump(*args)
ZAML.dump(*args)
end
#
# Workaround for bug in MRI 1.8.7, see
# http://redmine.ruby-lang.org/issues/show/2708
# for details
#
if RUBY_VERSION == '1.8.7'
class NilClass
def closed?
true
end
end
end
class Object
# ActiveSupport 2.3.x mixes in a dangerous method
# that can cause rspec to fork bomb
# and other strange things like that.
def daemonize
raise NotImplementedError, "Kernel.daemonize is too dangerous, please don't try to use it."
end
end
# Workaround for yaml_initialize, which isn't supported before Ruby
# 1.8.3.
if RUBY_VERSION == '1.8.1' || RUBY_VERSION == '1.8.2'
YAML.add_ruby_type( /^object/ ) { |tag, val|
type, obj_class = YAML.read_type_class( tag, Object )
r = YAML.object_maker( obj_class, val )
if r.respond_to? :yaml_initialize
r.instance_eval { instance_variables.each { |name| remove_instance_variable name } }
r.yaml_initialize(tag, val)
end
r
}
end
class Array
# Ruby < 1.8.7 doesn't have this method but we use it in tests
def combination(num)
return [] if num < 0 || num > size
return [[]] if num == 0
return map{|e| [e] } if num == 1
tmp = self.dup
self[0, size - (num - 1)].inject([]) do |ret, e|
tmp.shift
ret += tmp.combination(num - 1).map{|a| a.unshift(e) }
end
end unless method_defined? :combination
alias :count :length unless method_defined? :count
end
class Symbol
def to_proc
Proc.new { |*args| args.shift.__send__(self, *args) }
end unless method_defined? :to_proc
# Defined in 1.9, absent in 1.8, and used for compatibility in various
# places, typically in third party gems.
def intern
return self
end unless method_defined? :intern
end
class String
unless method_defined? :lines
require 'puppet/util/monkey_patches/lines'
include Puppet::Util::MonkeyPatches::Lines
end
end
require 'fcntl'
class IO
unless method_defined? :lines
require 'puppet/util/monkey_patches/lines'
include Puppet::Util::MonkeyPatches::Lines
end
def self.binread(name, length = nil, offset = 0)
File.open(name, 'rb') do |f|
f.seek(offset) if offset > 0
f.read(length)
end
end unless singleton_methods.include?(:binread)
def self.binwrite(name, string, offset = nil)
# Determine if we should truncate or not. Since the truncate method on a
# file handle isn't implemented on all platforms, safer to do this in what
# looks like the libc interface - which is usually pretty robust.
# --daniel 2012-03-11
mode = Fcntl::O_CREAT | Fcntl::O_WRONLY | (offset.nil? ? Fcntl::O_TRUNC : 0)
IO.open(IO::sysopen(name, mode)) do |f|
# ...seek to our desired offset, then write the bytes. Don't try to
# seek past the start of the file, eh, because who knows what platform
# would legitimately blow up if we did that.
#
# Double-check the positioning, too, since destroying data isn't my idea
# of a good time. --daniel 2012-03-11
target = [0, offset.to_i].max
unless (landed = f.sysseek(target, IO::SEEK_SET)) == target
raise "unable to seek to target offset #{target} in #{name}: got to #{landed}"
end
f.syswrite(string)
end
end unless singleton_methods.include?(:binwrite)
end
class Range
def intersection(other)
raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
return unless other === self.first || self === other.first
start = [self.first, other.first].max
if self.exclude_end? && self.last <= other.last
start ... self.last
elsif other.exclude_end? && self.last >= other.last
start ... other.last
else
start .. [ self.last, other.last ].min
end
end unless method_defined? :intersection
alias_method :&, :intersection unless method_defined? :&
end
# Ruby 1.8.5 doesn't have tap
module Kernel
def tap
yield(self)
self
end unless method_defined?(:tap)
end
########################################################################
# The return type of `instance_variables` changes between Ruby 1.8 and 1.9
# releases; it used to return an array of strings in the form "@foo", but
# now returns an array of symbols in the form :@foo.
#
# Nothing else in the stack cares which form you get - you can pass the
# string or symbol to things like `instance_variable_set` and they will work
# transparently.
#
# Having the same form in all releases of Puppet is a win, though, so we
# pick a unification and enforce than on all releases. That way developers
# who do set math on them (eg: for YAML rendering) don't have to handle the
# distinction themselves.
#
# In the sane tradition, we bring older releases into conformance with newer
# releases, so we return symbols rather than strings, to be more like the
# future versions of Ruby are.
#
# We also carefully support reloading, by only wrapping when we don't
# already have the original version of the method aliased away somewhere.
if RUBY_VERSION[0,3] == '1.8'
unless Object.respond_to?(:puppet_original_instance_variables)
# Add our wrapper to the method.
class Object
alias :puppet_original_instance_variables :instance_variables
def instance_variables
puppet_original_instance_variables.map(&:to_sym)
end
end
# The one place that Ruby 1.8 assumes something about the return format of
# the `instance_variables` method is actually kind of odd, because it uses
# eval to get at instance variables of another object.
#
# This takes the original code and applies replaces instance_eval with
# instance_variable_get through it. All other bugs in the original (such
# as equality depending on the instance variables having the same order
# without any promise from the runtime) are preserved. --daniel 2012-03-11
require 'resolv'
class Resolv::DNS::Resource
def ==(other) # :nodoc:
return self.class == other.class &&
self.instance_variables == other.instance_variables &&
self.instance_variables.collect {|name| self.instance_variable_get name} ==
other.instance_variables.collect {|name| other.instance_variable_get name}
end
end
end
end
+
+# The mv method in Ruby 1.8.5 can't mv directories across devices
+# File.rename causes "Invalid cross-device link", which is rescued, but in Ruby
+# 1.8.5 it tries to recover with a copy and unlink, but the unlink causes the
+# error "Is a directory". In newer Rubies remove_entry is used
+# The implementation below is what's used in Ruby 1.8.7 and Ruby 1.9
+if RUBY_VERSION == '1.8.5'
+ require 'fileutils'
+
+ module FileUtils
+ def mv(src, dest, options = {})
+ fu_check_options options, OPT_TABLE['mv']
+ fu_output_message "mv#{options[:force] ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose]
+ return if options[:noop]
+ fu_each_src_dest(src, dest) do |s, d|
+ destent = Entry_.new(d, nil, true)
+ begin
+ if destent.exist?
+ if destent.directory?
+ raise Errno::EEXIST, dest
+ else
+ destent.remove_file if rename_cannot_overwrite_file?
+ end
+ end
+ begin
+ File.rename s, d
+ rescue Errno::EXDEV
+ copy_entry s, d, true
+ if options[:secure]
+ remove_entry_secure s, options[:force]
+ else
+ remove_entry s, options[:force]
+ end
+ end
+ rescue SystemCallError
+ raise unless options[:force]
+ end
+ end
+ end
+ module_function :mv
+
+ alias move mv
+ module_function :move
+ end
+end
diff --git a/lib/puppet/util/terminal.rb b/lib/puppet/util/terminal.rb
new file mode 100644
index 000000000..ba725702f
--- /dev/null
+++ b/lib/puppet/util/terminal.rb
@@ -0,0 +1,16 @@
+module Puppet::Util::Terminal
+ # Attempts to determine the width of the terminal. This is currently only
+ # supported on POSIX systems, and relies on the claims of `stty` (or `tput`).
+ #
+ # Inspired by code from Thor; thanks wycats!
+ # @return [Number] The column width of the terminal. Defaults to 80 columns.
+ def self.width
+ if Puppet.features.posix?
+ result = %x{stty size 2>/dev/null}.split[1] ||
+ %x{tput cols 2>/dev/null}.split[0]
+ end
+ return (result || '80').to_i
+ rescue
+ return 80
+ end
+end
diff --git a/lib/semver.rb b/lib/semver.rb
index 5029d96b4..53c7e697a 100644
--- a/lib/semver.rb
+++ b/lib/semver.rb
@@ -1,116 +1,121 @@
require 'puppet/util/monkey_patches'
# We need to subclass Numeric to force range comparisons not to try to iterate over SemVer
# and instead use numeric comparisons (eg >, <, >=, <=)
# Ruby 1.8 already did this for all ranges, but Ruby 1.9 changed range include behavior
class SemVer < Numeric
include Comparable
VERSION = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z-]*|)$/
SIMPLE_RANGE = /^v?(\d+|[xX])(?:\.(\d+|[xX])(?:\.(\d+|[xX]))?)?$/
def self.valid?(ver)
VERSION =~ ver
end
def self.find_matching(pattern, versions)
versions.select { |v| v.matched_by?("#{pattern}") }.sort.last
end
def self.[](range)
+ pre = proc { |vstring| vstring =~ /-/ ? vstring : vstring + '-' }
range.gsub(/([><=])\s+/, '\1').split(/\b\s+(?!-)/).map do |r|
case r
when SemVer::VERSION
- SemVer.new(r + '-') .. SemVer.new(r)
+ SemVer.new(pre[r]) .. SemVer.new(r)
when SemVer::SIMPLE_RANGE
r += ".0" unless SemVer.valid?(r.gsub(/x/i, '0'))
SemVer.new(r.gsub(/x/i, '0'))...SemVer.new(r.gsub(/(\d+)\.x/i) { "#{$1.to_i + 1}.0" } + '-')
when /\s+-\s+/
a, b = r.split(/\s+-\s+/)
- SemVer.new(a + '-') .. SemVer.new(b)
+ SemVer.new(pre[a]) .. SemVer.new(b)
when /^~/
ver = r.sub(/~/, '').split('.').map(&:to_i)
start = (ver + [0] * (3 - ver.length)).join('.')
ver.pop unless ver.length == 1
ver[-1] = ver.last + 1
finish = (ver + [0] * (3 - ver.length)).join('.')
- SemVer.new(start + '-') ... SemVer.new(finish + '-')
+ SemVer.new(pre[start]) ... SemVer.new(pre[finish])
when /^>=/
ver = r.sub(/^>=/, '')
- SemVer.new(ver + '-') .. SemVer::MAX
+ SemVer.new(pre[ver]) .. SemVer::MAX
when /^<=/
ver = r.sub(/^<=/, '')
SemVer::MIN .. SemVer.new(ver)
when /^>/
- ver = r.sub(/^>/, '').split('.').map(&:to_i)
- ver[2] = ver.last + 1
+ if r =~ /-/
+ ver = [r[1..-1]]
+ else
+ ver = r.sub(/^>/, '').split('.').map(&:to_i)
+ ver[2] = ver.last + 1
+ end
SemVer.new(ver.join('.') + '-') .. SemVer::MAX
when /^
ver = r.sub(/^, '')
- SemVer::MIN ... SemVer.new(ver + '-')
+ SemVer::MIN ... SemVer.new(pre[ver])
else
(1..1)
end
end.inject { |a,e| a & e }
end
attr_reader :major, :minor, :tiny, :special
def initialize(ver)
unless SemVer.valid?(ver)
raise ArgumentError.new("Invalid version string '#{ver}'!")
end
@major, @minor, @tiny, @special = VERSION.match(ver).captures.map do |x|
# Because Kernel#Integer tries to interpret hex and octal strings, which
# we specifically do not want, and which cannot be overridden in 1.8.7.
Float(x).to_i rescue x
end
end
def <=>(other)
other = SemVer.new("#{other}") unless other.is_a? SemVer
return self.major <=> other.major unless self.major == other.major
return self.minor <=> other.minor unless self.minor == other.minor
return self.tiny <=> other.tiny unless self.tiny == other.tiny
return 0 if self.special == other.special
return 1 if self.special == ''
return -1 if other.special == ''
return self.special <=> other.special
end
def matched_by?(pattern)
# For the time being, this is restricted to exact version matches and
# simple range patterns. In the future, we should implement some or all of
# the comparison operators here:
# https://github.com/isaacs/node-semver/blob/d474801/semver.js#L340
case pattern
when SIMPLE_RANGE
pattern = SIMPLE_RANGE.match(pattern).captures
pattern[1] = @minor unless pattern[1] && pattern[1] !~ /x/i
pattern[2] = @tiny unless pattern[2] && pattern[2] !~ /x/i
[@major, @minor, @tiny] == pattern.map { |x| x.to_i }
when VERSION
self == SemVer.new(pattern)
else
false
end
end
def inspect
@vstring || "v#{@major}.#{@minor}.#{@tiny}#{@special}"
end
alias :to_s :inspect
MIN = SemVer.new('0.0.0-')
MIN.instance_variable_set(:@vstring, 'vMIN')
MAX = SemVer.new('8.0.0')
MAX.instance_variable_set(:@major, (1.0/0)) # => Infinity
MAX.instance_variable_set(:@vstring, 'vMAX')
end
diff --git a/spec/fixtures/unit/reports/tagmail/tagmail_email.conf b/spec/fixtures/unit/reports/tagmail/tagmail_email.conf
new file mode 100644
index 000000000..94efca400
--- /dev/null
+++ b/spec/fixtures/unit/reports/tagmail/tagmail_email.conf
@@ -0,0 +1,2 @@
+secure: user@domain.com
+
diff --git a/spec/integration/faces/documentation_spec.rb b/spec/integration/faces/documentation_spec.rb
index 9ddf2f1b3..371143c6a 100755
--- a/spec/integration/faces/documentation_spec.rb
+++ b/spec/integration/faces/documentation_spec.rb
@@ -1,55 +1,55 @@
#!/usr/bin/env rspec
require 'spec_helper'
require 'puppet/face'
describe "documentation of faces" do
it "should generate global help" do
help = nil
expect { help = Puppet::Face[:help, :current].help }.not_to raise_error
help.should be_an_instance_of String
help.length.should be > 200
end
########################################################################
# Can we actually generate documentation for the face, and the actions it
# has? This avoids situations where the ERB template turns out to have a
# bug in it, triggered in something the user might do.
Puppet::Face.faces.sort.each do |face_name|
# REVISIT: We should walk all versions of the face here...
let :help do Puppet::Face[:help, :current] end
context "generating help" do
it "for #{face_name}" do
expect {
text = help.help(face_name)
text.should be_an_instance_of String
text.length.should be > 100
}.not_to raise_error
end
Puppet::Face[face_name, :current].actions.sort.each do |action_name|
it "for #{face_name}.#{action_name}" do
expect {
text = help.help(face_name, action_name)
text.should be_an_instance_of String
text.length.should be > 100
}.not_to raise_error
end
end
end
########################################################################
# Ensure that we have authorship and copyright information in *our* faces;
# if you apply this to third party faces you might well be disappointed.
context "licensing of Puppet Labs face '#{face_name}'" do
subject { Puppet::Face[face_name, :current] }
its :license do should =~ /Apache\s*2/ end
its :copyright do should =~ /Puppet Labs/ end
# REVISIT: This is less that ideal, I think, but right now I am more
# comfortable watching us ship with some copyright than without any; we
# can redress that when it becomes appropriate. --daniel 2011-04-27
- its :copyright do should =~ /2011/ end
+ its :copyright do should =~ /20\d{2}/ end
end
end
end
diff --git a/spec/integration/module_tool_spec.rb b/spec/integration/module_tool_spec.rb
deleted file mode 100644
index bd3b12649..000000000
--- a/spec/integration/module_tool_spec.rb
+++ /dev/null
@@ -1,475 +0,0 @@
-require 'spec_helper'
-require 'tmpdir'
-require 'fileutils'
-
-# FIXME This are helper methods that could be used by other tests in the
-# future, should we move these to a more central location
-def stub_repository_read(code, body)
- kind = Net::HTTPResponse.send(:response_class, code.to_s)
- response = kind.new('1.0', code.to_s, 'HTTP MESSAGE')
- response.stubs(:read_body).returns(body)
- Puppet::Forge::Repository.any_instance.stubs(:read_response).returns(response)
-end
-
-def stub_installer_read(body)
- Puppet::Module::Tool::Applications::Installer.any_instance.stubs(:read_match).returns(body)
-end
-
-def stub_cache_read(body)
- Puppet::Forge::Cache.any_instance.stubs(:read_retrieve).returns(body)
-end
-
-# Return path to temparory directory for testing.
-def testdir
- return @testdir ||= tmpdir("module_tool_testdir")
-end
-
-# Create a temporary testing directory, change into it, and execute the
-# +block+. When the block exists, remove the test directory and change back
-# to the previous directory.
-def mktestdircd(&block)
- previousdir = Dir.pwd
- rmtestdir
- FileUtils.mkdir_p(testdir)
- Dir.chdir(testdir)
- block.call
-ensure
- rmtestdir
- Dir.chdir previousdir
-end
-
-# Remove the temporary test directory.
-def rmtestdir
- FileUtils.rm_rf(testdir) if File.directory?(testdir)
-end
-# END helper methods
-
-
-# Directory that contains sample releases.
-RELEASE_FIXTURES_DIR = File.join(PuppetSpec::FIXTURE_DIR, "releases")
-
-# Return the pathname string to the directory containing the release fixture called +name+.
-def release_fixture(name)
- return File.join(RELEASE_FIXTURES_DIR, name)
-end
-
-# Copy the release fixture called +name+ into the current working directory.
-def install_release_fixture(name)
- release_fixture(name)
- FileUtils.cp_r(release_fixture(name), name)
-end
-
-describe "module_tool", :fails_on_windows => true do
- include PuppetSpec::Files
- before do
- @tmp_confdir = Puppet[:confdir] = tmpdir("module_tool_test_confdir")
- @tmp_vardir = Puppet[:vardir] = tmpdir("module_tool_test_vardir")
- Puppet[:module_repository] = "http://forge.puppetlabs.com"
- @mytmpdir = Pathname.new(tmpdir("module_tool_test"))
- @options = {}
- @options[:install_dir] = @mytmpdir
- @options[:module_repository] = "http://forge.puppetlabs.com"
- end
-
- def build_and_install_module
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- FileUtils.mv("#{@full_module_name}/pkg/#{@release_name}.tar.gz", "#{@release_name}.tar.gz")
- FileUtils.rm_rf(@full_module_name)
-
- Puppet::Module::Tool::Applications::Installer.run("#{@release_name}.tar.gz", @options)
- end
-
- # Return STDOUT and STDERR output generated from +block+ as it's run within a temporary test directory.
- def run(&block)
- mktestdircd do
- block.call
- end
- end
-
- before :all do
- @username = "myuser"
- @module_name = "mymodule"
- @full_module_name = "#{@username}-#{@module_name}"
- @version = "0.0.1"
- @release_name = "#{@full_module_name}-#{@version}"
- end
-
- before :each do
- Puppet.settings.stubs(:parse)
- Puppet::Forge::Cache.clean
- end
-
- after :each do
- Puppet::Forge::Cache.clean
- end
-
- describe "generate" do
- it "should generate a module if given a dashed name" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
-
- File.directory?(@full_module_name).should == true
- modulefile = File.join(@full_module_name, "Modulefile")
- File.file?(modulefile).should == true
- metadata = Puppet::Module::Tool::Metadata.new
- Puppet::Module::Tool::ModulefileReader.evaluate(metadata, modulefile)
- metadata.full_module_name.should == @full_module_name
- metadata.username.should == @username
- metadata.name.should == @module_name
- end
- end
-
- it "should fail if given an undashed name" do
- run do
- lambda { Puppet::Module::Tool::Applications::Generator.run("invalid") }.should raise_error(RuntimeError)
- end
- end
-
- it "should fail if directory already exists" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- lambda { Puppet::Module::Tool::Applications::Generator.run(@full_module_name) }.should raise_error(ArgumentError)
- end
- end
-
- it "should return an array of Pathname objects representing paths of generated files" do
- run do
- return_value = Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- return_value.each do |generated_file|
- generated_file.should be_kind_of(Pathname)
- end
- return_value.should be_kind_of(Array)
- end
- end
- end
-
- describe "build" do
- it "should build a module in a directory" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- File.directory?(File.join(@full_module_name, "pkg", @release_name)).should == true
- File.file?(File.join(@full_module_name, "pkg", @release_name + ".tar.gz")).should == true
- metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json")
- File.file?(metadata_file).should == true
- metadata = PSON.parse(File.read(metadata_file))
- metadata["name"].should == @full_module_name
- metadata["version"].should == @version
- metadata["checksums"].should be_a_kind_of(Hash)
- metadata["dependencies"].should == []
- metadata["types"].should == []
- end
- end
-
- it "should build a module's checksums" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json")
- metadata = PSON.parse(File.read(metadata_file))
- metadata["checksums"].should be_a_kind_of(Hash)
-
- modulefile_path = Pathname.new(File.join(@full_module_name, "Modulefile"))
- metadata["checksums"]["Modulefile"].should == Digest::MD5.hexdigest(modulefile_path.read)
- end
- end
-
- it "should build a module's types and providers" do
- run do
- name = "jamtur01-apache"
- install_release_fixture name
- Puppet::Module::Tool::Applications::Builder.run(name)
-
- metadata_file = File.join(name, "pkg", "#{name}-0.0.1", "metadata.json")
- metadata = PSON.parse(File.read(metadata_file))
-
- metadata["types"].size.should == 1
- type = metadata["types"].first
- type["name"].should == "a2mod"
- type["doc"].should == "Manage Apache 2 modules"
-
-
- type["parameters"].size.should == 1
- type["parameters"].first.tap do |o|
- o["name"].should == "name"
- o["doc"].should == "The name of the module to be managed"
- end
-
- type["properties"].size.should == 1
- type["properties"].first.tap do |o|
- o["name"].should == "ensure"
- o["doc"].should =~ /present.+absent/
- end
-
- type["providers"].size.should == 1
- type["providers"].first.tap do |o|
- o["name"].should == "debian"
- o["doc"].should =~ /Manage Apache 2 modules on Debian-like OSes/
- end
- end
- end
-
- it "should build a module's dependencies" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- modulefile = File.join(@full_module_name, "Modulefile")
-
- dependency1_name = "anotheruser-anothermodule"
- dependency1_requirement = ">= 1.2.3"
- dependency2_name = "someuser-somemodule"
- dependency2_requirement = "4.2"
- dependency2_repository = "http://some.repo"
-
- File.open(modulefile, "a") do |handle|
- handle.puts "dependency '#{dependency1_name}', '#{dependency1_requirement}'"
- handle.puts "dependency '#{dependency2_name}', '#{dependency2_requirement}', '#{dependency2_repository}'"
- end
-
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- metadata_file = File.join(@full_module_name, "pkg", "#{@full_module_name}-#{@version}", "metadata.json")
- metadata = PSON.parse(File.read(metadata_file))
-
- metadata['dependencies'].size.should == 2
- metadata['dependencies'].sort_by{|t| t['name']}.tap do |dependencies|
- dependencies[0].tap do |dependency1|
- dependency1['name'].should == dependency1_name
- dependency1['version_requirement'].should == dependency1_requirement
- dependency1['repository'].should be_nil
- end
-
- dependencies[1].tap do |dependency2|
- dependency2['name'].should == dependency2_name
- dependency2['version_requirement'].should == dependency2_requirement
- dependency2['repository'].should == dependency2_repository
- end
- end
- end
- end
-
- it "should rebuild a module in a directory" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
- end
- end
-
- it "should build a module in the current directory" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Dir.chdir(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil))
-
- File.file?(File.join("pkg", @release_name + ".tar.gz")).should == true
- end
- end
-
- it "should fail to build a module without a Modulefile" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- FileUtils.rm(File.join(@full_module_name, "Modulefile"))
-
- lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError)
- end
- end
-
- it "should fail to build a module directory that doesn't exist" do
- run do
- lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError)
- end
- end
-
- it "should fail to build a module in the current directory that's not a module" do
- run do
- lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil)) }.should raise_error(ArgumentError)
- end
- end
-
- it "should return a Pathname object representing the path to the release archive." do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name).should be_kind_of(Pathname)
- end
- end
- end
-
- describe "search" do
- it "should display matching modules" do
- run do
- stub_repository_read 200, <<-HERE
- [
- {"full_module_name": "cli", "version": "1.0"},
- {"full_module_name": "web", "version": "2.0"}
- ]
- HERE
- Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).size.should == 2
- end
- end
-
- it "should display no matches" do
- run do
- stub_repository_read 200, "[]"
- Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).should == []
- end
- end
-
- it "should fail if can't get a connection" do
- run do
- stub_repository_read 500, "OH NOES!!1!"
- lambda { Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options) }.should raise_error(RuntimeError)
- end
- end
-
- it "should return an array of module metadata hashes" do
- run do
- results = <<-HERE
- [
- {"full_module_name": "cli", "version": "1.0"},
- {"full_module_name": "web", "version": "2.0"}
- ]
- HERE
- expected = [
- {"version"=>"1.0", "full_module_name"=>"cli"},
- {"version"=>"2.0", "full_module_name"=>"web"}
- ]
- stub_repository_read 200, results
- return_value = Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options)
- return_value.should == expected
- return_value.should be_kind_of(Array)
- end
- end
- end
-
- describe "install" do
- it "should install a module to the puppet modulepath by default" do
- myothertmpdir = Pathname.new(tmpdir("module_tool_test_myothertmpdir"))
- run do
- @options[:install_dir] = myothertmpdir
- Puppet::Module::Tool.unstub(:install_dir)
-
- build_and_install_module
-
- File.directory?(myothertmpdir + @module_name).should == true
- File.file?(myothertmpdir + @module_name + 'metadata.json').should == true
- end
- end
-
- it "should install a module from a filesystem path" do
- run do
- build_and_install_module
-
- File.directory?(@mytmpdir + @module_name).should == true
- File.file?(@mytmpdir + @module_name + 'metadata.json').should == true
- end
- end
-
- it "should install a module from a webserver URL" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz")
- FileUtils.rm_rf(@full_module_name)
-
- release = {"file" => "/foo/bar/#{@release_name}.tar.gz", "version" => "#{@version}"}
- Puppet::Forge::Forge.any_instance.stubs(:get_release).returns(release)
-
- Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options)
-
- File.directory?(@mytmpdir + @module_name).should == true
- File.file?(@mytmpdir + @module_name + 'metadata.json').should == true
- end
- end
-
- it "should install a module from a webserver URL using a version requirement" # TODO
-
- it "should fail if module isn't a slashed name" do
- run do
- lambda { Puppet::Module::Tool::Applications::Installer.run("invalid") }.should raise_error(RuntimeError)
- end
- end
-
- it "should fail if module doesn't exist on webserver" do
- run do
- stub_installer_read "{}"
- lambda { Puppet::Module::Tool::Applications::Installer.run("not-found", @options) }.should raise_error(RuntimeError)
- end
- end
-
- it "should fail gracefully when receiving invalid PSON" do
- pending "Implement PSON error wrapper" # TODO
- run do
- stub_installer_read "1/0"
- lambda { Puppet::Module::Tool::Applications::Installer.run("not-found") }.should raise_error(SystemExit)
- end
- end
-
- it "should fail if installing a module that's already installed" do
- run do
- name = "myuser-mymodule"
- Dir.mkdir name
- lambda { Puppet::Module::Tool::Applications::Installer.run(name) }.should raise_error(ArgumentError)
- end
- end
-
- it "should return a Pathname object representing the path to the installed module" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
-
- stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz")
- FileUtils.rm_rf(@full_module_name)
-
- release = {"file" => "/foo/bar/#{@release_name}.tar.gz", "version" => "#{@version}"}
- Puppet::Forge::Forge.any_instance.stubs(:get_release).returns(release)
-
- Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options).should be_kind_of(Pathname)
- end
- end
-
- end
-
- describe "clean" do
- require 'puppet/module_tool'
- it "should clean cache" do
- run do
- build_and_install_module
- Puppet::Forge::Cache.base_path.directory?.should == true
- Puppet::Module::Tool::Applications::Cleaner.run
- Puppet::Forge::Cache.base_path.directory?.should == false
- end
- end
-
- it "should return a status Hash" do
- run do
- build_and_install_module
- return_value = Puppet::Module::Tool::Applications::Cleaner.run
- return_value.should include(:msg)
- return_value.should include(:status)
- return_value.should be_kind_of(Hash)
- end
- end
- end
-
- describe "changes" do
- it "should return an array of modified files" do
- run do
- Puppet::Module::Tool::Applications::Generator.run(@full_module_name)
- Puppet::Module::Tool::Applications::Builder.run(@full_module_name)
- Dir.chdir("#{@full_module_name}/pkg/#{@release_name}")
- File.open("Modulefile", "a") do |handle|
- handle.puts
- handle.puts "# Added"
- end
- return_value = Puppet::Module::Tool::Applications::Checksummer.run(".")
- return_value.should include("Modulefile")
- return_value.should be_kind_of(Array)
- end
- end
- end
-end
diff --git a/spec/unit/face/module/install_spec.rb b/spec/unit/face/module/install_spec.rb
new file mode 100644
index 000000000..9f67800a4
--- /dev/null
+++ b/spec/unit/face/module/install_spec.rb
@@ -0,0 +1,158 @@
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/module_tool'
+
+describe "puppet module install" do
+
+ subject { Puppet::Face[:module, :current] }
+
+ let(:options) do
+ {}
+ end
+
+ describe "option validation" do
+ before do
+ Puppet.settings[:modulepath] = fakemodpath
+ end
+
+ let(:expected_options) do
+ {
+ :target_dir => fakefirstpath,
+ :modulepath => fakemodpath,
+ :environment => 'production'
+ }
+ end
+
+ let(:sep) { File::PATH_SEPARATOR }
+ let(:fakefirstpath) { "/my/fake/modpath" }
+ let(:fakesecondpath) { "/other/fake/path" }
+ let(:fakemodpath) { "#{fakefirstpath}#{sep}#{fakesecondpath}" }
+ let(:fakedirpath) { "/my/fake/path" }
+
+ context "without any options" do
+ it "should require a name" do
+ pattern = /wrong number of arguments/
+ expect { subject.install }.to raise_error ArgumentError, pattern
+ end
+
+ it "should not require any options" do
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache")
+ end
+ end
+
+ it "should accept the --force option" do
+ options[:force] = true
+ expected_options.merge!(options)
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache", options)
+ end
+
+ it "should accept the --target-dir option" do
+ options[:target_dir] = "/foo/puppet/modules"
+ expected_options.merge!(options)
+ expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}"
+
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache", options)
+ end
+
+ it "should accept the --version option" do
+ options[:version] = "0.0.1"
+ expected_options.merge!(options)
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache", options)
+ end
+
+ it "should accept the --ignore-dependencies option" do
+ options[:ignore_dependencies] = true
+ expected_options.merge!(options)
+ Puppet::Module::Tool::Applications::Installer.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.install("puppetlabs-apache", options)
+ end
+
+ describe "when modulepath option is passed" do
+ let(:expected_options) { { :modulepath => fakemodpath, :environment => 'production' } }
+ let(:options) { { :modulepath => fakemodpath } }
+
+ describe "when target-dir option is not passed" do
+ it "should set target-dir to be first path from modulepath" do
+ expected_options[:target_dir] = fakefirstpath
+
+ Puppet::Module::Tool::Applications::Installer.
+ expects(:run).
+ with("puppetlabs-apache", expected_options)
+
+ Puppet::Face[:module, :current].install("puppetlabs-apache", options)
+
+ Puppet.settings[:modulepath].should == fakemodpath
+ end
+ end
+
+ describe "when target-dir option is passed" do
+ it "should set target-dir to be first path of modulepath" do
+ options[:target_dir] = fakedirpath
+ expected_options[:target_dir] = fakedirpath
+ expected_options[:modulepath] = "#{fakedirpath}#{sep}#{fakemodpath}"
+
+ Puppet::Module::Tool::Applications::Installer.
+ expects(:run).
+ with("puppetlabs-apache", expected_options)
+
+ Puppet::Face[:module, :current].install("puppetlabs-apache", options)
+
+ Puppet.settings[:modulepath].should == "#{fakedirpath}#{sep}#{fakemodpath}"
+ end
+ end
+ end
+
+ describe "when modulepath option is not passed" do
+ before do
+ Puppet.settings[:modulepath] = fakemodpath
+ end
+
+ describe "when target-dir option is not passed" do
+ it "should set target-dir to be first path of default mod path" do
+ expected_options[:target_dir] = fakefirstpath
+ expected_options[:modulepath] = fakemodpath
+
+ Puppet::Module::Tool::Applications::Installer.
+ expects(:run).
+ with("puppetlabs-apache", expected_options)
+
+ Puppet::Face[:module, :current].install("puppetlabs-apache", options)
+ end
+ end
+
+ describe "when target-dir option is passed" do
+ it "should prepend target-dir to modulepath" do
+ options[:target_dir] = fakedirpath
+ expected_options[:target_dir] = fakedirpath
+ expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}"
+
+ Puppet::Module::Tool::Applications::Installer.
+ expects(:run).
+ with("puppetlabs-apache", expected_options)
+
+ Puppet::Face[:module, :current].install("puppetlabs-apache", options)
+ Puppet.settings[:modulepath].should == expected_options[:modulepath]
+ end
+ end
+ end
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :install }
+
+ its(:summary) { should =~ /install.*module/im }
+ its(:description) { should =~ /install.*module/im }
+ its(:returns) { should =~ /pathname/i }
+ its(:examples) { should_not be_empty }
+
+ %w{ license copyright summary description returns examples }.each do |doc|
+ context "of the" do
+ its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
+ end
+ end
+ end
+end
diff --git a/spec/unit/face/module/list_spec.rb b/spec/unit/face/module/list_spec.rb
new file mode 100644
index 000000000..fa1636a4b
--- /dev/null
+++ b/spec/unit/face/module/list_spec.rb
@@ -0,0 +1,182 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/module_tool'
+require 'puppet_spec/modules'
+
+describe "puppet module list", :fails_on_windows => true do
+ include PuppetSpec::Files
+
+ before do
+ dir = tmpdir("deep_path")
+
+ @modpath1 = File.join(dir, "modpath1")
+ @modpath2 = File.join(dir, "modpath2")
+ @modulepath = "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}"
+ Puppet.settings[:modulepath] = @modulepath
+
+ FileUtils.mkdir_p(@modpath1)
+ FileUtils.mkdir_p(@modpath2)
+ end
+
+ it "should return an empty list per dir in path if there are no modules" do
+ Puppet.settings[:modulepath] = @modulepath
+ Puppet::Face[:module, :current].list.should == {
+ @modpath1 => [],
+ @modpath2 => []
+ }
+ end
+
+ it "should include modules separated by the environment's modulepath" do
+ foomod1 = PuppetSpec::Modules.create('foo', @modpath1)
+ barmod1 = PuppetSpec::Modules.create('bar', @modpath1)
+ foomod2 = PuppetSpec::Modules.create('foo', @modpath2)
+
+ env = Puppet::Node::Environment.new
+
+ Puppet::Face[:module, :current].list.should == {
+ @modpath1 => [
+ Puppet::Module.new('bar', :environment => env, :path => barmod1.path),
+ Puppet::Module.new('foo', :environment => env, :path => foomod1.path)
+ ],
+ @modpath2 => [Puppet::Module.new('foo', :environment => env, :path => foomod2.path)]
+ }
+ end
+
+ it "should use the specified environment" do
+ PuppetSpec::Modules.create('foo', @modpath1)
+ PuppetSpec::Modules.create('bar', @modpath1)
+
+ usedenv = Puppet::Node::Environment.new('useme')
+ usedenv.modulepath = [@modpath1, @modpath2]
+
+ Puppet::Face[:module, :current].list(:environment => 'useme').should == {
+ @modpath1 => [
+ Puppet::Module.new('bar', :environment => usedenv),
+ Puppet::Module.new('foo', :environment => usedenv)
+ ],
+ @modpath2 => []
+ }
+ end
+
+ it "should use the specified modulepath" do
+ PuppetSpec::Modules.create('foo', @modpath1)
+ PuppetSpec::Modules.create('bar', @modpath2)
+
+ Puppet::Face[:module, :current].list(:modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}").should == {
+ @modpath1 => [ Puppet::Module.new('foo') ],
+ @modpath2 => [ Puppet::Module.new('bar') ]
+ }
+ end
+
+ it "should use the specified modulepath over the specified environment in place of the environment's default path" do
+ foomod1 = PuppetSpec::Modules.create('foo', @modpath1)
+ barmod2 = PuppetSpec::Modules.create('bar', @modpath2)
+ env = Puppet::Node::Environment.new('myenv')
+ env.modulepath = ['/tmp/notused']
+
+ list = Puppet::Face[:module, :current].list(:environment => 'myenv', :modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}")
+
+ # Changing Puppet[:modulepath] causes Puppet::Node::Environment.new('myenv')
+ # to have a different object_id than the env above
+ env = Puppet::Node::Environment.new('myenv')
+ list.should == {
+ @modpath1 => [ Puppet::Module.new('foo', :environment => env, :path => foomod1.path) ],
+ @modpath2 => [ Puppet::Module.new('bar', :environment => env, :path => barmod2.path) ]
+ }
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :list }
+
+ its(:summary) { should =~ /list.*module/im }
+ its(:description) { should =~ /list.*module/im }
+ its(:returns) { should =~ /hash of paths to module objects/i }
+ its(:examples) { should_not be_empty }
+ end
+
+ describe "when rendering" do
+ it "should explicitly state when a modulepath is empty" do
+ empty_modpath = tmpdir('empty')
+ Puppet::Face[:module, :current].list_when_rendering_console(
+ { empty_modpath => [] },
+ {:modulepath => empty_modpath}
+ ).should == <<-HEREDOC.gsub(' ', '')
+ #{empty_modpath} (no modules installed)
+ HEREDOC
+ end
+
+ it "should print both modules with and without metadata" do
+ modpath = tmpdir('modpath')
+ Puppet.settings[:modulepath] = modpath
+ PuppetSpec::Modules.create('nometadata', modpath)
+ PuppetSpec::Modules.create('metadata', modpath, :metadata => {:author => 'metaman'})
+
+ dependency_tree = Puppet::Face[:module, :current].list
+
+ output = Puppet::Face[:module, :current].list_when_rendering_console(
+ dependency_tree,
+ {}
+ )
+
+ output.should == <<-HEREDOC.gsub(' ', '')
+ #{modpath}
+ ├── metaman-metadata (\e[0;36mv9.9.9\e[0m)
+ └── nometadata (\e[0;36m???\e[0m)
+ HEREDOC
+ end
+
+ it "should print the modulepaths in the order they are in the modulepath setting" do
+ path1 = tmpdir('b')
+ path2 = tmpdir('c')
+ path3 = tmpdir('a')
+
+ sep = File::PATH_SEPARATOR
+ Puppet.settings[:modulepath] = "#{path1}#{sep}#{path2}#{sep}#{path3}"
+
+ Puppet::Face[:module, :current].list_when_rendering_console(
+ {
+ path2 => [],
+ path3 => [],
+ path1 => [],
+ },
+ {}
+ ).should == <<-HEREDOC.gsub(' ', '')
+ #{path1} (no modules installed)
+ #{path2} (no modules installed)
+ #{path3} (no modules installed)
+ HEREDOC
+ end
+
+ it "should print dependencies as a tree" do
+ PuppetSpec::Modules.create('dependable', @modpath1, :metadata => { :version => '0.0.5'})
+ PuppetSpec::Modules.create(
+ 'other_mod',
+ @modpath1,
+ :metadata => {
+ :version => '1.0.0',
+ :dependencies => [{
+ "version_requirement" => ">= 0.0.5",
+ "name" => "puppetlabs/dependable"
+ }]
+ }
+ )
+
+ dependency_tree = Puppet::Face[:module, :current].list
+
+ output = Puppet::Face[:module, :current].list_when_rendering_console(
+ dependency_tree,
+ {:tree => true}
+ )
+
+ output.should == <<-HEREDOC.gsub(' ', '')
+ #{@modpath1}
+ └─┬ puppetlabs-other_mod (\e[0;36mv1.0.0\e[0m)
+ └── puppetlabs-dependable (\e[0;36mv0.0.5\e[0m)
+ #{@modpath2} (no modules installed)
+ HEREDOC
+ end
+ end
+
+end
diff --git a/spec/unit/face/module/search_spec.rb b/spec/unit/face/module/search_spec.rb
new file mode 100644
index 000000000..51f62bd1f
--- /dev/null
+++ b/spec/unit/face/module/search_spec.rb
@@ -0,0 +1,163 @@
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/application/module'
+require 'puppet/module_tool'
+
+describe "puppet module search", :fails_on_windows => true do
+ subject { Puppet::Face[:module, :current] }
+
+ let(:options) do
+ {}
+ end
+
+ describe Puppet::Application::Module do
+ subject do
+ app = Puppet::Application::Module.new
+ app.stubs(:action).returns(Puppet::Face.find_action(:module, :search))
+ app
+ end
+
+ before { subject.render_as = :console }
+ before { Puppet::Util::Terminal.stubs(:width).returns(100) }
+
+ it 'should output nothing when receiving an empty dataset' do
+ subject.render([], ['apache', {}]).should == "No results found for 'apache'."
+ end
+
+ it 'should output a header when receiving a non-empty dataset' do
+ results = [
+ {'full_name' => '', 'author' => '', 'desc' => '', 'tag_list' => [] },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /NAME/
+ subject.render(results, ['apache', {}]).should =~ /DESCRIPTION/
+ subject.render(results, ['apache', {}]).should =~ /AUTHOR/
+ subject.render(results, ['apache', {}]).should =~ /KEYWORDS/
+ end
+
+ it 'should output the relevant fields when receiving a non-empty dataset' do
+ results = [
+ {'full_name' => 'Name', 'author' => 'Author', 'desc' => 'Summary', 'tag_list' => ['tag1', 'tag2'] },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /Name/
+ subject.render(results, ['apache', {}]).should =~ /Author/
+ subject.render(results, ['apache', {}]).should =~ /Summary/
+ subject.render(results, ['apache', {}]).should =~ /tag1/
+ subject.render(results, ['apache', {}]).should =~ /tag2/
+ end
+
+ it 'should elide really long descriptions' do
+ results = [
+ {
+ 'full_name' => 'Name',
+ 'author' => 'Author',
+ 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it',
+ 'tag_list' => ['tag1', 'tag2'],
+ },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /\.{3} @Author/
+ end
+
+ it 'should never truncate the module name' do
+ results = [
+ {
+ 'full_name' => 'This-module-has-a-really-really-long-name',
+ 'author' => 'Author',
+ 'desc' => 'Description',
+ 'tag_list' => ['tag1', 'tag2'],
+ },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /This-module-has-a-really-really-long-name/
+ end
+
+ it 'should never truncate the author name' do
+ results = [
+ {
+ 'full_name' => 'Name',
+ 'author' => 'This-author-has-a-really-really-long-name',
+ 'desc' => 'Description',
+ 'tag_list' => ['tag1', 'tag2'],
+ },
+ ]
+
+ subject.render(results, ['apache', {}]).should =~ /@This-author-has-a-really-really-long-name/
+ end
+
+ it 'should never remove tags that match the search term' do
+ results = [
+ {
+ 'full_name' => 'Name',
+ 'author' => 'Author',
+ 'desc' => 'Description',
+ 'tag_list' => ['Supercalifragilisticexpialidocious'] + (1..100).map { |i| "tag#{i}" },
+ },
+ ]
+
+ subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should =~ /Supercalifragilisticexpialidocious/
+ subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should_not =~ /tag/
+ end
+
+ {
+ 100 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*15}\n"\
+ "Name This description is really too long to fit ... @JohnnyApples tag1 tag2 taggitty3#{' '*4}\n",
+
+ 70 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*5}\n"\
+ "Name This description is rea... @JohnnyApples tag1 tag2#{' '*4}\n",
+
+ 80 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*8}\n"\
+ "Name This description is really too... @JohnnyApples tag1 tag2#{' '*7}\n",
+
+ 200 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*48}\n"\
+ "Name This description is really too long to fit in a single data table, guys -- we should probably set about trunca... @JohnnyApples tag1 tag2 taggitty3#{' '*37}\n"
+ }.each do |width, expectation|
+ it "should resize the table to fit the screen, when #{width} columns" do
+ results = [
+ {
+ 'full_name' => 'Name',
+ 'author' => 'JohnnyApples',
+ 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it',
+ 'tag_list' => ['tag1', 'tag2', 'taggitty3'],
+ },
+ ]
+
+ Puppet::Util::Terminal.expects(:width).returns(width)
+ result = subject.render(results, ['apache', {}])
+ result.lines.sort_by(&:length).last.chomp.length.should <= width
+ result.should == expectation
+ end
+ end
+ end
+
+ describe "option validation" do
+ context "without any options" do
+ it "should require a search term" do
+ pattern = /wrong number of arguments/
+ expect { subject.search }.to raise_error ArgumentError, pattern
+ end
+ end
+
+ it "should accept the --module-repository option" do
+ options[:module_repository] = "http://forge.example.com"
+ Puppet::Module::Tool::Applications::Searcher.expects(:run).with("puppetlabs-apache", options).once
+ subject.search("puppetlabs-apache", options)
+ end
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :search }
+
+ its(:summary) { should =~ /search.*module/im }
+ its(:description) { should =~ /search.*module/im }
+ its(:returns) { should =~ /array/i }
+ its(:examples) { should_not be_empty }
+
+ %w{ license copyright summary description returns examples }.each do |doc|
+ context "of the" do
+ its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
+ end
+ end
+ end
+end
diff --git a/spec/unit/face/module/uninstall_spec.rb b/spec/unit/face/module/uninstall_spec.rb
new file mode 100644
index 000000000..a157df509
--- /dev/null
+++ b/spec/unit/face/module/uninstall_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/module_tool'
+
+describe "puppet module uninstall" do
+ subject { Puppet::Face[:module, :current] }
+
+ let(:options) do
+ {}
+ end
+
+ describe "option validation" do
+ context "without any options" do
+ it "should require a name" do
+ pattern = /wrong number of arguments/
+ expect { subject.uninstall }.to raise_error ArgumentError, pattern
+ end
+
+ it "should not require any options" do
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).once
+ subject.uninstall("puppetlabs-apache")
+ end
+ end
+
+ it "should accept the --environment option" do
+ options[:environment] = "development"
+ expected_options = { :environment => 'development' }
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.uninstall("puppetlabs-apache", options)
+ end
+
+ it "should accept the --modulepath option" do
+ options[:modulepath] = "/foo/puppet/modules"
+ expected_options = {
+ :modulepath => '/foo/puppet/modules',
+ :environment => 'production',
+ }
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.uninstall("puppetlabs-apache", options)
+ end
+
+ it "should accept the --version option" do
+ options[:version] = "1.0.0"
+ expected_options = {
+ :version => '1.0.0',
+ :environment => 'production',
+ }
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.uninstall("puppetlabs-apache", options)
+ end
+
+ it "should accept the --force flag" do
+ options[:force] = true
+ expected_options = {
+ :environment => 'production',
+ :force => true,
+ }
+ Puppet::Module::Tool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", expected_options).once
+ subject.uninstall("puppetlabs-apache", options)
+ end
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :uninstall }
+
+ its(:summary) { should =~ /uninstall.*module/im }
+ its(:description) { should =~ /uninstall.*module/im }
+ its(:returns) { should =~ /hash of module objects.*/im }
+ its(:examples) { should_not be_empty }
+
+ %w{ license copyright summary description returns examples }.each do |doc|
+ context "of the" do
+ its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
+ end
+ end
+ end
+end
diff --git a/spec/unit/face/module/upgrade_spec.rb b/spec/unit/face/module/upgrade_spec.rb
new file mode 100644
index 000000000..c7c2bbcef
--- /dev/null
+++ b/spec/unit/face/module/upgrade_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+require 'puppet/face'
+require 'puppet/module_tool'
+
+describe "puppet module upgrade" do
+ subject { Puppet::Face[:module, :current] }
+
+ let(:options) do
+ {}
+ end
+
+ describe "inline documentation" do
+ subject { Puppet::Face[:module, :current].get_action :upgrade }
+
+ its(:summary) { should =~ /upgrade.*module/im }
+ its(:description) { should =~ /upgrade.*module/im }
+ its(:returns) { should =~ /hash/i }
+ its(:examples) { should_not be_empty }
+
+ %w{ license copyright summary description returns examples }.each do |doc|
+ context "of the" do
+ its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
+ end
+ end
+ end
+end
diff --git a/spec/unit/forge/repository_spec.rb b/spec/unit/forge/repository_spec.rb
index 6d8ce38f1..bbfc0d136 100644
--- a/spec/unit/forge/repository_spec.rb
+++ b/spec/unit/forge/repository_spec.rb
@@ -1,86 +1,56 @@
require 'spec_helper'
require 'net/http'
require 'puppet/forge/repository'
require 'puppet/forge/cache'
describe Puppet::Forge::Repository do
describe 'instances' do
let(:repository) { Puppet::Forge::Repository.new('http://fake.com') }
- describe '#make_http_request' do
- before do
- # Do a mock of the Proxy call so we can do proper expects for
- # Net::HTTP
- Net::HTTP.expects(:Proxy).returns(Net::HTTP)
- Net::HTTP.expects(:start)
- end
- context "when not given an :authenticate option" do
- it "should authenticate" do
- repository.expects(:authenticate).never
- repository.make_http_request(nil)
- end
- end
- context "when given an :authenticate option" do
- it "should authenticate" do
- repository.expects(:authenticate)
- repository.make_http_request(nil, :authenticate => true)
- end
- end
- end
-
- describe '#authenticate' do
- it "should set basic auth on the request" do
- authenticated_request = stub
- authenticated_request.expects(:basic_auth)
- repository.expects(:prompt).twice
- repository.authenticate(authenticated_request)
- end
- end
-
describe '#retrieve' do
before do
@uri = URI.parse('http://some.url.com')
end
it "should access the cache" do
repository.cache.expects(:retrieve).with(@uri)
repository.retrieve(@uri)
end
end
describe 'http_proxy support' do
before :each do
ENV["http_proxy"] = nil
end
after :each do
ENV["http_proxy"] = nil
end
it "should support environment variable for port and host" do
ENV["http_proxy"] = "http://test.com:8011"
repository.http_proxy_host.should == "test.com"
repository.http_proxy_port.should == 8011
end
it "should support puppet configuration for port and host" do
ENV["http_proxy"] = nil
Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test.com')
Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456)
repository.http_proxy_port.should == 7456
repository.http_proxy_host.should == "test.com"
end
it "should use environment variable before puppet settings" do
ENV["http_proxy"] = "http://test1.com:8011"
Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test2.com')
Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456)
repository.http_proxy_host.should == "test1.com"
repository.http_proxy_port.should == 8011
end
end
end
end
diff --git a/spec/unit/forge_spec.rb b/spec/unit/forge_spec.rb
index 905f1bd24..95e47a03e 100644
--- a/spec/unit/forge_spec.rb
+++ b/spec/unit/forge_spec.rb
@@ -1,114 +1,56 @@
require 'spec_helper'
require 'puppet/forge'
require 'net/http'
+require 'puppet/module_tool'
+
+describe Puppet::Forge do
+ include PuppetSpec::Files
+
+ let(:response_body) do
+ <<-EOF
+ [
+ {
+ "author": "puppetlabs",
+ "name": "bacula",
+ "tag_list": ["backup", "bacula"],
+ "releases": [{"version": "0.0.1"}, {"version": "0.0.2"}],
+ "full_name": "puppetlabs/bacula",
+ "version": "0.0.2",
+ "project_url": "http://github.com/puppetlabs/puppetlabs-bacula",
+ "desc": "bacula"
+ }
+ ]
+ EOF
+ end
+ let(:response) { stub(:body => response_body, :code => '200') }
-describe Puppet::Forge::Forge do
before do
Puppet::Forge::Repository.any_instance.stubs(:make_http_request).returns(response)
Puppet::Forge::Repository.any_instance.stubs(:retrieve).returns("/tmp/foo")
end
- let(:forge) { forge = Puppet::Forge::Forge.new('http://forge.puppetlabs.com') }
-
describe "the behavior of the search method" do
context "when there are matches for the search term" do
before do
Puppet::Forge::Repository.any_instance.stubs(:make_http_request).returns(response)
end
- let(:response) { stub(:body => response_body, :code => '200') }
- let(:response_body) do
- <<-EOF
- [
- {
- "author": "puppetlabs",
- "name": "bacula",
- "tag_list": ["backup", "bacula"],
- "releases": [{"version": "0.0.1"}, {"version": "0.0.2"}],
- "full_name": "puppetlabs/bacula",
- "version": "0.0.2",
- "project_url": "http://github.com/puppetlabs/puppetlabs-bacula",
- "desc": "bacula"
- }
- ]
- EOF
- end
-
it "should return a list of matches from the forge" do
- forge.search('bacula').should == PSON.load(response_body)
+ Puppet::Forge.search('bacula').should == PSON.load(response_body)
end
end
context "when the connection to the forge fails" do
- let(:response) { stub(:body => '[]', :code => '404') }
+ let(:response) { stub(:body => '{}', :code => '404') }
- it "should raise an error" do
- lambda { forge.search('bacula') }.should raise_error RuntimeError
+ it "should raise an error for search" do
+ lambda { Puppet::Forge.search('bacula') }.should raise_error RuntimeError
end
- end
- end
-
- describe "the behavior of the get_release_package method" do
- let(:response) do
- response = mock()
- response.stubs(:body).returns('{"file": "/system/releases/p/puppetlabs/puppetlabs-apache-0.0.3.tar.gz", "version": "0.0.3"}')
- response
- end
-
- context "when source is not filesystem or repository" do
- it "should raise an error" do
- params = { :source => 'foo' }
- lambda { forge.get_release_package(params) }.should
- raise_error(ArgumentError, "Could not determine installation source")
- end
- end
-
- context "when the source is a repository" do
- let(:params) do
- {
- :source => :repository,
- :author => 'fakeauthor',
- :modname => 'fakemodule',
- :version => '0.0.1'
- }
- end
-
- it "should require author" do
- params.delete(:author)
- lambda { forge.get_release_package(params) }.should
- raise_error(ArgumentError, ":author and :modename required")
- end
-
- it "should require modname" do
- params.delete(:modname)
- lambda { forge.get_release_package(params) }.should
- raise_error(ArgumentError, ":author and :modename required")
- end
-
- it "should download the release package" do
- forge.get_release_package(params).should == "/tmp/foo"
- end
- end
-
- context "when the source is a filesystem" do
- it "should require filename" do
- params = { :source => :filesystem }
- lambda { forge.get_release_package(params) }.should
- raise_error(ArgumentError, ":filename required")
+ it "should raise an error for remote_dependency_info" do
+ lambda { Puppet::Forge.remote_dependency_info('puppetlabs', 'bacula', '0.0.1') }.should raise_error RuntimeError
end
end
end
- describe "the behavior of the get_releases method" do
- let(:response) do
- response = mock()
- response.stubs(:body).returns('{"releases": [{"version": "0.0.1"}, {"version": "0.0.2"}, {"version": "0.0.3"}]}')
- response
- end
-
- it "should return a list of module releases" do
- forge.get_releases('fakeauthor', 'fakemodule').should == ["0.0.1", "0.0.2", "0.0.3"]
- end
- end
end
diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb
index 67f36167b..bb32ccdde 100755
--- a/spec/unit/module_spec.rb
+++ b/spec/unit/module_spec.rb
@@ -1,722 +1,855 @@
#!/usr/bin/env rspec
require 'spec_helper'
require 'puppet_spec/files'
require 'puppet_spec/modules'
require 'puppet/module_tool/checksums'
describe Puppet::Module do
include PuppetSpec::Files
before do
# This is necessary because of the extra checks we have for the deprecated
# 'plugins' directory
FileTest.stubs(:exist?).returns false
end
it "should have a class method that returns a named module from a given environment" do
env = mock 'module'
env.expects(:module).with("mymod").returns "yep"
Puppet::Node::Environment.expects(:new).with("myenv").returns env
Puppet::Module.find("mymod", "myenv").should == "yep"
end
it "should return nil if asked for a named module that doesn't exist" do
env = mock 'module'
env.expects(:module).with("mymod").returns nil
Puppet::Node::Environment.expects(:new).with("myenv").returns env
Puppet::Module.find("mymod", "myenv").should be_nil
end
it "should support a 'version' attribute" do
mod = Puppet::Module.new("mymod")
mod.version = 1.09
mod.version.should == 1.09
end
it "should support a 'source' attribute" do
mod = Puppet::Module.new("mymod")
mod.source = "http://foo/bar"
mod.source.should == "http://foo/bar"
end
it "should support a 'project_page' attribute" do
mod = Puppet::Module.new("mymod")
mod.project_page = "http://foo/bar"
mod.project_page.should == "http://foo/bar"
end
it "should support an 'author' attribute" do
mod = Puppet::Module.new("mymod")
mod.author = "Luke Kanies "
mod.author.should == "Luke Kanies "
end
it "should support a 'license' attribute" do
mod = Puppet::Module.new("mymod")
mod.license = "GPL2"
mod.license.should == "GPL2"
end
it "should support a 'summary' attribute" do
mod = Puppet::Module.new("mymod")
mod.summary = "GPL2"
mod.summary.should == "GPL2"
end
it "should support a 'description' attribute" do
mod = Puppet::Module.new("mymod")
mod.description = "GPL2"
mod.description.should == "GPL2"
end
it "should support specifying a compatible puppet version" do
mod = Puppet::Module.new("mymod")
mod.puppetversion = "0.25"
mod.puppetversion.should == "0.25"
end
it "should validate that the puppet version is compatible" do
mod = Puppet::Module.new("mymod")
mod.puppetversion = "0.25"
Puppet.expects(:version).returns "0.25"
mod.validate_puppet_version
end
it "should fail if the specified puppet version is not compatible" do
mod = Puppet::Module.new("mymod")
mod.puppetversion = "0.25"
Puppet.stubs(:version).returns "0.24"
lambda { mod.validate_puppet_version }.should raise_error(Puppet::Module::IncompatibleModule)
end
describe "when finding unmet dependencies" do
before do
- @mod = Puppet::Module.new("mymod")
- @mod.stubs(:dependencies).returns [
- {
- "version_requirement" => ">= 2.2.0",
- "name" => "baz/foobar"
- }
- ]
+ FileTest.unstub(:exist?)
+ @modpath = tmpdir('modpath')
+ Puppet.settings[:modulepath] = @modpath
end
it "should list modules that are missing" do
- @mod.unmet_dependencies.should == [{
- :name => 'baz/foobar',
- :error => <<-HEREDOC.gsub(/^\s{10}/, '')
- Missing dependency `foobar`:
- `mymod` () requires `baz/foobar` (>= 2.2.0)
- HEREDOC
+ mod = PuppetSpec::Modules.create(
+ 'needy',
+ @modpath,
+ :metadata => {
+ :dependencies => [{
+ "version_requirement" => ">= 2.2.0",
+ "name" => "baz/foobar"
+ }]
+ }
+ )
+ mod.unmet_dependencies.should == [{
+ :reason => :missing,
+ :name => "baz/foobar",
+ :version_constraint => ">= 2.2.0",
+ :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' },
+ :mod_details => { :installed_version => nil }
}]
end
- it "should list modules with unmet version" do
- foobar = Puppet::Module.new("foobar")
- foobar.version = '2.0.0'
- @mod.environment.expects(:module).with("foobar").returns foobar
-
- @mod.unmet_dependencies.should == [{
- :name => 'baz/foobar',
- :error => <<-HEREDOC.gsub(/^\s{10}/, '')
- Version dependency mismatch `foobar` (2.0.0):
- `mymod` () requires `baz/foobar` (>= 2.2.0)
- HEREDOC
+ it "should list modules that are missing and have invalid names" do
+ mod = PuppetSpec::Modules.create(
+ 'needy',
+ @modpath,
+ :metadata => {
+ :dependencies => [{
+ "version_requirement" => ">= 2.2.0",
+ "name" => "baz/foobar=bar"
+ }]
+ }
+ )
+ mod.unmet_dependencies.should == [{
+ :reason => :missing,
+ :name => "baz/foobar=bar",
+ :version_constraint => ">= 2.2.0",
+ :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' },
+ :mod_details => { :installed_version => nil }
}]
end
- it "should consider a dependency without a version requirement to be satisfied" do
- mod = Puppet::Module.new("mymod")
- mod.stubs(:dependencies).returns [{ "name" => "baz/foobar" }]
-
- foobar = Puppet::Module.new("foobar")
- mod.environment.expects(:module).with("foobar").returns foobar
-
- mod.unmet_dependencies.should be_empty
- end
+ it "should list modules with unmet version requirement" do
+ mod = PuppetSpec::Modules.create(
+ 'foobar',
+ @modpath,
+ :metadata => {
+ :dependencies => [{
+ "version_requirement" => ">= 2.2.0",
+ "name" => "baz/foobar"
+ }]
+ }
+ )
+ mod2 = PuppetSpec::Modules.create(
+ 'foobaz',
+ @modpath,
+ :metadata => {
+ :dependencies => [{
+ "version_requirement" => "1.0.0",
+ "name" => "baz/foobar"
+ }]
+ }
+ )
- it "should consider a dependency without a version to be unmet" do
- foobar = Puppet::Module.new("foobar")
- @mod.environment.expects(:module).with("foobar").returns foobar
+ PuppetSpec::Modules.create(
+ 'foobar',
+ @modpath,
+ :metadata => { :version => '2.0.0', :author => 'baz' }
+ )
- @mod.unmet_dependencies.should == [{
- :name => 'baz/foobar',
- :error => <<-HEREDOC.gsub(/^\s{10}/, '')
- Unversioned dependency `foobar`:
- `mymod` () requires `baz/foobar` (>= 2.2.0)
- HEREDOC
+ mod.unmet_dependencies.should == [{
+ :reason => :version_mismatch,
+ :name => "baz/foobar",
+ :version_constraint => ">= 2.2.0",
+ :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" },
+ :mod_details => { :installed_version => "2.0.0" }
}]
- end
- it "should consider a dependency without a semantic version to be unmet" do
- foobar = Puppet::Module.new("foobar")
- foobar.version = '5.1'
- @mod.environment.expects(:module).with("foobar").returns foobar
-
- @mod.unmet_dependencies.should == [{
- :name => 'baz/foobar',
- :error => <<-HEREDOC.gsub(/^\s{10}/, '')
- Non semantic version dependency `foobar` (5.1):
- `mymod` () requires `baz/foobar` (>= 2.2.0)
- HEREDOC
+ mod2.unmet_dependencies.should == [{
+ :reason => :version_mismatch,
+ :name => "baz/foobar",
+ :version_constraint => "v1.0.0",
+ :parent => { :version => "v9.9.9", :name => "puppetlabs/foobaz" },
+ :mod_details => { :installed_version => "2.0.0" }
}]
+
end
- it "should consider a dependency requirement without a semantic version to be unmet" do
- foobar = Puppet::Module.new("foobar")
- foobar.version = '5.1.0'
+ it "should consider a dependency without a version requirement to be satisfied" do
+ mod = PuppetSpec::Modules.create(
+ 'foobar',
+ @modpath,
+ :metadata => {
+ :dependencies => [{
+ "name" => "baz/foobar"
+ }]
+ }
+ )
+ PuppetSpec::Modules.create(
+ 'foobar',
+ @modpath,
+ :metadata => {
+ :version => '2.0.0',
+ :author => 'baz'
+ }
+ )
+
+ mod.unmet_dependencies.should be_empty
+ end
- mod = Puppet::Module.new("mymod")
- mod.stubs(:dependencies).returns [{ "name" => "baz/foobar", "version_requirement" => '> 2.0' }]
- mod.environment.expects(:module).with("foobar").returns foobar
+ it "should consider a dependency without a semantic version to be unmet" do
+ mod = PuppetSpec::Modules.create(
+ 'foobar',
+ @modpath,
+ :metadata => {
+ :dependencies => [{
+ "name" => "baz/foobar"
+ }]
+ }
+ )
+ PuppetSpec::Modules.create(
+ 'foobar',
+ @modpath,
+ :metadata => {
+ :version => '5.1',
+ :author => 'baz'
+ }
+ )
mod.unmet_dependencies.should == [{
- :name => 'baz/foobar',
- :error => <<-HEREDOC.gsub(/^\s{10}/, '')
- Non semantic version dependency `foobar` (5.1.0):
- `mymod` () requires `baz/foobar` (> 2.0)
- HEREDOC
+ :reason => :non_semantic_version,
+ :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" },
+ :mod_details => { :installed_version => "5.1" },
+ :name => "baz/foobar",
+ :version_constraint => ">= 0.0.0"
}]
end
it "should have valid dependencies when no dependencies have been specified" do
- mod = Puppet::Module.new("mymod")
+ mod = PuppetSpec::Modules.create(
+ 'foobar',
+ @modpath,
+ :metadata => {
+ :dependencies => []
+ }
+ )
mod.unmet_dependencies.should == []
end
it "should only list unmet dependencies" do
- mod = Puppet::Module.new("mymod")
- mod.stubs(:dependencies).returns [
- {
- "version_requirement" => ">= 2.2.0",
- "name" => "baz/satisfied"
- },
- {
- "version_requirement" => ">= 2.2.0",
- "name" => "baz/notsatisfied"
+ mod = PuppetSpec::Modules.create(
+ 'mymod',
+ @modpath,
+ :metadata => {
+ :dependencies => [
+ {
+ "version_requirement" => ">= 2.2.0",
+ "name" => "baz/satisfied"
+ },
+ {
+ "version_requirement" => ">= 2.2.0",
+ "name" => "baz/notsatisfied"
+ }
+ ]
}
- ]
-
- satisfied = Puppet::Module.new("satisfied")
- satisfied.version = "3.3.0"
-
- mod.environment.expects(:module).with("satisfied").returns satisfied
- mod.environment.expects(:module).with("notsatisfied").returns nil
+ )
+ PuppetSpec::Modules.create(
+ 'satisfied',
+ @modpath,
+ :metadata => {
+ :version => '3.3.0',
+ :author => 'baz'
+ }
+ )
mod.unmet_dependencies.should == [{
- :name => 'baz/notsatisfied',
- :error => <<-HEREDOC.gsub(/^\s{10}/, '')
- Missing dependency `notsatisfied`:
- `mymod` () requires `baz/notsatisfied` (>= 2.2.0)
- HEREDOC
+ :reason => :missing,
+ :mod_details => { :installed_version => nil },
+ :parent => { :version => "v9.9.9", :name => "puppetlabs/mymod" },
+ :name => "baz/notsatisfied",
+ :version_constraint => ">= 2.2.0"
}]
end
it "should be empty when all dependencies are met" do
- mod = Puppet::Module.new("mymod")
- mod.stubs(:dependencies).returns [
- {
- "version_requirement" => ">= 2.2.0",
- "name" => "baz/satisfied"
- },
- {
- "version_requirement" => "< 2.2.0",
- "name" => "baz/alsosatisfied"
+ mod = PuppetSpec::Modules.create(
+ 'mymod2',
+ @modpath,
+ :metadata => {
+ :dependencies => [
+ {
+ "version_requirement" => ">= 2.2.0",
+ "name" => "baz/satisfied"
+ },
+ {
+ "version_requirement" => "< 2.2.0",
+ "name" => "baz/alsosatisfied"
+ }
+ ]
}
- ]
- satisfied = Puppet::Module.new("satisfied")
- satisfied.version = "3.3.0"
- alsosatisfied = Puppet::Module.new("alsosatisfied")
- alsosatisfied.version = "2.1.0"
-
- mod.environment.expects(:module).with("satisfied").returns satisfied
- mod.environment.expects(:module).with("alsosatisfied").returns alsosatisfied
+ )
+ PuppetSpec::Modules.create(
+ 'satisfied',
+ @modpath,
+ :metadata => {
+ :version => '3.3.0',
+ :author => 'baz'
+ }
+ )
+ PuppetSpec::Modules.create(
+ 'alsosatisfied',
+ @modpath,
+ :metadata => {
+ :version => '2.1.0',
+ :author => 'baz'
+ }
+ )
mod.unmet_dependencies.should be_empty
end
end
describe "when managing supported platforms" do
it "should support specifying a supported platform" do
mod = Puppet::Module.new("mymod")
mod.supports "solaris"
end
it "should support specifying a supported platform and version" do
mod = Puppet::Module.new("mymod")
mod.supports "solaris", 1.0
end
it "should fail when not running on a supported platform" do
pending "Not sure how to send client platform to the module"
mod = Puppet::Module.new("mymod")
Facter.expects(:value).with("operatingsystem").returns "Solaris"
mod.supports "hpux"
lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::UnsupportedPlatform)
end
it "should fail when supported platforms are present but of the wrong version" do
pending "Not sure how to send client platform to the module"
mod = Puppet::Module.new("mymod")
Facter.expects(:value).with("operatingsystem").returns "Solaris"
Facter.expects(:value).with("operatingsystemrelease").returns 2.0
mod.supports "Solaris", 1.0
lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform)
end
it "should be considered supported when no supported platforms have been specified" do
pending "Not sure how to send client platform to the module"
mod = Puppet::Module.new("mymod")
lambda { mod.validate_supported_platform }.should_not raise_error
end
it "should be considered supported when running on a supported platform" do
pending "Not sure how to send client platform to the module"
mod = Puppet::Module.new("mymod")
Facter.expects(:value).with("operatingsystem").returns "Solaris"
Facter.expects(:value).with("operatingsystemrelease").returns 2.0
mod.supports "Solaris", 1.0
lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform)
end
it "should be considered supported when running on any of multiple supported platforms" do
pending "Not sure how to send client platform to the module"
end
it "should validate its platform support on initialization" do
pending "Not sure how to send client platform to the module"
end
end
it "should return nil if asked for a module whose name is 'nil'" do
Puppet::Module.find(nil, "myenv").should be_nil
end
it "should provide support for logging" do
Puppet::Module.ancestors.should be_include(Puppet::Util::Logging)
end
it "should be able to be converted to a string" do
Puppet::Module.new("foo").to_s.should == "Module foo"
end
it "should add the path to its string form if the module is found" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns "/a"
mod.to_s.should == "Module foo(/a)"
end
it "should fail if its name is not alphanumeric" do
lambda { Puppet::Module.new(".something") }.should raise_error(Puppet::Module::InvalidName)
end
it "should require a name at initialization" do
lambda { Puppet::Module.new }.should raise_error(ArgumentError)
end
it "should convert an environment name into an Environment instance" do
Puppet::Module.new("foo", :environment => "prod").environment.should be_instance_of(Puppet::Node::Environment)
end
it "should accept an environment at initialization" do
Puppet::Module.new("foo", :environment => :prod).environment.name.should == :prod
end
it "should use the default environment if none is provided" do
env = Puppet::Node::Environment.new
Puppet::Module.new("foo").environment.should equal(env)
end
it "should use any provided Environment instance" do
env = Puppet::Node::Environment.new
Puppet::Module.new("foo", :environment => env).environment.should equal(env)
end
describe ".path" do
before do
dir = tmpdir("deep_path")
@first = File.join(dir, "first")
@second = File.join(dir, "second")
Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}"
FileUtils.mkdir_p(@first)
FileUtils.mkdir_p(@second)
end
it "should return the path to the first found instance in its environment's module paths as its path" do
modpath = File.join(@first, "foo")
FileUtils.mkdir_p(modpath)
# Make a second one, which we shouldn't find
FileUtils.mkdir_p(File.join(@second, "foo"))
mod = Puppet::Module.new("foo")
mod.path.should == modpath
end
it "should be able to find itself in a directory other than the first directory in the module path" do
modpath = File.join(@second, "foo")
FileUtils.mkdir_p(modpath)
mod = Puppet::Module.new("foo")
mod.should be_exist
mod.path.should == modpath
end
it "should be able to find itself in a directory other than the first directory in the module path even when it exists in the first" do
environment = Puppet::Node::Environment.new
first_modpath = File.join(@first, "foo")
FileUtils.mkdir_p(first_modpath)
second_modpath = File.join(@second, "foo")
FileUtils.mkdir_p(second_modpath)
mod = Puppet::Module.new("foo", :environment => environment, :path => second_modpath)
mod.path.should == File.join(@second, "foo")
mod.environment.should == environment
end
end
+ describe '#modulepath' do
+ it "should return the directory the module is installed in, if a path exists" do
+ mod = Puppet::Module.new("foo")
+ mod.stubs(:path).returns "/a/foo"
+ mod.modulepath.should == '/a'
+ end
+
+ it "should return nil if no path exists" do
+ mod = Puppet::Module.new("foo")
+ mod.stubs(:path).returns nil
+ mod.modulepath.should be_nil
+ end
+ end
+
it "should be considered existent if it exists in at least one module path" do
mod = Puppet::Module.new("foo")
mod.expects(:path).returns "/a/foo"
mod.should be_exist
end
it "should be considered nonexistent if it does not exist in any of the module paths" do
mod = Puppet::Module.new("foo")
mod.expects(:path).returns nil
mod.should_not be_exist
end
[:plugins, :templates, :files, :manifests].each do |filetype|
dirname = filetype == :plugins ? "lib" : filetype.to_s
it "should be able to return individual #{filetype}" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns "/a/foo"
path = File.join("/a/foo", dirname, "my/file")
FileTest.expects(:exist?).with(path).returns true
mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should == path
end
it "should consider #{filetype} to be present if their base directory exists" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns "/a/foo"
path = File.join("/a/foo", dirname)
FileTest.expects(:exist?).with(path).returns true
mod.send(filetype.to_s + "?").should be_true
end
it "should consider #{filetype} to be absent if their base directory does not exist" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns "/a/foo"
path = File.join("/a/foo", dirname)
FileTest.expects(:exist?).with(path).returns false
mod.send(filetype.to_s + "?").should be_false
end
it "should consider #{filetype} to be absent if the module base directory does not exist" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns nil
mod.send(filetype.to_s + "?").should be_false
end
it "should return nil if asked to return individual #{filetype} that don't exist" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns "/a/foo"
path = File.join("/a/foo", dirname, "my/file")
FileTest.expects(:exist?).with(path).returns false
mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil
end
it "should return nil when asked for individual #{filetype} if the module does not exist" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns nil
mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil
end
it "should return the base directory if asked for a nil path" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns "/a/foo"
base = File.join("/a/foo", dirname)
FileTest.expects(:exist?).with(base).returns true
mod.send(filetype.to_s.sub(/s$/, ''), nil).should == base
end
end
it "should return the path to the plugin directory" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns "/a/foo"
mod.plugin_directory.should == "/a/foo/lib"
end
it "should throw a warning if plugins are in a 'plugins' directory rather than a 'lib' directory" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns "/a/foo"
FileTest.expects(:exist?).with("/a/foo/plugins").returns true
Puppet.expects(:deprecation_warning).with("using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'")
mod.plugin_directory.should == "/a/foo/plugins"
end
it "should default to 'lib' for the plugins directory" do
mod = Puppet::Module.new("foo")
mod.stubs(:path).returns "/a/foo"
mod.plugin_directory.should == "/a/foo/lib"
end
end
describe Puppet::Module, "when finding matching manifests" do
before do
@mod = Puppet::Module.new("mymod")
@mod.stubs(:path).returns "/a"
@pq_glob_with_extension = "yay/*.xx"
@fq_glob_with_extension = "/a/manifests/#{@pq_glob_with_extension}"
end
it "should return all manifests matching the glob pattern" do
Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar})
FileTest.stubs(:directory?).returns false
@mod.match_manifests(@pq_glob_with_extension).should == %w{foo bar}
end
it "should not return directories" do
Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar})
FileTest.expects(:directory?).with("foo").returns false
FileTest.expects(:directory?).with("bar").returns true
@mod.match_manifests(@pq_glob_with_extension).should == %w{foo}
end
it "should default to the 'init' file if no glob pattern is specified" do
Dir.expects(:glob).with("/a/manifests/init.{pp,rb}").returns(%w{/a/manifests/init.pp})
@mod.match_manifests(nil).should == %w{/a/manifests/init.pp}
end
it "should return all manifests matching the glob pattern in all existing paths" do
Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{a b})
@mod.match_manifests(@pq_glob_with_extension).should == %w{a b}
end
it "should match the glob pattern plus '.{pp,rb}' if no extention is specified" do
Dir.expects(:glob).with("/a/manifests/yay/foo.{pp,rb}").returns(%w{yay})
@mod.match_manifests("yay/foo").should == %w{yay}
end
it "should return an empty array if no manifests matched" do
Dir.expects(:glob).with(@fq_glob_with_extension).returns([])
@mod.match_manifests(@pq_glob_with_extension).should == []
end
end
describe Puppet::Module do
include PuppetSpec::Files
before do
@modpath = tmpdir('modpath')
@module = PuppetSpec::Modules.create('mymod', @modpath)
end
it "should use 'License' in its current path as its metadata file" do
@module.license_file.should == "#{@modpath}/mymod/License"
end
it "should return nil as its license file when the module has no path" do
Puppet::Module.any_instance.stubs(:path).returns nil
Puppet::Module.new("foo").license_file.should be_nil
end
it "should cache the license file" do
@module.expects(:path).once.returns nil
@module.license_file
@module.license_file
end
it "should use 'metadata.json' in its current path as its metadata file" do
@module.metadata_file.should == "#{@modpath}/mymod/metadata.json"
end
it "should return nil as its metadata file when the module has no path" do
Puppet::Module.any_instance.stubs(:path).returns nil
Puppet::Module.new("foo").metadata_file.should be_nil
end
it "should cache the metadata file" do
Puppet::Module.any_instance.expects(:path).once.returns nil
mod = Puppet::Module.new("foo")
mod.metadata_file.should == mod.metadata_file
end
it "should have metadata if it has a metadata file and its data is not empty" do
FileTest.expects(:exist?).with(@module.metadata_file).returns true
File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}"
@module.should be_has_metadata
end
it "should have metadata if it has a metadata file and its data is not empty" do
FileTest.expects(:exist?).with(@module.metadata_file).returns true
File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}"
@module.should be_has_metadata
end
it "should not have metadata if has a metadata file and its data is empty" do
FileTest.expects(:exist?).with(@module.metadata_file).returns true
File.stubs(:read).with(@module.metadata_file).returns "/*
+-----------------------------------------------------------------------+
| |
| ==> DO NOT EDIT THIS FILE! <== |
| |
| You should edit the `Modulefile` and run `puppet-module build` |
| to generate the `metadata.json` file for your releases. |
| |
+-----------------------------------------------------------------------+
*/
{}"
@module.should_not be_has_metadata
end
it "should know if it is missing a metadata file" do
FileTest.expects(:exist?).with(@module.metadata_file).returns false
@module.should_not be_has_metadata
end
it "should be able to parse its metadata file" do
@module.should respond_to(:load_metadata)
end
it "should parse its metadata file on initialization if it is present" do
Puppet::Module.any_instance.expects(:has_metadata?).returns true
Puppet::Module.any_instance.expects(:load_metadata)
Puppet::Module.new("yay")
end
describe "when loading the metadata file", :if => Puppet.features.pson? do
before do
@data = {
:license => "GPL2",
:author => "luke",
:version => "1.0",
:source => "http://foo/",
:puppetversion => "0.25",
:dependencies => []
}
@text = @data.to_pson
@module = Puppet::Module.new("foo")
@module.stubs(:metadata_file).returns "/my/file"
File.stubs(:read).with("/my/file").returns @text
end
%w{source author version license}.each do |attr|
it "should set #{attr} if present in the metadata file" do
@module.load_metadata
@module.send(attr).should == @data[attr.to_sym]
end
it "should fail if #{attr} is not present in the metadata file" do
@data.delete(attr.to_sym)
@text = @data.to_pson
File.stubs(:read).with("/my/file").returns @text
lambda { @module.load_metadata }.should raise_error(
Puppet::Module::MissingMetadata,
"No #{attr} module metadata provided for foo"
)
end
end
it "should set puppetversion if present in the metadata file" do
@module.load_metadata
@module.puppetversion.should == @data[:puppetversion]
end
+ context "when versionRequirement is used for dependency version info" do
+ before do
+ @data = {
+ :license => "GPL2",
+ :author => "luke",
+ :version => "1.0",
+ :source => "http://foo/",
+ :puppetversion => "0.25",
+ :dependencies => [
+ {
+ "versionRequirement" => "0.0.1",
+ "name" => "pmtacceptance/stdlib"
+ },
+ {
+ "versionRequirement" => "0.1.0",
+ "name" => "pmtacceptance/apache"
+ }
+ ]
+ }
+ @text = @data.to_pson
+
+ @module = Puppet::Module.new("foo")
+ @module.stubs(:metadata_file).returns "/my/file"
+ File.stubs(:read).with("/my/file").returns @text
+ end
+
+ it "should set the dependency version_requirement key" do
+ @module.load_metadata
+ @module.dependencies[0]['version_requirement'].should == "0.0.1"
+ end
+
+ it "should set the version_requirement key for all dependencies" do
+ @module.load_metadata
+ @module.dependencies[0]['version_requirement'].should == "0.0.1"
+ @module.dependencies[1]['version_requirement'].should == "0.1.0"
+ end
+ end
+
it "should fail if the discovered name is different than the metadata name"
end
- it "should be able to tell if there are local changes" do
+ it "should be able to tell if there are local changes", :fails_on_windows => true do
modpath = tmpdir('modpath')
foo_checksum = 'acbd18db4cc2f85cedef654fccc4a4d8'
checksummed_module = PuppetSpec::Modules.create(
'changed',
modpath,
:metadata => {
:checksums => {
"foo" => foo_checksum,
}
}
)
foo_path = Pathname.new(File.join(checksummed_module.path, 'foo'))
IO.binwrite(foo_path, 'notfoo')
Puppet::Module::Tool::Checksums.new(foo_path).checksum(foo_path).should_not == foo_checksum
checksummed_module.has_local_changes?.should be_true
IO.binwrite(foo_path, 'foo')
Puppet::Module::Tool::Checksums.new(foo_path).checksum(foo_path).should == foo_checksum
checksummed_module.has_local_changes?.should be_false
end
it "should know what other modules require it" do
Puppet.settings[:modulepath] = @modpath
dependable = PuppetSpec::Modules.create(
'dependable',
@modpath,
:metadata => {:author => 'puppetlabs'}
)
PuppetSpec::Modules.create(
'needy',
@modpath,
:metadata => {
:author => 'beggar',
:dependencies => [{
"version_requirement" => ">= 2.2.0",
"name" => "puppetlabs/dependable"
}]
}
)
PuppetSpec::Modules.create(
'wantit',
@modpath,
:metadata => {
:author => 'spoiled',
:dependencies => [{
"version_requirement" => "< 5.0.0",
"name" => "puppetlabs/dependable"
}]
}
)
dependable.required_by.should =~ [
{
"name" => "beggar/needy",
"version" => "9.9.9",
"version_requirement" => ">= 2.2.0"
},
{
"name" => "spoiled/wantit",
"version" => "9.9.9",
"version_requirement" => "< 5.0.0"
}
]
end
end
diff --git a/spec/unit/module_tool/application_spec.rb b/spec/unit/module_tool/application_spec.rb
index b86ec5c39..a2ef184f7 100644
--- a/spec/unit/module_tool/application_spec.rb
+++ b/spec/unit/module_tool/application_spec.rb
@@ -1,29 +1,27 @@
require 'spec_helper'
require 'puppet/module_tool'
-describe Puppet::Module::Tool::Applications::Application do
+describe Puppet::Module::Tool::Applications::Application, :fails_on_windows => true do
describe 'app' do
good_versions = %w{ 1.2.4 0.0.1 0.0.0 0.0.2-git-8-g3d316d1 0.0.3-b1 10.100.10000
0.1.2-rc1 0.1.2-dev-1 0.1.2-svn12345 0.1.2-3 }
bad_versions = %w{ 0.1 0 0.1.2.3 dev 0.1.2beta }
before do
@app = Class.new(described_class).new
end
good_versions.each do |ver|
it "should accept version string #{ver}" do
- @app.instance_eval("@filename=%q{puppetlabs-ntp-#{ver}}")
- @app.parse_filename!
+ @app.parse_filename("puppetlabs-ntp-#{ver}")
end
end
bad_versions.each do |ver|
it "should not accept version string #{ver}" do
- @app.instance_eval("@filename=%q{puppetlabs-ntp-#{ver}}")
- lambda { @app.parse_filename! }.should raise_error
+ lambda { @app.parse_filename("puppetlabs-ntp-#{ver}") }.should raise_error
end
end
end
end
diff --git a/spec/unit/module_tool/applications/application_spec.rb b/spec/unit/module_tool/applications/application_spec.rb
new file mode 100644
index 000000000..101079f61
--- /dev/null
+++ b/spec/unit/module_tool/applications/application_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'puppet/module_tool/applications'
+
+describe Puppet::Module::Tool::Applications do
+ module Puppet::Module::Tool
+ module Applications
+ class Fake < Application
+ end
+ end
+ end
+
+ it "should raise an error on microsoft windows" do
+ Puppet.features.stubs(:microsoft_windows?).returns true
+ expect { Puppet::Module::Tool::Applications::Fake.new }.to raise_error(
+ Puppet::Error,
+ "`puppet module` actions are currently not supported on Microsoft Windows"
+ )
+ end
+end
diff --git a/spec/unit/module_tool/applications/installer_spec.rb b/spec/unit/module_tool/applications/installer_spec.rb
new file mode 100644
index 000000000..1ad80ee60
--- /dev/null
+++ b/spec/unit/module_tool/applications/installer_spec.rb
@@ -0,0 +1,205 @@
+require 'spec_helper'
+require 'puppet/module_tool/applications'
+require 'puppet_spec/modules'
+require 'semver'
+
+describe Puppet::Module::Tool::Applications::Installer, :fails_on_windows => true do
+ include PuppetSpec::Files
+
+ before do
+ FileUtils.mkdir_p(modpath1)
+ fake_env.modulepath = [modpath1]
+ FileUtils.touch(stdlib_pkg)
+ Puppet.settings[:modulepath] = modpath1
+ Puppet::Forge.stubs(:remote_dependency_info).returns(remote_dependency_info)
+ Puppet::Forge.stubs(:repository).returns(repository)
+ end
+
+ let(:unpacker) { stub(:run) }
+ let(:installer_class) { Puppet::Module::Tool::Applications::Installer }
+ let(:modpath1) { File.join(tmpdir("installer"), "modpath1") }
+ let(:stdlib_pkg) { File.join(modpath1, "pmtacceptance-stdlib-0.0.1.tar.gz") }
+ let(:fake_env) { Puppet::Node::Environment.new('fake_env') }
+ let(:options) { Hash[:target_dir => modpath1] }
+
+ let(:repository) do
+ repository = mock()
+ repository.stubs(:uri => 'forge-dev.puppetlabs.com')
+
+ releases = remote_dependency_info.each_key do |mod|
+ remote_dependency_info[mod].each do |release|
+ repository.stubs(:retrieve).with(release['file'])\
+ .returns("/fake_cache#{release['file']}")
+ end
+ end
+
+ repository
+ end
+
+ let(:remote_dependency_info) do
+ {
+ "pmtacceptance/stdlib" => [
+ { "dependencies" => [],
+ "version" => "0.0.1",
+ "file" => "/pmtacceptance-stdlib-0.0.1.tar.gz" },
+ { "dependencies" => [],
+ "version" => "0.0.2",
+ "file" => "/pmtacceptance-stdlib-0.0.2.tar.gz" },
+ { "dependencies" => [],
+ "version" => "1.0.0",
+ "file" => "/pmtacceptance-stdlib-1.0.0.tar.gz" }
+ ],
+ "pmtacceptance/java" => [
+ { "dependencies" => [["pmtacceptance/stdlib", ">= 0.0.1"]],
+ "version" => "1.7.0",
+ "file" => "/pmtacceptance-java-1.7.0.tar.gz" },
+ { "dependencies" => [["pmtacceptance/stdlib", "1.0.0"]],
+ "version" => "1.7.1",
+ "file" => "/pmtacceptance-java-1.7.1.tar.gz" }
+ ],
+ "pmtacceptance/apollo" => [
+ { "dependencies" => [
+ ["pmtacceptance/java", "1.7.1"],
+ ["pmtacceptance/stdlib", "0.0.1"]
+ ],
+ "version" => "0.0.1",
+ "file" => "/pmtacceptance-apollo-0.0.1.tar.gz" },
+ { "dependencies" => [
+ ["pmtacceptance/java", ">= 1.7.0"],
+ ["pmtacceptance/stdlib", ">= 1.0.0"]
+ ],
+ "version" => "0.0.2",
+ "file" => "/pmtacceptance-apollo-0.0.2.tar.gz" }
+ ]
+ }
+ end
+
+ describe "the behavior of .is_module_package?" do
+ it "should return true when file is a module package" do
+ installer = installer_class.new("foo", options)
+ installer.send(:is_module_package?, stdlib_pkg).should be_true
+ end
+
+ it "should return false when file is not a module package" do
+ installer = installer_class.new("foo", options)
+ installer.send(:is_module_package?, "pmtacceptance-apollo-0.0.2.tar").should be_false
+ end
+ end
+
+ context "when the source is a repository" do
+ it "should require a valid name" do
+ lambda { installer_class.run('puppet', params) }.should
+ raise_error(ArgumentError, "Could not install module with invalid name: puppet")
+ end
+
+ it "should install the requested module" do
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options)\
+ .returns(unpacker)
+ results = installer_class.run('pmtacceptance-stdlib', options)
+ results[:installed_modules].length == 1
+ results[:installed_modules][0][:module].should == "pmtacceptance-stdlib"
+ results[:installed_modules][0][:version][:vstring].should == "1.0.0"
+ end
+
+ context "when the requested module has dependencies" do
+ it "should install dependencies" do
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options)\
+ .returns(unpacker)
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\
+ .returns(unpacker)
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-java-1.7.1.tar.gz', options)\
+ .returns(unpacker)
+
+ results = installer_class.run('pmtacceptance-apollo', options)
+ installed_dependencies = results[:installed_modules][0][:dependencies]
+
+ dependencies = installed_dependencies.inject({}) do |result, dep|
+ result[dep[:module]] = dep[:version][:vstring]
+ result
+ end
+
+ dependencies.length.should == 2
+ dependencies['pmtacceptance-java'].should == '1.7.1'
+ dependencies['pmtacceptance-stdlib'].should == '1.0.0'
+ end
+
+ it "should install requested module if the '--force' flag is used" do
+ options = { :force => true, :target_dir => modpath1 }
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\
+ .returns(unpacker)
+ results = installer_class.run('pmtacceptance-apollo', options)
+ results[:installed_modules][0][:module].should == "pmtacceptance-apollo"
+ end
+
+ it "should not install dependencies if the '--force' flag is used" do
+ options = { :force => true, :target_dir => modpath1 }
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\
+ .returns(unpacker)
+ results = installer_class.run('pmtacceptance-apollo', options)
+ dependencies = results[:installed_modules][0][:dependencies]
+ dependencies.should == []
+ end
+
+ it "should not install dependencies if the '--ignore-dependencies' flag is used" do
+ options = { :ignore_dependencies => true, :target_dir => modpath1 }
+ Puppet::Module::Tool::Applications::Unpacker.expects(:new)\
+ .with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options)\
+ .returns(unpacker)
+ results = installer_class.run('pmtacceptance-apollo', options)
+ dependencies = results[:installed_modules][0][:dependencies]
+ dependencies.should == []
+ end
+
+ it "should set an error if dependencies can't be resolved" do
+ options = { :version => '0.0.1', :target_dir => modpath1 }
+ oneline = "'pmtacceptance-apollo' (v0.0.1) requested; Invalid dependency cycle"
+ multiline = <<-MSG.strip
+Could not install module 'pmtacceptance-apollo' (v0.0.1)
+ No version of 'pmtacceptance-stdlib' will satisfy dependencies
+ You specified 'pmtacceptance-apollo' (v0.0.1),
+ which depends on 'pmtacceptance-java' (v1.7.1),
+ which depends on 'pmtacceptance-stdlib' (v1.0.0)
+ Use `puppet module install --force` to install this module anyway
+MSG
+
+ results = installer_class.run('pmtacceptance-apollo', options)
+ results[:result].should == :failure
+ results[:error][:oneline].should == oneline
+ results[:error][:multiline].should == multiline
+ end
+ end
+
+ context "when there are modules installed" do
+ it "should use local version when already exists and satisfies constraints"
+ it "should reinstall the local version if force is used"
+ it "should upgrade local version when necessary to satisfy constraints"
+ it "should error when a local version can't be upgraded to satisfy constraints"
+ end
+
+ context "when a local module needs upgrading to satisfy constraints but has changes" do
+ it "should error"
+ it "should warn and continue if force is used"
+ end
+
+ it "should error when a local version of a dependency has no version metadata"
+ it "should error when a local version of a dependency has a non-semver version"
+ it "should error when a local version of a dependency has a different forge name"
+ it "should error when a local version of a dependency has no metadata"
+ end
+
+ context "when the source is a filesystem" do
+ before do
+ @sourcedir = tmpdir('sourcedir')
+ end
+
+ it "should error if it can't parse the name"
+
+ it "should try to get_release_package_from_filesystem if it has a valid name"
+ end
+end
diff --git a/spec/unit/module_tool/applications/uninstaller_spec.rb b/spec/unit/module_tool/applications/uninstaller_spec.rb
new file mode 100644
index 000000000..e8a4b3f46
--- /dev/null
+++ b/spec/unit/module_tool/applications/uninstaller_spec.rb
@@ -0,0 +1,206 @@
+require 'spec_helper'
+require 'puppet/module_tool'
+require 'tmpdir'
+require 'puppet_spec/modules'
+
+describe Puppet::Module::Tool::Applications::Uninstaller, :fails_on_windows => true do
+ include PuppetSpec::Files
+
+ def mkmod(name, path, metadata=nil)
+ modpath = File.join(path, name)
+ FileUtils.mkdir_p(modpath)
+
+ if metadata
+ File.open(File.join(modpath, 'metadata.json'), 'w') do |f|
+ f.write(metadata.to_pson)
+ end
+ end
+
+ modpath
+ end
+
+ describe "the behavior of the instances" do
+
+ before do
+ @uninstaller = Puppet::Module::Tool::Applications::Uninstaller
+ FileUtils.mkdir_p(modpath1)
+ FileUtils.mkdir_p(modpath2)
+ fake_env.modulepath = [modpath1, modpath2]
+ end
+
+ let(:modpath1) { File.join(tmpdir("uninstaller"), "modpath1") }
+ let(:modpath2) { File.join(tmpdir("uninstaller"), "modpath2") }
+ let(:fake_env) { Puppet::Node::Environment.new('fake_env') }
+ let(:options) { {:environment => "fake_env"} }
+
+ let(:foo_metadata) do
+ {
+ :author => "puppetlabs",
+ :name => "puppetlabs/foo",
+ :version => "1.0.0",
+ :source => "http://dummyurl/foo",
+ :license => "Apache2",
+ :dependencies => [],
+ }
+ end
+
+ let(:bar_metadata) do
+ {
+ :author => "puppetlabs",
+ :name => "puppetlabs/bar",
+ :version => "1.0.0",
+ :source => "http://dummyurl/bar",
+ :license => "Apache2",
+ :dependencies => [],
+ }
+ end
+
+ context "when the module is not installed" do
+ it "should fail" do
+ @uninstaller.new('fakemod_not_installed', options).run[:result].should == :failure
+ end
+ end
+
+ context "when the module is installed" do
+
+ it "should uninstall the module" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ end
+
+ it "should only uninstall the requested module" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+ PuppetSpec::Modules.create('bar', modpath1, :metadata => bar_metadata)
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ end
+
+ it "should uninstall fail if a module exists twice in the modpath" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+ PuppetSpec::Modules.create('foo', modpath2, :metadata => foo_metadata)
+
+ @uninstaller.new('puppetlabs-foo', options).run[:result].should == :failure
+ end
+
+ context "when options[:version] is specified" do
+
+ it "should uninstall the module if the version matches" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ options[:version] = "1.0.0"
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length.should == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ results[:affected_modules].first.version.should == "1.0.0"
+ end
+
+ it "should not uninstall the module if the version does not match" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ options[:version] = "2.0.0"
+
+ @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ end
+ end
+
+ context "when the module metadata is missing" do
+
+ it "should not uninstall the module" do
+ PuppetSpec::Modules.create('foo', modpath1)
+
+ @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ end
+ end
+
+ context "when the module has local changes" do
+
+ it "should not uninstall the module" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+ Puppet::Module.any_instance.stubs(:has_local_changes?).returns(true)
+
+ @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ end
+
+ end
+
+ context "when the module does not have local changes" do
+
+ it "should uninstall the module" do
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length.should == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ end
+ end
+
+ context "when uninstalling the module will cause broken dependencies" do
+ it "should not uninstall the module" do
+ Puppet.settings[:modulepath] = modpath1
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ PuppetSpec::Modules.create(
+ 'needy',
+ modpath1,
+ :metadata => {
+ :author => 'beggar',
+ :dependencies => [{
+ "version_requirement" => ">= 1.0.0",
+ "name" => "puppetlabs/foo"
+ }]
+ }
+ )
+
+ @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ end
+ end
+
+ context "when using the --force flag" do
+
+ let(:fakemod) do
+ stub(
+ :forge_name => 'puppetlabs/fakemod',
+ :version => '0.0.1',
+ :has_local_changes? => true
+ )
+ end
+
+ it "should ignore local changes" do
+ foo = mkmod("foo", modpath1, foo_metadata)
+ options[:force] = true
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length.should == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ end
+
+ it "should ignore broken dependencies" do
+ Puppet.settings[:modulepath] = modpath1
+ PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+
+ PuppetSpec::Modules.create(
+ 'needy',
+ modpath1,
+ :metadata => {
+ :author => 'beggar',
+ :dependencies => [{
+ "version_requirement" => ">= 1.0.0",
+ "name" => "puppetlabs/foo"
+ }]
+ }
+ )
+ options[:force] = true
+
+ results = @uninstaller.new("puppetlabs-foo", options).run
+ results[:affected_modules].length.should == 1
+ results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/unit/module_tool/applications/upgrader_spec.rb b/spec/unit/module_tool/applications/upgrader_spec.rb
new file mode 100644
index 000000000..542e00b7a
--- /dev/null
+++ b/spec/unit/module_tool/applications/upgrader_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'puppet/module_tool/applications'
+require 'puppet_spec/modules'
+require 'semver'
+
+describe Puppet::Module::Tool::Applications::Upgrader, :fails_on_windows => true do
+ include PuppetSpec::Files
+
+ before do
+ end
+
+ it "should update the requested module"
+ it "should not update dependencies"
+ it "should fail when updating a dependency to an unsupported version"
+ it "should fail when updating a module that is not installed"
+ it "should warn when the latest version is already installed"
+ it "should warn when the best version is already installed"
+
+ context "when using the '--version' option" do
+ it "should update an installed module to the requested version"
+ end
+
+ context "when using the '--force' flag" do
+ it "should ignore missing dependencies"
+ it "should ignore version constraints"
+ it "should not update a module that is not installed"
+ end
+
+ context "when using the '--env' option" do
+ it "should use the correct environment"
+ end
+
+ context "when there are missing dependencies" do
+ it "should fail to upgrade the original module"
+ it "should raise an error"
+ end
+end
diff --git a/spec/unit/module_tool/uninstaller_spec.rb b/spec/unit/module_tool/uninstaller_spec.rb
deleted file mode 100644
index 7a5a93aff..000000000
--- a/spec/unit/module_tool/uninstaller_spec.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-require 'spec_helper'
-require 'puppet/module_tool'
-require 'tmpdir'
-
-describe Puppet::Module::Tool::Applications::Uninstaller do
- include PuppetSpec::Files
-
- def mkmod(name, path, metadata=nil)
- modpath = File.join(path, name)
- FileUtils.mkdir_p(modpath)
-
- # For some tests we need the metadata to be present, mainly
- # when testing against specific versions of a module.
- if metadata
- File.open(File.join(modpath, 'metadata.json'), 'w') do |f|
- f.write(metadata.to_pson)
- end
- end
-
- modpath
- end
-
- describe "the behavior of the instances" do
-
- before do
- @uninstaller = Puppet::Module::Tool::Applications::Uninstaller
- FileUtils.mkdir_p(modpath1)
- FileUtils.mkdir_p(modpath2)
- fake_env.modulepath = [modpath1, modpath2]
- end
-
- let(:modpath1) { File.join(tmpdir("uninstaller"), "modpath1") }
- let(:modpath2) { File.join(tmpdir("uninstaller"), "modpath2") }
- let(:fake_env) { Puppet::Node::Environment.new('fake_env') }
- let(:options) { {:environment => "fake_env"} }
-
- context "when the module is not installed" do
- it "should return an empty list" do
- results = @uninstaller.new('fakemod_not_installed', options).run
- results[:removed_mods].should == []
- end
- end
-
- context "when the module is installed" do
- it "should uninstall the module" do
- foo = mkmod("foo", modpath1)
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].should == [
- Puppet::Module.new('foo', :environment => fake_env, :path => foo)
- ]
- end
-
- it "should only uninstall the requested module" do
- foo = mkmod("foo", modpath1)
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].should == [
- Puppet::Module.new("foo", :environment => fake_env, :path => foo)
- ]
- end
-
- it "should uninstall the module from every path in the modpath" do
- foo1 = mkmod('foo', modpath1)
- foo2 = mkmod('foo', modpath2)
-
- results = @uninstaller.new('foo', options).run
- results[:removed_mods].length.should == 2
- results[:removed_mods].should include(
- Puppet::Module.new('foo', :environment => fake_env, :path => foo1),
- Puppet::Module.new('foo', :environment => fake_env, :path => foo2)
- )
- end
-
- context "when options[:version] is specified" do
- let(:metadata) do
- {
- "author" => "",
- "name" => "foo",
- "version" => "1.0.0",
- "source" => "http://dummyurl",
- "license" => "Apache2",
- "dependencies" => [],
- }
- end
-
- it "should uninstall the module if the version matches" do
- foo = mkmod('foo', modpath1, metadata)
-
- options[:version] = "1.0.0"
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].length.should == 1
- results[:removed_mods].first.name.should == "foo"
- results[:removed_mods].first.version.should == "1.0.0"
- end
-
- it "should not uninstall the module if the version does not match" do
- foo = mkmod("foo", modpath1, metadata)
-
- options[:version] = "2.0.0"
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].should == []
- end
-
- context "when the module metadata is missing" do
- it "should not uninstall the module" do
- foo = mkmod("foo", modpath1)
-
- options[:version] = "2.0.0"
-
- results = @uninstaller.new("foo", options).run
- results[:removed_mods].should == []
- end
- end
- end
-
- # This test is pending work in #11803 to which will add
- # dependency resolution.
- it "should check for broken dependencies"
- end
- end
-end
diff --git a/spec/unit/module_tool_spec.rb b/spec/unit/module_tool_spec.rb
index 86d421e69..1517f30f5 100644
--- a/spec/unit/module_tool_spec.rb
+++ b/spec/unit/module_tool_spec.rb
@@ -1,5 +1,113 @@
+# encoding: UTF-8
+
require 'spec_helper'
require 'puppet/module_tool'
-describe Puppet::Module::Tool do
+describe Puppet::Module::Tool, :fails_on_windows => true do
+ describe '.format_tree' do
+ it 'should return an empty tree when given an empty list' do
+ subject.format_tree([]).should == ''
+ end
+
+ it 'should return a shallow when given a list without dependencies' do
+ list = [ { :text => 'first' }, { :text => 'second' }, { :text => 'third' } ]
+ subject.format_tree(list).should == <<-TREE
+├── first
+├── second
+└── third
+TREE
+ end
+
+ it 'should return a deeply nested tree when given a list with deep dependencies' do
+ list = [
+ {
+ :text => 'first',
+ :dependencies => [
+ {
+ :text => 'second',
+ :dependencies => [
+ { :text => 'third' }
+ ]
+ }
+ ]
+ },
+ ]
+ subject.format_tree(list).should == <<-TREE
+└─┬ first
+ └─┬ second
+ └── third
+TREE
+ end
+
+ it 'should show connectors when deep dependencies are not on the last node of the top level' do
+ list = [
+ {
+ :text => 'first',
+ :dependencies => [
+ {
+ :text => 'second',
+ :dependencies => [
+ { :text => 'third' }
+ ]
+ }
+ ]
+ },
+ { :text => 'fourth' }
+ ]
+ subject.format_tree(list).should == <<-TREE
+├─┬ first
+│ └─┬ second
+│ └── third
+└── fourth
+TREE
+ end
+
+ it 'should show connectors when deep dependencies are not on the last node of any level' do
+ list = [
+ {
+ :text => 'first',
+ :dependencies => [
+ {
+ :text => 'second',
+ :dependencies => [
+ { :text => 'third' }
+ ]
+ },
+ { :text => 'fourth' }
+ ]
+ }
+ ]
+ subject.format_tree(list).should == <<-TREE
+└─┬ first
+ ├─┬ second
+ │ └── third
+ └── fourth
+TREE
+ end
+
+ it 'should show connectors in every case when deep dependencies are not on the last node' do
+ list = [
+ {
+ :text => 'first',
+ :dependencies => [
+ {
+ :text => 'second',
+ :dependencies => [
+ { :text => 'third' }
+ ]
+ },
+ { :text => 'fourth' }
+ ]
+ },
+ { :text => 'fifth' }
+ ]
+ subject.format_tree(list).should == <<-TREE
+├─┬ first
+│ ├─┬ second
+│ │ └── third
+│ └── fourth
+└── fifth
+TREE
+ end
+ end
end
diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb
index 17d55bf28..34afc4449 100755
--- a/spec/unit/node/environment_spec.rb
+++ b/spec/unit/node/environment_spec.rb
@@ -1,432 +1,444 @@
#!/usr/bin/env rspec
require 'spec_helper'
require 'tmpdir'
require 'puppet/node/environment'
require 'puppet/util/execution'
require 'puppet_spec/modules'
describe Puppet::Node::Environment do
let(:env) { Puppet::Node::Environment.new("testing") }
include PuppetSpec::Files
after do
Puppet::Node::Environment.clear
end
it "should use the filetimeout for the ttl for the modulepath" do
Puppet::Node::Environment.attr_ttl(:modulepath).should == Integer(Puppet[:filetimeout])
end
it "should use the filetimeout for the ttl for the module list" do
Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout])
end
it "should use the default environment if no name is provided while initializing an environment" do
Puppet.settings.expects(:value).with(:environment).returns("one")
Puppet::Node::Environment.new.name.should == :one
end
it "should treat environment instances as singletons" do
Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one"))
end
it "should treat an environment specified as names or strings as equivalent" do
Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one"))
end
it "should return its name when converted to a string" do
Puppet::Node::Environment.new(:one).to_s.should == "one"
end
it "should just return any provided environment if an environment is provided as the name" do
one = Puppet::Node::Environment.new(:one)
Puppet::Node::Environment.new(one).should equal(one)
end
describe "when managing known resource types" do
before do
@collection = Puppet::Resource::TypeCollection.new(env)
env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new(''))
Thread.current[:known_resource_types] = nil
end
it "should create a resource type collection if none exists" do
Puppet::Resource::TypeCollection.expects(:new).with(env).returns @collection
env.known_resource_types.should equal(@collection)
end
it "should reuse any existing resource type collection" do
env.known_resource_types.should equal(env.known_resource_types)
end
it "should perform the initial import when creating a new collection" do
env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new(''))
env.known_resource_types
end
it "should return the same collection even if stale if it's the same thread" do
Puppet::Resource::TypeCollection.stubs(:new).returns @collection
env.known_resource_types.stubs(:stale?).returns true
env.known_resource_types.should equal(@collection)
end
it "should return the current thread associated collection if there is one" do
Thread.current[:known_resource_types] = @collection
env.known_resource_types.should equal(@collection)
end
it "should give to all threads using the same environment the same collection if the collection isn't stale" do
original_thread_type_collection = Puppet::Resource::TypeCollection.new(env)
Puppet::Resource::TypeCollection.expects(:new).with(env).returns original_thread_type_collection
env.known_resource_types.should equal(original_thread_type_collection)
original_thread_type_collection.expects(:require_reparse?).returns(false)
Puppet::Resource::TypeCollection.stubs(:new).with(env).returns @collection
t = Thread.new {
env.known_resource_types.should equal(original_thread_type_collection)
}
t.join
end
it "should generate a new TypeCollection if the current one requires reparsing" do
old_type_collection = env.known_resource_types
old_type_collection.stubs(:require_reparse?).returns true
Thread.current[:known_resource_types] = nil
new_type_collection = env.known_resource_types
new_type_collection.should be_a Puppet::Resource::TypeCollection
new_type_collection.should_not equal(old_type_collection)
end
end
it "should validate the modulepath directories" do
real_file = tmpdir('moduledir')
path = %W[/one /two #{real_file}].join(File::PATH_SEPARATOR)
Puppet[:modulepath] = path
env.modulepath.should == [real_file]
end
it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do
Puppet::Util.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do
module_path = %w{/one /two}.join(File::PATH_SEPARATOR)
env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two}
env.expects(:[]).with(:modulepath).returns module_path
env.modulepath.should == %w{/l1 /l2 /one /two}
end
end
describe "when validating modulepath or manifestdir directories" do
before :each do
@path_one = tmpdir("path_one")
@path_two = tmpdir("path_one")
sep = File::PATH_SEPARATOR
Puppet[:modulepath] = "#{@path_one}#{sep}#{@path_two}"
end
it "should not return non-directories" do
FileTest.expects(:directory?).with(@path_one).returns true
FileTest.expects(:directory?).with(@path_two).returns false
env.validate_dirs([@path_one, @path_two]).should == [@path_one]
end
it "should use the current working directory to fully-qualify unqualified paths" do
FileTest.stubs(:directory?).returns true
two = File.expand_path(File.join(Dir.getwd, "two"))
env.validate_dirs([@path_one, 'two']).should == [@path_one, two]
end
end
describe "when modeling a specific environment" do
it "should have a method for returning the environment name" do
Puppet::Node::Environment.new("testing").name.should == :testing
end
it "should provide an array-like accessor method for returning any environment-specific setting" do
env.should respond_to(:[])
end
it "should ask the Puppet settings instance for the setting qualified with the environment name" do
Puppet.settings.expects(:value).with("myvar", :testing).returns("myval")
env["myvar"].should == "myval"
end
it "should be able to return an individual module that exists in its module path" do
mod = mock 'module'
Puppet::Module.expects(:new).with("one", :environment => env).returns mod
mod.expects(:exist?).returns true
env.module("one").should equal(mod)
end
it "should return nil if asked for a module that does not exist in its path" do
modpath = tmpdir('modpath')
env.modulepath = [modpath]
env.module("one").should be_nil
end
describe "module data" do
before do
dir = tmpdir("deep_path")
@first = File.join(dir, "first")
@second = File.join(dir, "second")
Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}"
FileUtils.mkdir_p(@first)
FileUtils.mkdir_p(@second)
end
describe "#modules_by_path" do
it "should return an empty list if there are no modules" do
env.modules_by_path.should == {
@first => [],
@second => []
}
end
it "should include modules even if they exist in multiple dirs in the modulepath" do
modpath1 = File.join(@first, "foo")
FileUtils.mkdir_p(modpath1)
modpath2 = File.join(@second, "foo")
FileUtils.mkdir_p(modpath2)
env.modules_by_path.should == {
@first => [Puppet::Module.new('foo', :environment => env, :path => modpath1)],
@second => [Puppet::Module.new('foo', :environment => env, :path => modpath2)]
}
end
+
+ it "should ignore modules with invalid names" do
+ FileUtils.mkdir_p(File.join(@first, 'foo'))
+ FileUtils.mkdir_p(File.join(@first, 'foo2'))
+ FileUtils.mkdir_p(File.join(@first, 'foo-bar'))
+ FileUtils.mkdir_p(File.join(@first, 'foo_bar'))
+ FileUtils.mkdir_p(File.join(@first, 'foo=bar'))
+ FileUtils.mkdir_p(File.join(@first, 'foo bar'))
+ FileUtils.mkdir_p(File.join(@first, 'foo.bar'))
+
+ env.modules_by_path[@first].collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar}
+ end
+
end
describe "#module_requirements" do
it "should return a list of what modules depend on other modules" do
PuppetSpec::Modules.create(
'foo',
@first,
:metadata => {
:author => 'puppetlabs',
:dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => ">= 1.0.0" }]
}
)
PuppetSpec::Modules.create(
'bar',
@second,
:metadata => {
:author => 'puppetlabs',
:dependencies => [{ 'name' => 'puppetlabs/foo', "version_requirement" => "<= 2.0.0" }]
}
)
PuppetSpec::Modules.create(
'baz',
@first,
:metadata => {
:author => 'puppetlabs',
:dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "3.0.0" }]
}
)
PuppetSpec::Modules.create(
'alpha',
@first,
:metadata => {
:author => 'puppetlabs',
:dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "~3.0.0" }]
}
)
env.module_requirements.should == {
'puppetlabs/alpha' => [],
'puppetlabs/foo' => [
{
"name" => "puppetlabs/bar",
"version" => "9.9.9",
"version_requirement" => "<= 2.0.0"
}
],
'puppetlabs/bar' => [
{
"name" => "puppetlabs/alpha",
"version" => "9.9.9",
"version_requirement" => "~3.0.0"
},
{
"name" => "puppetlabs/baz",
"version" => "9.9.9",
"version_requirement" => "3.0.0"
},
{
"name" => "puppetlabs/foo",
"version" => "9.9.9",
"version_requirement" => ">= 1.0.0"
}
],
'puppetlabs/baz' => []
}
end
end
describe ".module_by_forge_name" do
it "should find modules by forge_name" do
mod = PuppetSpec::Modules.create(
'baz',
@first,
:metadata => {:author => 'puppetlabs'},
:environment => env
)
env.module_by_forge_name('puppetlabs/baz').should == mod
end
it "should not find modules with same name by the wrong author" do
mod = PuppetSpec::Modules.create(
'baz',
@first,
:metadata => {:author => 'sneakylabs'},
:environment => env
)
env.module_by_forge_name('puppetlabs/baz').should == nil
end
it "should return nil when the module can't be found" do
env.module_by_forge_name('ima/nothere').should be_nil
end
end
describe ".modules" do
it "should return an empty list if there are no modules" do
env.modules.should == []
end
it "should return a module named for every directory in each module path" do
%w{foo bar}.each do |mod_name|
FileUtils.mkdir_p(File.join(@first, mod_name))
end
%w{bee baz}.each do |mod_name|
FileUtils.mkdir_p(File.join(@second, mod_name))
end
env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort
end
it "should remove duplicates" do
FileUtils.mkdir_p(File.join(@first, 'foo'))
FileUtils.mkdir_p(File.join(@second, 'foo'))
env.modules.collect{|mod| mod.name}.sort.should == %w{foo}
end
it "should ignore modules with invalid names" do
FileUtils.mkdir_p(File.join(@first, 'foo'))
FileUtils.mkdir_p(File.join(@first, 'foo2'))
FileUtils.mkdir_p(File.join(@first, 'foo-bar'))
FileUtils.mkdir_p(File.join(@first, 'foo_bar'))
FileUtils.mkdir_p(File.join(@first, 'foo=bar'))
FileUtils.mkdir_p(File.join(@first, 'foo bar'))
env.modules.collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar}
end
it "should create modules with the correct environment" do
FileUtils.mkdir_p(File.join(@first, 'foo'))
-
env.modules.each {|mod| mod.environment.should == env }
end
end
end
it "should cache the module list" do
env.modulepath = %w{/a}
Dir.expects(:entries).once.with("/a").returns %w{foo}
env.modules
env.modules
end
end
describe Puppet::Node::Environment::Helper do
before do
@helper = Object.new
@helper.extend(Puppet::Node::Environment::Helper)
end
it "should be able to set and retrieve the environment as a symbol" do
@helper.environment = :foo
@helper.environment.name.should == :foo
end
it "should accept an environment directly" do
@helper.environment = Puppet::Node::Environment.new(:foo)
@helper.environment.name.should == :foo
end
it "should accept an environment as a string" do
@helper.environment = 'foo'
@helper.environment.name.should == :foo
end
end
describe "when performing initial import" do
before do
@parser = Puppet::Parser::Parser.new("test")
Puppet::Parser::Parser.stubs(:new).returns @parser
end
it "should set the parser's string to the 'code' setting and parse if code is available" do
Puppet.settings[:code] = "my code"
@parser.expects(:string=).with "my code"
@parser.expects(:parse)
env.instance_eval { perform_initial_import }
end
it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do
filename = tmpfile('myfile')
File.open(filename, 'w'){|f| }
Puppet.settings[:manifest] = filename
@parser.expects(:file=).with filename
@parser.expects(:parse)
env.instance_eval { perform_initial_import }
end
it "should pass the manifest file to the parser even if it does not exist on disk" do
filename = tmpfile('myfile')
Puppet.settings[:code] = ""
Puppet.settings[:manifest] = filename
@parser.expects(:file=).with(filename).once
@parser.expects(:parse).once
env.instance_eval { perform_initial_import }
end
it "should fail helpfully if there is an error importing" do
File.stubs(:exist?).returns true
env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env)
@parser.expects(:file=).once
@parser.expects(:parse).raises ArgumentError
lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error)
end
it "should not do anything if the ignore_import settings is set" do
Puppet.settings[:ignoreimport] = true
@parser.expects(:string=).never
@parser.expects(:file=).never
@parser.expects(:parse).never
env.instance_eval { perform_initial_import }
end
it "should mark the type collection as needing a reparse when there is an error parsing" do
@parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...")
env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env)
lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../)
env.known_resource_types.require_reparse?.should be_true
end
end
end
diff --git a/spec/unit/parser/functions/create_resources_spec.rb b/spec/unit/parser/functions/create_resources_spec.rb
index cdf61b84b..6301c3ea9 100755
--- a/spec/unit/parser/functions/create_resources_spec.rb
+++ b/spec/unit/parser/functions/create_resources_spec.rb
@@ -1,157 +1,171 @@
require 'puppet'
require 'spec_helper'
describe 'function for dynamically creating resources' do
def get_scope
@topscope = Puppet::Parser::Scope.new
# This is necessary so we don't try to use the compiler to discover our parent.
@topscope.parent = nil
@scope = Puppet::Parser::Scope.new
@scope.compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("floppy", :environment => 'production'))
@scope.parent = @topscope
@compiler = @scope.compiler
end
before :each do
get_scope
Puppet::Parser::Functions.function(:create_resources)
end
it "should exist" do
Puppet::Parser::Functions.function(:create_resources).should == "function_create_resources"
end
it 'should require two or three arguments' do
expect { @scope.function_create_resources(['foo']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (1; must be 2 or 3)')
expect { @scope.function_create_resources(['foo', 'bar', 'blah', 'baz']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (4; must be 2 or 3)')
end
+ describe 'when the caller does not supply a name parameter' do
+ it 'should set a default resource name equal to the resource title' do
+ Puppet::Parser::Resource.any_instance.expects(:set_parameter).with(:name, 'test').once
+ @scope.function_create_resources(['notify', {'test'=>{}}])
+ end
+ end
+ describe 'when the caller supplies a name parameter' do
+ it 'should set the resource name to the value provided' do
+ Puppet::Parser::Resource.any_instance.expects(:set_parameter).with(:name, 'user_supplied').once
+ Puppet::Parser::Resource.any_instance.expects(:set_parameter).with(:name, 'test').never
+ @scope.function_create_resources(['notify', {'test'=>{'name' => 'user_supplied'}}])
+ end
+ end
+
describe 'when creating native types' do
before :each do
Puppet[:code]='notify{test:}'
get_scope
@scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope)
end
it 'empty hash should not cause resources to be added' do
@scope.function_create_resources(['file', {}])
@compiler.catalog.resources.size == 1
end
it 'should be able to add' do
@scope.function_create_resources(['file', {'/etc/foo'=>{'ensure'=>'present'}}])
@compiler.catalog.resource(:file, "/etc/foo")['ensure'].should == 'present'
end
it 'should accept multiple types' do
type_hash = {}
type_hash['foo'] = {'message' => 'one'}
type_hash['bar'] = {'message' => 'two'}
@scope.function_create_resources(['notify', type_hash])
@compiler.catalog.resource(:notify, "foo")['message'].should == 'one'
@compiler.catalog.resource(:notify, "bar")['message'].should == 'two'
end
it 'should fail to add non-existing type' do
expect { @scope.function_create_resources(['create-resource-foo', {}]) }.should raise_error(ArgumentError, 'could not create resource of unknown type create-resource-foo')
end
it 'should be able to add edges' do
@scope.function_create_resources(['notify', {'foo'=>{'require' => 'Notify[test]'}}])
@scope.compiler.compile
rg = @scope.compiler.catalog.to_ral.relationship_graph
test = rg.vertices.find { |v| v.title == 'test' }
foo = rg.vertices.find { |v| v.title == 'foo' }
test.should be
foo.should be
rg.path_between(test,foo).should be
end
it 'should account for default values' do
@scope.function_create_resources(['file', {'/etc/foo'=>{'ensure'=>'present'}, '/etc/baz'=>{'group'=>'food'}}, {'group' => 'bar'}])
@compiler.catalog.resource(:file, "/etc/foo")['group'].should == 'bar'
@compiler.catalog.resource(:file, "/etc/baz")['group'].should == 'food'
end
end
describe 'when dynamically creating resource types' do
before :each do
Puppet[:code]=
'define foocreateresource($one){notify{$name: message => $one}}
notify{test:}
'
get_scope
@scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope)
Puppet::Parser::Functions.function(:create_resources)
end
it 'should be able to create defined resoure types' do
@scope.function_create_resources(['foocreateresource', {'blah'=>{'one'=>'two'}}])
# still have to compile for this to work...
# I am not sure if this constraint ruins the tests
@scope.compiler.compile
@compiler.catalog.resource(:notify, "blah")['message'].should == 'two'
end
it 'should fail if defines are missing params' do
@scope.function_create_resources(['foocreateresource', {'blah'=>{}}])
expect { @scope.compiler.compile }.should raise_error(Puppet::ParseError, 'Must pass one to Foocreateresource[blah]')
end
it 'should be able to add multiple defines' do
hash = {}
hash['blah'] = {'one' => 'two'}
hash['blaz'] = {'one' => 'three'}
@scope.function_create_resources(['foocreateresource', hash])
# still have to compile for this to work...
# I am not sure if this constraint ruins the tests
@scope.compiler.compile
@compiler.catalog.resource(:notify, "blah")['message'].should == 'two'
@compiler.catalog.resource(:notify, "blaz")['message'].should == 'three'
end
it 'should be able to add edges' do
@scope.function_create_resources(['foocreateresource', {'blah'=>{'one'=>'two', 'require' => 'Notify[test]'}}])
@scope.compiler.compile
rg = @scope.compiler.catalog.to_ral.relationship_graph
test = rg.vertices.find { |v| v.title == 'test' }
blah = rg.vertices.find { |v| v.title == 'blah' }
test.should be
blah.should be
# (Yoda speak like we do)
rg.path_between(test,blah).should be
@compiler.catalog.resource(:notify, "blah")['message'].should == 'two'
end
it 'should account for default values' do
@scope.function_create_resources(['foocreateresource', {'blah'=>{}}, {'one' => 'two'}])
@scope.compiler.compile
@compiler.catalog.resource(:notify, "blah")['message'].should == 'two'
end
end
describe 'when creating classes' do
before :each do
Puppet[:code]=
'class bar($one){notify{test: message => $one}}
notify{tester:}
'
get_scope
@scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope)
Puppet::Parser::Functions.function(:create_resources)
end
it 'should be able to create classes' do
@scope.function_create_resources(['class', {'bar'=>{'one'=>'two'}}])
@scope.compiler.compile
@compiler.catalog.resource(:notify, "test")['message'].should == 'two'
@compiler.catalog.resource(:class, "bar").should_not be_nil
end
it 'should fail to create non-existing classes' do
expect { @scope.function_create_resources(['class', {'blah'=>{'one'=>'two'}}]) }.should raise_error(ArgumentError ,'could not find hostclass blah')
end
it 'should be able to add edges' do
@scope.function_create_resources(['class', {'bar'=>{'one'=>'two', 'require' => 'Notify[tester]'}}])
@scope.compiler.compile
rg = @scope.compiler.catalog.to_ral.relationship_graph
test = rg.vertices.find { |v| v.title == 'test' }
tester = rg.vertices.find { |v| v.title == 'tester' }
test.should be
tester.should be
rg.path_between(tester,test).should be
end
it 'should account for default values' do
@scope.function_create_resources(['class', {'bar'=>{}}, {'one' => 'two'}])
@scope.compiler.compile
@compiler.catalog.resource(:notify, "test")['message'].should == 'two'
@compiler.catalog.resource(:class, "bar").should_not be_nil
end
end
end
diff --git a/spec/unit/provider/nameservice/directoryservice_spec.rb b/spec/unit/provider/nameservice/directoryservice_spec.rb
index e3d32d713..a09894385 100755
--- a/spec/unit/provider/nameservice/directoryservice_spec.rb
+++ b/spec/unit/provider/nameservice/directoryservice_spec.rb
@@ -1,179 +1,189 @@
#!/usr/bin/env rspec
require 'spec_helper'
# We use this as a reasonable way to obtain all the support infrastructure.
[:user, :group].each do |type_for_this_round|
provider_class = Puppet::Type.type(type_for_this_round).provider(:directoryservice)
describe provider_class do
before do
@resource = stub("resource")
@provider = provider_class.new(@resource)
end
it "[#6009] should handle nested arrays of members" do
current = ["foo", "bar", "baz"]
desired = ["foo", ["quux"], "qorp"]
group = 'example'
@resource.stubs(:[]).with(:name).returns(group)
@resource.stubs(:[]).with(:auth_membership).returns(true)
@provider.instance_variable_set(:@property_value_cache_hash,
{ :members => current })
%w{bar baz}.each do |del|
@provider.expects(:execute).once.
with([:dseditgroup, '-o', 'edit', '-n', '.', '-d', del, group])
end
%w{quux qorp}.each do |add|
@provider.expects(:execute).once.
with([:dseditgroup, '-o', 'edit', '-n', '.', '-a', add, group])
end
expect { @provider.set(:members, desired) }.should_not raise_error
end
end
end
describe 'DirectoryService.single_report' do
it 'should fail on OS X < 10.5' do
Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.4")
lambda {
Puppet::Provider::NameService::DirectoryService.single_report('resource_name')
}.should raise_error(RuntimeError, "Puppet does not support OS X versions < 10.5")
end
it 'should use plist data on >= 10.5' do
Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.5")
Puppet::Provider::NameService::DirectoryService.stubs(:get_ds_path).returns('Users')
Puppet::Provider::NameService::DirectoryService.stubs(:list_all_present).returns(
['root', 'user1', 'user2', 'resource_name']
)
Puppet::Provider::NameService::DirectoryService.stubs(:generate_attribute_hash)
Puppet::Provider::NameService::DirectoryService.stubs(:execute)
Puppet::Provider::NameService::DirectoryService.expects(:parse_dscl_plist_data)
Puppet::Provider::NameService::DirectoryService.single_report('resource_name')
end
end
describe 'DirectoryService.get_exec_preamble' do
it 'should fail on OS X < 10.5' do
Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.4")
lambda {
Puppet::Provider::NameService::DirectoryService.get_exec_preamble('-list')
}.should raise_error(RuntimeError, "Puppet does not support OS X versions < 10.5")
end
it 'should use plist data on >= 10.5' do
Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.5")
Puppet::Provider::NameService::DirectoryService.stubs(:get_ds_path).returns('Users')
Puppet::Provider::NameService::DirectoryService.get_exec_preamble('-list').should include("-plist")
end
end
describe 'DirectoryService password behavior' do
# The below is a binary plist containing a ShadowHashData key which CONTAINS
# another binary plist. The nested binary plist contains a 'SALTED-SHA512'
# key that contains a base64 encoded salted-SHA512 password hash...
let (:binary_plist) { "bplist00\324\001\002\003\004\005\006\a\bXCRAM-MD5RNT]SALTED-SHA512[RECOVERABLEO\020 \231k2\3360\200GI\201\355J\216\202\215y\243\001\206J\300\363\032\031\022\006\2359\024\257\217<\361O\020\020F\353\at\377\277\226\276c\306\254\031\037J(\235O\020D\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245O\021\002\000k\024\221\270x\353\001\237\346D}\377?\265]\356+\243\v[\350\316a\340h\376<\322\266\327\016\306n\272r\t\212A\253L\216\214\205\016\241 [\360/\335\002#\\A\372\241a\261\346\346\\\251\330\312\365\016\n\341\017\016\225&;\322\\\004*\ru\316\372\a \362?8\031\247\231\030\030\267\315\023\v\343{@\227\301s\372h\212\000a\244&\231\366\nt\277\2036,\027bZ+\223W\212g\333`\264\331N\306\307\362\257(^~ b\262\247&\231\261t\341\231%\244\247\203eOt\365\271\201\273\330\350\363C^A\327F\214!\217hgf\e\320k\260n\315u~\336\371M\t\235k\230S\375\311\303\240\351\037d\273\321y\335=K\016`_\317\230\2612_\023K\036\350\v\232\323Y\310\317_\035\227%\237\v\340\023\016\243\233\025\306:\227\351\370\364x\234\231\266\367\016w\275\333-\351\210}\375x\034\262\272kRuHa\362T/F!\347B\231O`K\304\037'k$$\245h)e\363\365mT\b\317\\2\361\026\351\254\375Jl1~\r\371\267\352\2322I\341\272\376\243^Un\266E7\230[VocUJ\220N\2116D/\025f=\213\314\325\vG}\311\360\377DT\307m\261&\263\340\272\243_\020\271rG^BW\210\030l\344\0324\335\233\300\023\272\225Im\330\n\227*Yv[\006\315\330y'\a\321\373\273A\240\305F{S\246I#/\355\2425\031\031GGF\270y\n\331\004\023G@\331\000\361\343\350\264$\032\355_\210y\000\205\342\375\212q\024\004\026W:\205 \363v?\035\270L-\270=\022\323\2003\v\336\277\t\237\356\374\n\267n\003\367\342\330;\371S\326\016`B6@Njm>\240\021%\336\345\002(P\204Yn\3279l\0228\264\254\304\2528t\372h\217\347sA\314\345\245\337)]\000\b\000\021\000\032\000\035\000+\0007\000Z\000m\000\264\000\000\000\000\000\000\002\001\000\000\000\000\000\000\000\t\000\000\000\000\000\000\000\000\000\000\000\000\000\000\002\270" }
# The below is a base64 encoded salted-SHA512 password hash.
let (:pw_string) { "\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245" }
# The below is a salted-SHA512 password hash in hex.
let (:sha512_hash) { 'dd067bfc346740ff7a84d20dda7411d80a03a64b93ee1c2150b1c5741de6ea7086036ea74d4d41c8c15a3cf6a6130e315733e0ef00cf5409c1c92b84a64c37bef8d02aa5' }
let :plist_path do
'/var/db/dslocal/nodes/Default/users/jeff.plist'
end
let :ds_provider do
Puppet::Provider::NameService::DirectoryService
end
let :shadow_hash_data do
{'ShadowHashData' => [StringIO.new(binary_plist)]}
end
subject do
Puppet::Provider::NameService::DirectoryService
end
before :each do
subject.expects(:get_macosx_version_major).returns("10.7")
end
it 'should execute convert_binary_to_xml once when getting the password on >= 10.7' do
subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)})
File.expects(:exists?).with(plist_path).once.returns(true)
Plist.expects(:parse_xml).returns(shadow_hash_data)
# On Mac OS X 10.7 we first need to convert to xml when reading the password
subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path)
subject.get_password('uid', 'jeff')
end
it 'should fail if a salted-SHA512 password hash is not passed in >= 10.7' do
expect {
subject.set_password('jeff', 'uid', 'badpassword')
}.should raise_error(RuntimeError, /OS X 10.7 requires a Salted SHA512 hash password of 136 characters./)
end
it 'should convert xml-to-binary and binary-to-xml when setting the pw on >= 10.7' do
subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)})
subject.expects(:convert_xml_to_binary).returns(binary_plist)
File.expects(:exists?).with(plist_path).once.returns(true)
Plist.expects(:parse_xml).returns(shadow_hash_data)
# On Mac OS X 10.7 we first need to convert to xml
subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path)
# And again back to a binary plist or DirectoryService will complain
subject.expects(:plutil).with('-convert', 'binary1', plist_path)
Plist::Emit.expects(:save_plist).with(shadow_hash_data, plist_path)
subject.set_password('jeff', 'uid', sha512_hash)
end
+
+ it '[#13686] should handle an empty ShadowHashData field in the users plist' do
+ subject.expects(:convert_xml_to_binary).returns(binary_plist)
+ File.expects(:exists?).with(plist_path).once.returns(true)
+ Plist.expects(:parse_xml).returns({'ShadowHashData' => nil})
+ subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path)
+ subject.expects(:plutil).with('-convert', 'binary1', plist_path)
+ Plist::Emit.expects(:save_plist)
+ subject.set_password('jeff', 'uid', sha512_hash)
+ end
end
describe '(#4855) directoryservice group resource failure' do
let :provider_class do
Puppet::Type.type(:group).provider(:directoryservice)
end
let :group_members do
['root','jeff']
end
let :user_account do
['root']
end
let :stub_resource do
stub('resource')
end
subject do
provider_class.new(stub_resource)
end
before :each do
@resource = stub("resource")
@provider = provider_class.new(@resource)
end
it 'should delete a group member if the user does not exist' do
stub_resource.stubs(:[]).with(:name).returns('fake_group')
stub_resource.stubs(:name).returns('fake_group')
subject.expects(:execute).with([:dseditgroup, '-o', 'edit', '-n', '.',
'-d', 'jeff',
'fake_group']).raises(Puppet::ExecutionFailure,
'it broke')
subject.expects(:execute).with([:dscl, '.', '-delete',
'/Groups/fake_group', 'GroupMembership',
'jeff'])
subject.remove_unwanted_members(group_members, user_account)
end
end
diff --git a/spec/unit/reports/tagmail_spec.rb b/spec/unit/reports/tagmail_spec.rb
index a53d11978..00f78c932 100755
--- a/spec/unit/reports/tagmail_spec.rb
+++ b/spec/unit/reports/tagmail_spec.rb
@@ -1,91 +1,168 @@
#!/usr/bin/env rspec
require 'spec_helper'
require 'puppet/reports'
tagmail = Puppet::Reports.report(:tagmail)
describe tagmail do
before do
@processor = Puppet::Transaction::Report.new("apply")
@processor.extend(Puppet::Reports.report(:tagmail))
end
passers = my_fixture "tagmail_passers.conf"
File.readlines(passers).each do |line|
it "should be able to parse '#{line.inspect}'" do
@processor.parse(line)
end
end
failers = my_fixture "tagmail_failers.conf"
File.readlines(failers).each do |line|
it "should not be able to parse '#{line.inspect}'" do
lambda { @processor.parse(line) }.should raise_error(ArgumentError)
end
end
{
"tag: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag}, []],
"tag.localhost: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag.localhost}, []],
"tag, other: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag other}, []],
"tag-other: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag-other}, []],
"tag, !other: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag}, %w{other}],
"tag, !other, one, !two: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag one}, %w{other two}],
"tag: abuse@domain.com, other@domain.com" => [%w{abuse@domain.com other@domain.com}, %w{tag}, []]
}.each do |line, results|
it "should parse '#{line}' as #{results.inspect}" do
@processor.parse(line).shift.should == results
end
end
describe "when matching logs" do
before do
@processor << Puppet::Util::Log.new(:level => :notice, :message => "first", :tags => %w{one})
@processor << Puppet::Util::Log.new(:level => :notice, :message => "second", :tags => %w{one two})
@processor << Puppet::Util::Log.new(:level => :notice, :message => "third", :tags => %w{one two three})
end
def match(pos = [], neg = [])
pos = Array(pos)
neg = Array(neg)
result = @processor.match([[%w{abuse@domain.com}, pos, neg]])
actual_result = result.shift
if actual_result
actual_result[1]
else
nil
end
end
it "should match all messages when provided the 'all' tag as a positive matcher" do
results = match("all")
%w{first second third}.each do |str|
results.should be_include(str)
end
end
it "should remove messages that match a negated tag" do
match("all", "three").should_not be_include("third")
end
it "should find any messages tagged with a provided tag" do
results = match("two")
results.should be_include("second")
results.should be_include("third")
results.should_not be_include("first")
end
it "should allow negation of specific tags from a specific tag list" do
results = match("two", "three")
results.should be_include("second")
results.should_not be_include("third")
end
it "should allow a tag to negate all matches" do
results = match([], "one")
results.should be_nil
end
end
+
+ describe "the behavior of tagmail.process" do
+ before do
+ Puppet[:tagmap] = my_fixture "tagmail_email.conf"
+ end
+
+ let(:processor) do
+ processor = Puppet::Transaction::Report.new("apply")
+ processor.extend(Puppet::Reports.report(:tagmail))
+ processor
+ end
+
+ context "when any messages match a positive tag" do
+ before do
+ processor << log_entry
+ end
+
+ let(:log_entry) do
+ Puppet::Util::Log.new(
+ :level => :notice, :message => "Secure change", :tags => %w{secure})
+ end
+
+ let(:message) do
+ "#{log_entry.time} Puppet (notice): Secure change"
+ end
+
+ it "should send email if there are changes" do
+ processor.expects(:send).with([[['user@domain.com'], message]])
+ processor.expects(:raw_summary).returns({
+ "resources" => { "changed" => 1, "out_of_sync" => 0 }
+ })
+
+ processor.process
+ end
+
+ it "should send email if there are resources out of sync" do
+ processor.expects(:send).with([[['user@domain.com'], message]])
+ processor.expects(:raw_summary).returns({
+ "resources" => { "changed" => 0, "out_of_sync" => 1 }
+ })
+
+ processor.process
+ end
+
+ it "should not send email if no changes or resources out of sync" do
+ processor.expects(:send).never
+ processor.expects(:raw_summary).returns({
+ "resources" => { "changed" => 0, "out_of_sync" => 0 }
+ })
+
+ processor.process
+ end
+
+ it "should log a message if no changes or resources out of sync" do
+ processor.expects(:send).never
+ processor.expects(:raw_summary).returns({
+ "resources" => { "changed" => 0, "out_of_sync" => 0 }
+ })
+
+ Puppet.expects(:notice).with("Not sending tagmail report; no changes")
+ processor.process
+ end
+
+ it "should send email if raw_summary is not defined" do
+ processor.expects(:send).with([[['user@domain.com'], message]])
+ processor.expects(:raw_summary).returns(nil)
+ processor.process
+ end
+
+ it "should send email if there are no resource metrics" do
+ processor.expects(:send).with([[['user@domain.com'], message]])
+ processor.expects(:raw_summary).returns({'resources' => nil})
+ processor.process
+ end
+ end
+ end
end
+
diff --git a/spec/unit/semver_spec.rb b/spec/unit/semver_spec.rb
index 87fc968ba..8da6324fb 100644
--- a/spec/unit/semver_spec.rb
+++ b/spec/unit/semver_spec.rb
@@ -1,280 +1,288 @@
require 'spec_helper'
require 'semver'
describe SemVer do
describe '::valid?' do
it 'should validate basic version strings' do
%w[ 0.0.0 999.999.999 v0.0.0 v999.999.999 ].each do |vstring|
SemVer.valid?(vstring).should be_true
end
end
it 'should validate special version strings' do
%w[ 0.0.0-foo 999.999.999-bar v0.0.0-a v999.999.999-beta ].each do |vstring|
SemVer.valid?(vstring).should be_true
end
end
it 'should fail to validate invalid version strings' do
%w[ nope 0.0foo 999.999 x0.0.0 z.z.z 1.2.3beta 1.x.y ].each do |vstring|
SemVer.valid?(vstring).should be_false
end
end
end
describe '::find_matching' do
before :all do
@versions = %w[
0.0.1
0.0.2
1.0.0-rc1
1.0.0-rc2
1.0.0
1.0.1
1.1.0
1.1.1
1.1.2
1.1.3
1.1.4
1.2.0
1.2.1
2.0.0-rc1
].map { |v| SemVer.new(v) }
end
it 'should match exact versions by string' do
@versions.each do |version|
SemVer.find_matching(version, @versions).should == version
end
end
it 'should return nil if no versions match' do
%w[ 3.0.0 2.0.0-rc2 1.0.0-alpha ].each do |v|
SemVer.find_matching(v, @versions).should be_nil
end
end
it 'should find the greatest match for partial versions' do
SemVer.find_matching('1.0', @versions).should == 'v1.0.1'
SemVer.find_matching('1.1', @versions).should == 'v1.1.4'
SemVer.find_matching('1', @versions).should == 'v1.2.1'
SemVer.find_matching('2', @versions).should == 'v2.0.0-rc1'
SemVer.find_matching('2.1', @versions).should == nil
end
it 'should find the greatest match for versions with placeholders' do
SemVer.find_matching('1.0.x', @versions).should == 'v1.0.1'
SemVer.find_matching('1.1.x', @versions).should == 'v1.1.4'
SemVer.find_matching('1.x', @versions).should == 'v1.2.1'
SemVer.find_matching('1.x.x', @versions).should == 'v1.2.1'
SemVer.find_matching('2.x', @versions).should == 'v2.0.0-rc1'
SemVer.find_matching('2.x.x', @versions).should == 'v2.0.0-rc1'
SemVer.find_matching('2.1.x', @versions).should == nil
end
end
describe '::[]' do
it "should produce expected ranges" do
tests = {
- '1.2.3' => SemVer.new('v1.2.3-') .. SemVer.new('v1.2.3'),
- '>1.2.3' => SemVer.new('v1.2.4-') .. SemVer::MAX,
- '<1.2.3' => SemVer::MIN ... SemVer.new('v1.2.3-'),
- '>=1.2.3' => SemVer.new('v1.2.3-') .. SemVer::MAX,
- '<=1.2.3' => SemVer::MIN .. SemVer.new('v1.2.3'),
- '>1.2.3 <1.2.5' => SemVer.new('v1.2.4-') ... SemVer.new('v1.2.5-'),
- '>=1.2.3 <=1.2.5' => SemVer.new('v1.2.3-') .. SemVer.new('v1.2.5'),
- '1.2.3 - 2.3.4' => SemVer.new('v1.2.3-') .. SemVer.new('v2.3.4'),
- '~1.2.3' => SemVer.new('v1.2.3-') ... SemVer.new('v1.3.0-'),
- '~1.2' => SemVer.new('v1.2.0-') ... SemVer.new('v2.0.0-'),
- '~1' => SemVer.new('v1.0.0-') ... SemVer.new('v2.0.0-'),
- '1.2.x' => SemVer.new('v1.2.0') ... SemVer.new('v1.3.0-'),
- '1.x' => SemVer.new('v1.0.0') ... SemVer.new('v2.0.0-'),
+ '1.2.3-alpha' => SemVer.new('v1.2.3-alpha') .. SemVer.new('v1.2.3-alpha'),
+ '1.2.3' => SemVer.new('v1.2.3-') .. SemVer.new('v1.2.3'),
+ '>1.2.3-alpha' => SemVer.new('v1.2.3-alpha-') .. SemVer::MAX,
+ '>1.2.3' => SemVer.new('v1.2.4-') .. SemVer::MAX,
+ '<1.2.3-alpha' => SemVer::MIN ... SemVer.new('v1.2.3-alpha'),
+ '<1.2.3' => SemVer::MIN ... SemVer.new('v1.2.3-'),
+ '>=1.2.3-alpha' => SemVer.new('v1.2.3-alpha') .. SemVer::MAX,
+ '>=1.2.3' => SemVer.new('v1.2.3-') .. SemVer::MAX,
+ '<=1.2.3-alpha' => SemVer::MIN .. SemVer.new('v1.2.3-alpha'),
+ '<=1.2.3' => SemVer::MIN .. SemVer.new('v1.2.3'),
+ '>1.2.3-a <1.2.3-b' => SemVer.new('v1.2.3-a-') ... SemVer.new('v1.2.3-b'),
+ '>1.2.3 <1.2.5' => SemVer.new('v1.2.4-') ... SemVer.new('v1.2.5-'),
+ '>=1.2.3-a <= 1.2.3-b' => SemVer.new('v1.2.3-a') .. SemVer.new('v1.2.3-b'),
+ '>=1.2.3 <=1.2.5' => SemVer.new('v1.2.3-') .. SemVer.new('v1.2.5'),
+ '1.2.3-a - 2.3.4-b' => SemVer.new('v1.2.3-a') .. SemVer.new('v2.3.4-b'),
+ '1.2.3 - 2.3.4' => SemVer.new('v1.2.3-') .. SemVer.new('v2.3.4'),
+ '~1.2.3' => SemVer.new('v1.2.3-') ... SemVer.new('v1.3.0-'),
+ '~1.2' => SemVer.new('v1.2.0-') ... SemVer.new('v2.0.0-'),
+ '~1' => SemVer.new('v1.0.0-') ... SemVer.new('v2.0.0-'),
+ '1.2.x' => SemVer.new('v1.2.0') ... SemVer.new('v1.3.0-'),
+ '1.x' => SemVer.new('v1.0.0') ... SemVer.new('v2.0.0-'),
}
tests.each do |vstring, expected|
SemVer[vstring].should == expected
end
end
it "should suit up" do
suitability = {
[ '1.2.3', 'v1.2.2' ] => false,
[ '>=1.2.3', 'v1.2.2' ] => false,
[ '<=1.2.3', 'v1.2.2' ] => true,
[ '>= 1.2.3', 'v1.2.2' ] => false,
[ '<= 1.2.3', 'v1.2.2' ] => true,
[ '1.2.3 - 1.2.4', 'v1.2.2' ] => false,
[ '~1.2.3', 'v1.2.2' ] => false,
[ '~1.2', 'v1.2.2' ] => true,
[ '~1', 'v1.2.2' ] => true,
[ '1.2.x', 'v1.2.2' ] => true,
[ '1.x', 'v1.2.2' ] => true,
[ '1.2.3', 'v1.2.3-alpha' ] => true,
[ '>=1.2.3', 'v1.2.3-alpha' ] => true,
[ '<=1.2.3', 'v1.2.3-alpha' ] => true,
[ '>= 1.2.3', 'v1.2.3-alpha' ] => true,
[ '<= 1.2.3', 'v1.2.3-alpha' ] => true,
[ '>1.2.3', 'v1.2.3-alpha' ] => false,
[ '<1.2.3', 'v1.2.3-alpha' ] => false,
[ '> 1.2.3', 'v1.2.3-alpha' ] => false,
[ '< 1.2.3', 'v1.2.3-alpha' ] => false,
[ '1.2.3 - 1.2.4', 'v1.2.3-alpha' ] => true,
[ '1.2.3 - 1.2.4', 'v1.2.4-alpha' ] => true,
[ '1.2.3 - 1.2.4', 'v1.2.5-alpha' ] => false,
[ '~1.2.3', 'v1.2.3-alpha' ] => true,
[ '~1.2.3', 'v1.3.0-alpha' ] => false,
[ '~1.2', 'v1.2.3-alpha' ] => true,
[ '~1.2', 'v2.0.0-alpha' ] => false,
[ '~1', 'v1.2.3-alpha' ] => true,
[ '~1', 'v2.0.0-alpha' ] => false,
[ '1.2.x', 'v1.2.3-alpha' ] => true,
[ '1.2.x', 'v1.3.0-alpha' ] => false,
[ '1.x', 'v1.2.3-alpha' ] => true,
[ '1.x', 'v2.0.0-alpha' ] => false,
[ '1.2.3', 'v1.2.3' ] => true,
[ '>=1.2.3', 'v1.2.3' ] => true,
[ '<=1.2.3', 'v1.2.3' ] => true,
[ '>= 1.2.3', 'v1.2.3' ] => true,
[ '<= 1.2.3', 'v1.2.3' ] => true,
[ '1.2.3 - 1.2.4', 'v1.2.3' ] => true,
[ '~1.2.3', 'v1.2.3' ] => true,
[ '~1.2', 'v1.2.3' ] => true,
[ '~1', 'v1.2.3' ] => true,
[ '1.2.x', 'v1.2.3' ] => true,
[ '1.x', 'v1.2.3' ] => true,
[ '1.2.3', 'v1.2.4' ] => false,
[ '>=1.2.3', 'v1.2.4' ] => true,
[ '<=1.2.3', 'v1.2.4' ] => false,
[ '>= 1.2.3', 'v1.2.4' ] => true,
[ '<= 1.2.3', 'v1.2.4' ] => false,
[ '1.2.3 - 1.2.4', 'v1.2.4' ] => true,
[ '~1.2.3', 'v1.2.4' ] => true,
[ '~1.2', 'v1.2.4' ] => true,
[ '~1', 'v1.2.4' ] => true,
[ '1.2.x', 'v1.2.4' ] => true,
[ '1.x', 'v1.2.4' ] => true,
}
suitability.each do |arguments, expected|
range, vstring = arguments
actual = SemVer[range] === SemVer.new(vstring)
actual.should == expected
end
end
end
describe 'instantiation' do
it 'should raise an exception when passed an invalid version string' do
expect { SemVer.new('invalidVersion') }.to raise_exception ArgumentError
end
it 'should populate the appropriate fields for a basic version string' do
version = SemVer.new('1.2.3')
version.major.should == 1
version.minor.should == 2
version.tiny.should == 3
version.special.should == ''
end
it 'should populate the appropriate fields for a special version string' do
version = SemVer.new('3.4.5-beta6')
version.major.should == 3
version.minor.should == 4
version.tiny.should == 5
version.special.should == '-beta6'
end
end
describe '#matched_by?' do
subject { SemVer.new('v1.2.3-beta') }
describe 'should match against' do
describe 'literal version strings' do
it { should be_matched_by('1.2.3-beta') }
it { should_not be_matched_by('1.2.3-alpha') }
it { should_not be_matched_by('1.2.4-beta') }
it { should_not be_matched_by('1.3.3-beta') }
it { should_not be_matched_by('2.2.3-beta') }
end
describe 'partial version strings' do
it { should be_matched_by('1.2.3') }
it { should be_matched_by('1.2') }
it { should be_matched_by('1') }
end
describe 'version strings with placeholders' do
it { should be_matched_by('1.2.x') }
it { should be_matched_by('1.x.3') }
it { should be_matched_by('1.x.x') }
it { should be_matched_by('1.x') }
end
end
end
describe 'comparisons' do
describe 'against a string' do
it 'should just work' do
SemVer.new('1.2.3').should == '1.2.3'
end
end
describe 'against a symbol' do
it 'should just work' do
SemVer.new('1.2.3').should == :'1.2.3'
end
end
describe 'on a basic version (v1.2.3)' do
subject { SemVer.new('v1.2.3') }
it { should == SemVer.new('1.2.3') }
# Different major versions
it { should > SemVer.new('0.2.3') }
it { should < SemVer.new('2.2.3') }
# Different minor versions
it { should > SemVer.new('1.1.3') }
it { should < SemVer.new('1.3.3') }
# Different tiny versions
it { should > SemVer.new('1.2.2') }
it { should < SemVer.new('1.2.4') }
# Against special versions
it { should > SemVer.new('1.2.3-beta') }
it { should < SemVer.new('1.2.4-beta') }
end
describe 'on a special version (v1.2.3-beta)' do
subject { SemVer.new('v1.2.3-beta') }
it { should == SemVer.new('1.2.3-beta') }
# Same version, final release
it { should < SemVer.new('1.2.3') }
# Different major versions
it { should > SemVer.new('0.2.3') }
it { should < SemVer.new('2.2.3') }
# Different minor versions
it { should > SemVer.new('1.1.3') }
it { should < SemVer.new('1.3.3') }
# Different tiny versions
it { should > SemVer.new('1.2.2') }
it { should < SemVer.new('1.2.4') }
# Against special versions
it { should > SemVer.new('1.2.3-alpha') }
it { should < SemVer.new('1.2.3-beta2') }
end
end
end
diff --git a/spec/unit/type/user_spec.rb b/spec/unit/type/user_spec.rb
index c559be3c7..bb235b798 100755
--- a/spec/unit/type/user_spec.rb
+++ b/spec/unit/type/user_spec.rb
@@ -1,335 +1,340 @@
#!/usr/bin/env rspec
require 'spec_helper'
-user = Puppet::Type.type(:user)
-
-describe user do
- before do
- @provider = stub 'provider'
- @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil
+describe Puppet::Type.type(:user) do
+ before :each do
+ @provider_class = described_class.provide(:simple) do
+ has_features :manages_expiry, :manages_password_age, :manages_passwords, :manages_solaris_rbac
+ mk_resource_methods
+ def create; end
+ def delete; end
+ def exists?; get(:ensure) != :absent; end
+ def flush; end
+ def self.instances; []; end
+ end
+ described_class.stubs(:defaultprovider).returns @provider_class
end
it "should be able to create a instance" do
- user.new(:name => "foo").should_not be_nil
+ described_class.new(:name => "foo").should_not be_nil
end
it "should have an allows_duplicates feature" do
- user.provider_feature(:allows_duplicates).should_not be_nil
+ described_class.provider_feature(:allows_duplicates).should_not be_nil
end
it "should have an manages_homedir feature" do
- user.provider_feature(:manages_homedir).should_not be_nil
+ described_class.provider_feature(:manages_homedir).should_not be_nil
end
it "should have an manages_passwords feature" do
- user.provider_feature(:manages_passwords).should_not be_nil
+ described_class.provider_feature(:manages_passwords).should_not be_nil
end
it "should have a manages_solaris_rbac feature" do
- user.provider_feature(:manages_solaris_rbac).should_not be_nil
+ described_class.provider_feature(:manages_solaris_rbac).should_not be_nil
end
it "should have a manages_expiry feature" do
- user.provider_feature(:manages_expiry).should_not be_nil
+ described_class.provider_feature(:manages_expiry).should_not be_nil
end
it "should have a manages_password_age feature" do
- user.provider_feature(:manages_password_age).should_not be_nil
+ described_class.provider_feature(:manages_password_age).should_not be_nil
end
it "should have a system_users feature" do
- user.provider_feature(:system_users).should_not be_nil
+ described_class.provider_feature(:system_users).should_not be_nil
end
describe "instances" do
it "should delegate existence questions to its provider" do
- instance = user.new(:name => "foo")
- instance.provider.expects(:exists?).returns "eh"
- instance.exists?.should == "eh"
+ @provider = @provider_class.new(:name => 'foo', :ensure => :absent)
+ instance = described_class.new(:name => "foo", :provider => @provider)
+ instance.exists?.should == false
+
+ @provider.set(:ensure => :present)
+ instance.exists?.should == true
end
end
properties = [:ensure, :uid, :gid, :home, :comment, :shell, :password, :password_min_age, :password_max_age, :groups, :roles, :auths, :profiles, :project, :keys, :expiry]
properties.each do |property|
it "should have a #{property} property" do
- user.attrclass(property).ancestors.should be_include(Puppet::Property)
+ described_class.attrclass(property).ancestors.should be_include(Puppet::Property)
end
it "should have documentation for its #{property} property" do
- user.attrclass(property).doc.should be_instance_of(String)
+ described_class.attrclass(property).doc.should be_instance_of(String)
end
end
list_properties = [:groups, :roles, :auths]
list_properties.each do |property|
it "should have a list '#{property}'" do
- user.attrclass(property).ancestors.should be_include(Puppet::Property::List)
+ described_class.attrclass(property).ancestors.should be_include(Puppet::Property::List)
end
end
it "should have an ordered list 'profiles'" do
- user.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList)
+ described_class.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList)
end
it "should have key values 'keys'" do
- user.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue)
+ described_class.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue)
end
describe "when retrieving all current values" do
before do
- @user = user.new(:name => "foo", :uid => 10)
+ @provider = @provider_class.new(:name => 'foo', :ensure => :present, :uid => 15, :gid => 15)
+ @user = described_class.new(:name => "foo", :uid => 10, :provider => @provider)
end
it "should return a hash containing values for all set properties" do
@user[:gid] = 10
- @user.property(:ensure).expects(:retrieve).returns :present
- @user.property(:uid).expects(:retrieve).returns 15
- @user.property(:gid).expects(:retrieve).returns 15
values = @user.retrieve
[@user.property(:uid), @user.property(:gid)].each { |property| values.should be_include(property) }
end
it "should set all values to :absent if the user is absent" do
@user.property(:ensure).expects(:retrieve).returns :absent
@user.property(:uid).expects(:retrieve).never
@user.retrieve[@user.property(:uid)].should == :absent
end
it "should include the result of retrieving each property's current value if the user is present" do
- @user.property(:ensure).expects(:retrieve).returns :present
- @user.property(:uid).expects(:retrieve).returns 15
@user.retrieve[@user.property(:uid)].should == 15
end
end
describe "when managing the ensure property" do
- before do
- @ensure = user.attrclass(:ensure).new(:resource => @resource)
- end
-
it "should support a :present value" do
- lambda { @ensure.should = :present }.should_not raise_error
+ lambda { described_class.new(:name => 'foo', :ensure => :present) }.should_not raise_error
end
it "should support an :absent value" do
- lambda { @ensure.should = :absent }.should_not raise_error
+ lambda { described_class.new(:name => 'foo', :ensure => :absent) }.should_not raise_error
end
it "should call :create on the provider when asked to sync to the :present state" do
+ @provider = @provider_class.new(:name => 'foo', :ensure => :absent)
@provider.expects(:create)
- @ensure.should = :present
- @ensure.sync
+ described_class.new(:name => 'foo', :ensure => :present, :provider => @provider).parameter(:ensure).sync
end
it "should call :delete on the provider when asked to sync to the :absent state" do
+ @provider = @provider_class.new(:name => 'foo', :ensure => :present)
@provider.expects(:delete)
- @ensure.should = :absent
- @ensure.sync
+ described_class.new(:name => 'foo', :ensure => :absent, :provider => @provider).parameter(:ensure).sync
end
describe "and determining the current state" do
it "should return :present when the provider indicates the user exists" do
- @provider.expects(:exists?).returns true
- @ensure.retrieve.should == :present
+ @provider = @provider_class.new(:name => 'foo', :ensure => :present)
+ described_class.new(:name => 'foo', :ensure => :absent, :provider => @provider).parameter(:ensure).retrieve.should == :present
end
it "should return :absent when the provider indicates the user does not exist" do
- @provider.expects(:exists?).returns false
- @ensure.retrieve.should == :absent
+ @provider = @provider_class.new(:name => 'foo', :ensure => :absent)
+ described_class.new(:name => 'foo', :ensure => :present, :provider => @provider).parameter(:ensure).retrieve.should == :absent
end
end
end
describe "when managing the uid property" do
it "should convert number-looking strings into actual numbers" do
- uid = user.attrclass(:uid).new(:resource => @resource)
- uid.should = "50"
- uid.should.must == 50
+ described_class.new(:name => 'foo', :uid => '50')[:uid].should == 50
end
it "should support UIDs as numbers" do
- uid = user.attrclass(:uid).new(:resource => @resource)
- uid.should = 50
- uid.should.must == 50
+ described_class.new(:name => 'foo', :uid => 50)[:uid].should == 50
end
- it "should :absent as a value" do
- uid = user.attrclass(:uid).new(:resource => @resource)
- uid.should = :absent
- uid.should.must == :absent
+ it "should support :absent as a value" do
+ described_class.new(:name => 'foo', :uid => :absent)[:uid].should == :absent
end
end
describe "when managing the gid" do
- it "should :absent as a value" do
- gid = user.attrclass(:gid).new(:resource => @resource)
- gid.should = :absent
- gid.should.must == :absent
+ it "should support :absent as a value" do
+ described_class.new(:name => 'foo', :gid => :absent)[:gid].should == :absent
end
it "should convert number-looking strings into actual numbers" do
- gid = user.attrclass(:gid).new(:resource => @resource)
- gid.should = "50"
- gid.should.must == 50
+ described_class.new(:name => 'foo', :gid => '50')[:gid].should == 50
end
it "should support GIDs specified as integers" do
- gid = user.attrclass(:gid).new(:resource => @resource)
- gid.should = 50
- gid.should.must == 50
+ described_class.new(:name => 'foo', :gid => 50)[:gid].should == 50
end
it "should support groups specified by name" do
- gid = user.attrclass(:gid).new(:resource => @resource)
- gid.should = "foo"
- gid.should.must == "foo"
+ described_class.new(:name => 'foo', :gid => 'foo')[:gid].should == 'foo'
end
describe "when testing whether in sync" do
- before do
- @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar})
- end
-
it "should return true if no 'should' values are set" do
- @gid = user.attrclass(:gid).new(:resource => @resource)
-
- @gid.must be_safe_insync(500)
+ # this is currently not the case because gid has no default value, so we would never even
+ # call insync? on that property
+ if param = described_class.new(:name => 'foo').parameter(:gid)
+ param.must be_safe_insync(500)
+ end
end
it "should return true if any of the specified groups are equal to the current integer" do
Puppet::Util.expects(:gid).with("foo").returns 300
Puppet::Util.expects(:gid).with("bar").returns 500
-
- @gid.must be_safe_insync(500)
+ described_class.new(:name => 'baz', :gid => [ 'foo', 'bar' ]).parameter(:gid).must be_safe_insync(500)
end
it "should return false if none of the specified groups are equal to the current integer" do
Puppet::Util.expects(:gid).with("foo").returns 300
Puppet::Util.expects(:gid).with("bar").returns 500
-
- @gid.should_not be_safe_insync(700)
+ described_class.new(:name => 'baz', :gid => [ 'foo', 'bar' ]).parameter(:gid).must_not be_safe_insync(700)
end
end
describe "when syncing" do
- before do
- @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar})
- end
-
it "should use the first found, specified group as the desired value and send it to the provider" do
Puppet::Util.expects(:gid).with("foo").returns nil
Puppet::Util.expects(:gid).with("bar").returns 500
- @provider.expects(:gid=).with 500
+ @provider = @provider_class.new(:name => 'foo')
+ resource = described_class.new(:name => 'foo', :provider => @provider, :gid => [ 'foo', 'bar' ])
- @gid.sync
+ @provider.expects(:gid=).with 500
+ resource.parameter(:gid).sync
end
end
end
- describe "when managing expiry" do
- before do
- @expiry = user.attrclass(:expiry).new(:resource => @resource)
+ describe "when managing groups" do
+ it "should support a singe group" do
+ lambda { described_class.new(:name => 'foo', :groups => 'bar') }.should_not raise_error
end
- it "should fail if given an invalid date" do
- lambda { @expiry.should = "200-20-20" }.should raise_error(Puppet::Error)
+ it "should support multiple groups as an array" do
+ lambda { described_class.new(:name => 'foo', :groups => [ 'bar' ]) }.should_not raise_error
+ lambda { described_class.new(:name => 'foo', :groups => [ 'bar', 'baz' ]) }.should_not raise_error
+ end
+
+ it "should not support a comma separated list" do
+ lambda { described_class.new(:name => 'foo', :groups => 'bar,baz') }.should raise_error(Puppet::Error, /Group names must be provided as an array/)
+ end
+
+ it "should not support an empty string" do
+ lambda { described_class.new(:name => 'foo', :groups => '') }.should raise_error(Puppet::Error, /Group names must not be empty/)
+ end
+
+ describe "when testing is in sync" do
+
+ before :each do
+ # the useradd provider uses a single string to represent groups and so does Puppet::Property::List when converting to should values
+ @provider = @provider_class.new(:name => 'foo', :groups => 'a,b,e,f')
+ end
+
+ it "should not care about order" do
+ @property = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ]).property(:groups)
+ @property.must be_safe_insync([ 'a', 'b', 'c' ])
+ @property.must be_safe_insync([ 'a', 'c', 'b' ])
+ @property.must be_safe_insync([ 'b', 'a', 'c' ])
+ @property.must be_safe_insync([ 'b', 'c', 'a' ])
+ @property.must be_safe_insync([ 'c', 'a', 'b' ])
+ @property.must be_safe_insync([ 'c', 'b', 'a' ])
+ end
+
+ it "should merge current value and desired value if membership minimal" do
+ @instance = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ], :provider => @provider)
+ @instance[:membership] = :minimum
+ @instance[:groups].should == 'a,b,c,e,f'
+ end
+
+ it "should not treat a subset of groups insync if membership inclusive" do
+ @instance = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ], :provider => @provider)
+ @instance[:membership] = :inclusive
+ @instance[:groups].should == 'a,b,c'
+ end
end
end
- describe "when managing minimum password age" do
- before do
- @age = user.attrclass(:password_min_age).new(:resource => @resource)
+
+ describe "when managing expiry" do
+ it "should fail if given an invalid date" do
+ lambda { described_class.new(:name => 'foo', :expiry => "200-20-20") }.should raise_error(Puppet::Error, /Expiry dates must be YYYY-MM-DD/)
end
+ end
+ describe "when managing minimum password age" do
it "should accept a negative minimum age" do
- expect { @age.should = -1 }.should_not raise_error
+ expect { described_class.new(:name => 'foo', :password_min_age => '-1') }.should_not raise_error
end
it "should fail with an empty minimum age" do
- expect { @age.should = '' }.should raise_error(Puppet::Error)
+ expect { described_class.new(:name => 'foo', :password_min_age => '') }.should raise_error(Puppet::Error, /minimum age must be provided as a number/)
end
end
describe "when managing maximum password age" do
- before do
- @age = user.attrclass(:password_max_age).new(:resource => @resource)
- end
-
it "should accept a negative maximum age" do
- expect { @age.should = -1 }.should_not raise_error
+ expect { described_class.new(:name => 'foo', :password_max_age => '-1') }.should_not raise_error
end
it "should fail with an empty maximum age" do
- expect { @age.should = '' }.should raise_error(Puppet::Error)
+ expect { described_class.new(:name => 'foo', :password_max_age => '') }.should raise_error(Puppet::Error, /maximum age must be provided as a number/)
end
end
describe "when managing passwords" do
before do
- @password = user.attrclass(:password).new(:resource => @resource, :should => "mypass")
+ @password = described_class.new(:name => 'foo', :password => 'mypass').parameter(:password)
end
it "should not include the password in the change log when adding the password" do
@password.change_to_s(:absent, "mypass").should_not be_include("mypass")
end
it "should not include the password in the change log when changing the password" do
@password.change_to_s("other", "mypass").should_not be_include("mypass")
end
it "should redact the password when displaying the old value" do
@password.is_to_s("currentpassword").should =~ /^\[old password hash redacted\]$/
end
it "should redact the password when displaying the new value" do
@password.should_to_s("newpassword").should =~ /^\[new password hash redacted\]$/
end
it "should fail if a ':' is included in the password" do
- lambda { @password.should = "some:thing" }.should raise_error(Puppet::Error)
+ lambda { described_class.new(:name => 'foo', :password => "some:thing") }.should raise_error(Puppet::Error, /Passwords cannot include ':'/)
end
it "should allow the value to be set to :absent" do
- lambda { @password.should = :absent }.should_not raise_error
+ lambda { described_class.new(:name => 'foo', :password => :absent) }.should_not raise_error
end
end
describe "when manages_solaris_rbac is enabled" do
- before do
- @provider.stubs(:satisfies?).returns(false)
- @provider.expects(:satisfies?).with([:manages_solaris_rbac]).returns(true)
- end
-
it "should support a :role value for ensure" do
- @ensure = user.attrclass(:ensure).new(:resource => @resource)
- lambda { @ensure.should = :role }.should_not raise_error
+ lambda { described_class.new(:name => 'foo', :ensure => :role) }.should_not raise_error
end
end
describe "when user has roles" do
- before do
- # To test this feature, we have to support it.
- user.new(:name => "foo").provider.class.stubs(:feature?).returns(true)
- end
-
it "should autorequire roles" do
- testuser = Puppet::Type.type(:user).new(:name => "testuser")
- testuser.provider.stubs(:send).with(:roles).returns("")
- testuser[:roles] = "testrole"
-
- testrole = Puppet::Type.type(:user).new(:name => "testrole")
+ testuser = described_class.new(:name => "testuser", :roles => ['testrole'] )
+ testrole = described_class.new(:name => "testrole")
config = Puppet::Resource::Catalog.new :testing do |conf|
[testuser, testrole].each { |resource| conf.add_resource resource }
end
Puppet::Type::User::ProviderDirectoryservice.stubs(:get_macosx_version_major).returns "10.5"
rel = testuser.autorequire[0]
rel.source.ref.should == testrole.ref
rel.target.ref.should == testuser.ref
end
end
end
diff --git a/spec/unit/util/terminal_spec.rb b/spec/unit/util/terminal_spec.rb
new file mode 100644
index 000000000..d70bb94a9
--- /dev/null
+++ b/spec/unit/util/terminal_spec.rb
@@ -0,0 +1,42 @@
+#!/usr/bin/env rspec
+require 'spec_helper'
+require 'puppet/util/terminal'
+
+describe Puppet::Util::Terminal do
+ describe '.width' do
+ before { Puppet.features.stubs(:posix?).returns(true) }
+
+ it 'should invoke `stty` and return the width' do
+ height, width = 100, 200
+ subject.expects(:`).with('stty size 2>/dev/null').returns("#{height} #{width}\n")
+ subject.width.should == width
+ end
+
+ it 'should use `tput` if `stty` is unavailable' do
+ width = 200
+ subject.expects(:`).with('stty size 2>/dev/null').returns("\n")
+ subject.expects(:`).with('tput cols 2>/dev/null').returns("#{width}\n")
+ subject.width.should == width
+ end
+
+ it 'should default to 80 columns if `tput` and `stty` are unavailable' do
+ width = 80
+ subject.expects(:`).with('stty size 2>/dev/null').returns("\n")
+ subject.expects(:`).with('tput cols 2>/dev/null').returns("\n")
+ subject.width.should == width
+ end
+
+ it 'should default to 80 columns if `tput` or `stty` raise exceptions' do
+ width = 80
+ subject.expects(:`).with('stty size 2>/dev/null').raises()
+ subject.stubs(:`).with('tput cols 2>/dev/null').returns("#{width + 1000}\n")
+ subject.width.should == width
+ end
+
+ it 'should default to 80 columns if not in a POSIX environment' do
+ width = 80
+ Puppet.features.stubs(:posix?).returns(false)
+ subject.width.should == width
+ end
+ end
+end
diff --git a/spec/unit/util_spec.rb b/spec/unit/util_spec.rb
index 704e2851c..40df8c3bf 100755
--- a/spec/unit/util_spec.rb
+++ b/spec/unit/util_spec.rb
@@ -1,435 +1,455 @@
#!/usr/bin/env ruby
require 'spec_helper'
describe Puppet::Util do
include PuppetSpec::Files
if Puppet.features.microsoft_windows?
def set_mode(mode, file)
Puppet::Util::Windows::Security.set_mode(mode, file)
end
def get_mode(file)
Puppet::Util::Windows::Security.get_mode(file) & 07777
end
else
def set_mode(mode, file)
File.chmod(mode, file)
end
def get_mode(file)
File.lstat(file).mode & 07777
end
end
describe "#withenv" do
before :each do
@original_path = ENV["PATH"]
@new_env = {:PATH => "/some/bogus/path"}
end
it "should change environment variables within the block then reset environment variables to their original values" do
Puppet::Util.withenv @new_env do
ENV["PATH"].should == "/some/bogus/path"
end
ENV["PATH"].should == @original_path
end
it "should reset environment variables to their original values even if the block fails" do
begin
Puppet::Util.withenv @new_env do
ENV["PATH"].should == "/some/bogus/path"
raise "This is a failure"
end
rescue
end
ENV["PATH"].should == @original_path
end
it "should reset environment variables even when they are set twice" do
# Setting Path & Environment parameters in Exec type can cause weirdness
@new_env["PATH"] = "/someother/bogus/path"
Puppet::Util.withenv @new_env do
# When assigning duplicate keys, can't guarantee order of evaluation
ENV["PATH"].should =~ /\/some.*\/bogus\/path/
end
ENV["PATH"].should == @original_path
end
it "should remove any new environment variables after the block ends" do
@new_env[:FOO] = "bar"
Puppet::Util.withenv @new_env do
ENV["FOO"].should == "bar"
end
ENV["FOO"].should == nil
end
end
describe "#absolute_path?" do
it "should default to the platform of the local system" do
Puppet.features.stubs(:posix?).returns(true)
Puppet.features.stubs(:microsoft_windows?).returns(false)
Puppet::Util.should be_absolute_path('/foo')
Puppet::Util.should_not be_absolute_path('C:/foo')
Puppet.features.stubs(:posix?).returns(false)
Puppet.features.stubs(:microsoft_windows?).returns(true)
Puppet::Util.should be_absolute_path('C:/foo')
Puppet::Util.should_not be_absolute_path('/foo')
end
describe "when using platform :posix" do
%w[/ /foo /foo/../bar //foo //Server/Foo/Bar //?/C:/foo/bar /\Server/Foo /foo//bar/baz].each do |path|
it "should return true for #{path}" do
Puppet::Util.should be_absolute_path(path, :posix)
end
end
%w[. ./foo \foo C:/foo \\Server\Foo\Bar \\?\C:\foo\bar \/?/foo\bar \/Server/foo foo//bar/baz].each do |path|
it "should return false for #{path}" do
Puppet::Util.should_not be_absolute_path(path, :posix)
end
end
end
describe "when using platform :windows" do
%w[C:/foo C:\foo \\\\Server\Foo\Bar \\\\?\C:\foo\bar //Server/Foo/Bar //?/C:/foo/bar /\?\C:/foo\bar \/Server\Foo/Bar c:/foo//bar//baz].each do |path|
it "should return true for #{path}" do
Puppet::Util.should be_absolute_path(path, :windows)
end
end
%w[/ . ./foo \foo /foo /foo/../bar //foo C:foo/bar foo//bar/baz].each do |path|
it "should return false for #{path}" do
Puppet::Util.should_not be_absolute_path(path, :windows)
end
end
end
end
describe "#path_to_uri" do
%w[. .. foo foo/bar foo/../bar].each do |path|
it "should reject relative path: #{path}" do
lambda { Puppet::Util.path_to_uri(path) }.should raise_error(Puppet::Error)
end
end
it "should perform URI escaping" do
Puppet::Util.path_to_uri("/foo bar").path.should == "/foo%20bar"
end
describe "when using platform :posix" do
before :each do
Puppet.features.stubs(:posix).returns true
Puppet.features.stubs(:microsoft_windows?).returns false
end
%w[/ /foo /foo/../bar].each do |path|
it "should convert #{path} to URI" do
Puppet::Util.path_to_uri(path).path.should == path
end
end
end
describe "when using platform :windows" do
before :each do
Puppet.features.stubs(:posix).returns false
Puppet.features.stubs(:microsoft_windows?).returns true
end
it "should normalize backslashes" do
Puppet::Util.path_to_uri('c:\\foo\\bar\\baz').path.should == '/' + 'c:/foo/bar/baz'
end
%w[C:/ C:/foo/bar].each do |path|
it "should convert #{path} to absolute URI" do
Puppet::Util.path_to_uri(path).path.should == '/' + path
end
end
%w[share C$].each do |path|
it "should convert UNC #{path} to absolute URI" do
uri = Puppet::Util.path_to_uri("\\\\server\\#{path}")
uri.host.should == 'server'
uri.path.should == '/' + path
end
end
end
end
describe ".uri_to_path" do
require 'uri'
it "should strip host component" do
Puppet::Util.uri_to_path(URI.parse('http://foo/bar')).should == '/bar'
end
it "should accept puppet URLs" do
Puppet::Util.uri_to_path(URI.parse('puppet:///modules/foo')).should == '/modules/foo'
end
it "should return unencoded path" do
Puppet::Util.uri_to_path(URI.parse('http://foo/bar%20baz')).should == '/bar baz'
end
it "should be nil-safe" do
Puppet::Util.uri_to_path(nil).should be_nil
end
describe "when using platform :posix",:if => Puppet.features.posix? do
it "should accept root" do
Puppet::Util.uri_to_path(URI.parse('file:/')).should == '/'
end
it "should accept single slash" do
Puppet::Util.uri_to_path(URI.parse('file:/foo/bar')).should == '/foo/bar'
end
it "should accept triple slashes" do
Puppet::Util.uri_to_path(URI.parse('file:///foo/bar')).should == '/foo/bar'
end
end
describe "when using platform :windows", :if => Puppet.features.microsoft_windows? do
it "should accept root" do
Puppet::Util.uri_to_path(URI.parse('file:/C:/')).should == 'C:/'
end
it "should accept single slash" do
Puppet::Util.uri_to_path(URI.parse('file:/C:/foo/bar')).should == 'C:/foo/bar'
end
it "should accept triple slashes" do
Puppet::Util.uri_to_path(URI.parse('file:///C:/foo/bar')).should == 'C:/foo/bar'
end
it "should accept file scheme with double slashes as a UNC path" do
Puppet::Util.uri_to_path(URI.parse('file://host/share/file')).should == '//host/share/file'
end
end
end
describe "#which" do
let(:base) { File.expand_path('/bin') }
let(:path) { File.join(base, 'foo') }
before :each do
FileTest.stubs(:file?).returns false
FileTest.stubs(:file?).with(path).returns true
FileTest.stubs(:executable?).returns false
FileTest.stubs(:executable?).with(path).returns true
end
it "should accept absolute paths" do
Puppet::Util.which(path).should == path
end
it "should return nil if no executable found" do
Puppet::Util.which('doesnotexist').should be_nil
end
it "should warn if the user's HOME is not set but their PATH contains a ~" do
env_path = %w[~/bin /usr/bin /bin].join(File::PATH_SEPARATOR)
Puppet::Util.withenv({:HOME => nil, :PATH => env_path}) do
Puppet::Util::Warnings.expects(:warnonce).once
Puppet::Util.which('foo')
end
end
it "should reject directories" do
Puppet::Util.which(base).should be_nil
end
describe "on POSIX systems" do
before :each do
Puppet.features.stubs(:posix?).returns true
Puppet.features.stubs(:microsoft_windows?).returns false
end
it "should walk the search PATH returning the first executable" do
ENV.stubs(:[]).with('PATH').returns(File.expand_path('/bin'))
Puppet::Util.which('foo').should == path
end
end
describe "on Windows systems" do
let(:path) { File.expand_path(File.join(base, 'foo.CMD')) }
before :each do
Puppet.features.stubs(:posix?).returns false
Puppet.features.stubs(:microsoft_windows?).returns true
end
describe "when a file extension is specified" do
it "should walk each directory in PATH ignoring PATHEXT" do
ENV.stubs(:[]).with('PATH').returns(%w[/bar /bin].map{|dir| File.expand_path(dir)}.join(File::PATH_SEPARATOR))
FileTest.expects(:file?).with(File.join(File.expand_path('/bar'), 'foo.CMD')).returns false
ENV.expects(:[]).with('PATHEXT').never
Puppet::Util.which('foo.CMD').should == path
end
end
describe "when a file extension is not specified" do
it "should walk each extension in PATHEXT until an executable is found" do
bar = File.expand_path('/bar')
ENV.stubs(:[]).with('PATH').returns("#{bar}#{File::PATH_SEPARATOR}#{base}")
ENV.stubs(:[]).with('PATHEXT').returns(".EXE#{File::PATH_SEPARATOR}.CMD")
exts = sequence('extensions')
FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.EXE')).returns false
FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.CMD')).returns false
FileTest.expects(:file?).in_sequence(exts).with(File.join(base, 'foo.EXE')).returns false
FileTest.expects(:file?).in_sequence(exts).with(path).returns true
Puppet::Util.which('foo').should == path
end
it "should walk the default extension path if the environment variable is not defined" do
ENV.stubs(:[]).with('PATH').returns(base)
ENV.stubs(:[]).with('PATHEXT').returns(nil)
exts = sequence('extensions')
%w[.COM .EXE .BAT].each do |ext|
FileTest.expects(:file?).in_sequence(exts).with(File.join(base, "foo#{ext}")).returns false
end
FileTest.expects(:file?).in_sequence(exts).with(path).returns true
Puppet::Util.which('foo').should == path
end
it "should fall back if no extension matches" do
ENV.stubs(:[]).with('PATH').returns(base)
ENV.stubs(:[]).with('PATHEXT').returns(".EXE")
FileTest.stubs(:file?).with(File.join(base, 'foo.EXE')).returns false
FileTest.stubs(:file?).with(File.join(base, 'foo')).returns true
FileTest.stubs(:executable?).with(File.join(base, 'foo')).returns true
Puppet::Util.which('foo').should == File.join(base, 'foo')
end
end
end
end
describe "#binread" do
let(:contents) { "foo\r\nbar" }
it "should preserve line endings" do
path = tmpfile('util_binread')
File.open(path, 'wb') { |f| f.print contents }
Puppet::Util.binread(path).should == contents
end
it "should raise an error if the file doesn't exist" do
expect { Puppet::Util.binread('/path/does/not/exist') }.to raise_error(Errno::ENOENT)
end
end
+ describe "hash symbolizing functions" do
+ let (:myhash) { { "foo" => "bar", :baz => "bam" } }
+ let (:resulthash) { { :foo => "bar", :baz => "bam" } }
+
+ describe "#symbolizehash" do
+ it "should return a symbolized hash" do
+ newhash = Puppet::Util.symbolizehash(myhash)
+ newhash.should == resulthash
+ end
+ end
+
+ describe "#symbolizehash!" do
+ it "should symbolize the hash in place" do
+ localhash = myhash
+ Puppet::Util.symbolizehash!(localhash)
+ localhash.should == resulthash
+ end
+ end
+ end
+
context "#replace_file" do
subject { Puppet::Util }
it { should respond_to :replace_file }
let :target do
target = Tempfile.new("puppet-util-replace-file")
target.puts("hello, world")
target.flush # make sure content is on disk.
target.fsync rescue nil
target.close
target
end
it "should fail if no block is given" do
expect { subject.replace_file(target.path, 0600) }.to raise_error /block/
end
it "should replace a file when invoked" do
# Check that our file has the expected content.
File.read(target.path).should == "hello, world\n"
# Replace the file.
subject.replace_file(target.path, 0600) do |fh|
fh.puts "I am the passenger..."
end
# ...and check the replacement was complete.
File.read(target.path).should == "I am the passenger...\n"
end
[0555, 0600, 0660, 0700, 0770].each do |mode|
it "should copy 0#{mode.to_s(8)} permissions from the target file by default" do
set_mode(mode, target.path)
get_mode(target.path).should == mode
subject.replace_file(target.path, 0000) {|fh| fh.puts "bazam" }
get_mode(target.path).should == mode
File.read(target.path).should == "bazam\n"
end
end
it "should copy the permissions of the source file before yielding" do
set_mode(0555, target.path)
inode = File.stat(target.path).ino unless Puppet.features.microsoft_windows?
yielded = false
subject.replace_file(target.path, 0600) do |fh|
get_mode(fh.path).should == 0555
yielded = true
end
yielded.should be_true
# We can't check inode on Windows
File.stat(target.path).ino.should_not == inode unless Puppet.features.microsoft_windows?
get_mode(target.path).should == 0555
end
it "should use the default permissions if the source file doesn't exist" do
new_target = target.path + '.foo'
File.should_not be_exist(new_target)
begin
subject.replace_file(new_target, 0555) {|fh| fh.puts "foo" }
get_mode(new_target).should == 0555
ensure
File.unlink(new_target) if File.exists?(new_target)
end
end
it "should not replace the file if an exception is thrown in the block" do
yielded = false
threw = false
begin
subject.replace_file(target.path, 0600) do |fh|
yielded = true
fh.puts "different content written, then..."
raise "...throw some random failure"
end
rescue Exception => e
if e.to_s =~ /some random failure/
threw = true
else
raise
end
end
yielded.should be_true
threw.should be_true
# ...and check the replacement was complete.
File.read(target.path).should == "hello, world\n"
end
end
end