From c1f3d114f52654e9f285499dd548409edc0c99fe Mon Sep 17 00:00:00 2001 From: Gilles Cornu Date: Tue, 17 Nov 2015 22:06:06 +0100 Subject: [PATCH] provisioners/ansible(both): add galaxy support Close #2718 --- plugins/provisioners/ansible/config/base.rb | 13 ++++ plugins/provisioners/ansible/errors.rb | 2 +- .../provisioners/ansible/provisioner/base.rb | 20 ++++++ .../provisioners/ansible/provisioner/guest.rb | 29 +++++--- .../provisioners/ansible/provisioner/host.rb | 67 +++++++++++++------ templates/locales/en.yml | 3 + .../provisioners/ansible/config_test.rb | 13 ++++ .../provisioners/ansible/provisioner_test.rb | 29 +++++++- .../v2/provisioning/ansible_common.html.md | 24 +++++++ 9 files changed, 167 insertions(+), 33 deletions(-) diff --git a/plugins/provisioners/ansible/config/base.rb b/plugins/provisioners/ansible/config/base.rb index 66c765be7..04c703439 100644 --- a/plugins/provisioners/ansible/config/base.rb +++ b/plugins/provisioners/ansible/config/base.rb @@ -4,6 +4,9 @@ module VagrantPlugins class Base < Vagrant.plugin("2", :config) attr_accessor :extra_vars + attr_accessor :galaxy_role_file + attr_accessor :galaxy_roles_path + attr_accessor :galaxy_command attr_accessor :groups attr_accessor :inventory_path attr_accessor :limit @@ -19,6 +22,9 @@ module VagrantPlugins def initialize @extra_vars = UNSET_VALUE + @galaxy_role_file = UNSET_VALUE + @galaxy_roles_path = UNSET_VALUE + @galaxy_command = UNSET_VALUE @groups = UNSET_VALUE @inventory_path = UNSET_VALUE @limit = UNSET_VALUE @@ -35,6 +41,9 @@ module VagrantPlugins def finalize! @extra_vars = nil if @extra_vars == UNSET_VALUE + @galaxy_role_file = nil if @galaxy_role_file == UNSET_VALUE + @galaxy_roles_path = nil if @galaxy_roles_path == UNSET_VALUE + @galaxy_command = "ansible-galaxy install --role-file=%{ROLE_FILE} --roles-path=%{ROLES_PATH} --force" if @galaxy_command == UNSET_VALUE @groups = {} if @groups == UNSET_VALUE @inventory_path = nil if @inventory_path == UNSET_VALUE @limit = nil if @limit == UNSET_VALUE @@ -68,6 +77,10 @@ module VagrantPlugins check_path_exists(machine, inventory_path, "vagrant.provisioners.ansible.errors.inventory_path_invalid") end + if galaxy_role_file + check_path_is_a_file(machine, galaxy_role_file, "vagrant.provisioners.ansible.errors.galaxy_role_file_invalid") + end + if vault_password_file check_path_is_a_file(machine, vault_password_file, "vagrant.provisioners.ansible.errors.vault_password_file_invalid") end diff --git a/plugins/provisioners/ansible/errors.rb b/plugins/provisioners/ansible/errors.rb index aa154376e..06a1063cf 100644 --- a/plugins/provisioners/ansible/errors.rb +++ b/plugins/provisioners/ansible/errors.rb @@ -7,7 +7,7 @@ module VagrantPlugins error_namespace("vagrant.provisioners.ansible.errors") end - class AnsiblePlaybookAppFailed < AnsibleError + class AnsibleCommandFailed < AnsibleError error_key(:ansible_command_failed) end diff --git a/plugins/provisioners/ansible/provisioner/base.rb b/plugins/provisioners/ansible/provisioner/base.rb index 051473271..16f6d0d77 100644 --- a/plugins/provisioners/ansible/provisioner/base.rb +++ b/plugins/provisioners/ansible/provisioner/base.rb @@ -132,6 +132,26 @@ module VagrantPlugins end end + def get_galaxy_role_file(basedir) + Pathname.new(config.galaxy_role_file).expand_path(basedir) + end + + def get_galaxy_roles_path(basedir) + if config.galaxy_roles_path + Pathname.new(config.galaxy_roles_path).expand_path(basedir) + else + File.join(Pathname.new(config.playbook).expand_path(basedir).parent, 'roles') + end + end + + def ui_running_ansible_command(name, command) + @machine.ui.detail I18n.t("vagrant.provisioners.ansible.running_#{name}") + if verbosity_is_enabled? + # Show the ansible command in use + @machine.env.ui.detail(command) + end + end + def verbosity_is_enabled? config.verbose && !config.verbose.to_s.empty? end diff --git a/plugins/provisioners/ansible/provisioner/guest.rb b/plugins/provisioners/ansible/provisioner/guest.rb index e2b6cb8dd..9e4346190 100644 --- a/plugins/provisioners/ansible/provisioner/guest.rb +++ b/plugins/provisioners/ansible/provisioner/guest.rb @@ -14,8 +14,7 @@ module VagrantPlugins def provision check_and_install_ansible - prepare_common_command_arguments - prepare_common_environment_variables + execute_ansible_galaxy_on_guest if config.galaxy_role_file execute_ansible_playbook_on_guest end @@ -51,9 +50,9 @@ module VagrantPlugins @machine.guest.capability(:ansible_install) end - # Check for the existence of ansible-playbook binary on the guest, + # Check that ansible binaries are well installed on the guest, @machine.communicate.execute( - "ansible-playbook --help", + "ansible-galaxy --help && ansible-playbook --help", :error_class => Ansible::Errors::AnsibleNotFoundOnGuest, :error_key => :ansible_not_found_on_guest) @@ -65,15 +64,27 @@ module VagrantPlugins end end + def execute_ansible_galaxy_on_guest + command_values = { + :ROLE_FILE => get_galaxy_role_file(config.provisioning_path), + :ROLES_PATH => get_galaxy_roles_path(config.provisioning_path) + } + remote_command = config.galaxy_command % command_values + + ui_running_ansible_command "galaxy", remote_command + + result = execute_on_guest(remote_command) + raise Ansible::Errors::AnsibleGalaxyAppFailed if result != 0 + end + def execute_ansible_playbook_on_guest + prepare_common_command_arguments + prepare_common_environment_variables + command = (%w(ansible-playbook) << @command_arguments << config.playbook).flatten remote_command = "cd #{config.provisioning_path} && #{Helpers::stringify_ansible_playbook_command(@environment_variables, command)}" - # TODO: generic HOOK ??? - # Show the ansible command in use - if verbosity_is_enabled? - @machine.env.ui.detail(remote_command) - end + ui_running_ansible_command "playbook", remote_command result = execute_on_guest(remote_command) raise Ansible::Errors::AnsiblePlaybookAppFailed if result != 0 diff --git a/plugins/provisioners/ansible/provisioner/host.rb b/plugins/provisioners/ansible/provisioner/host.rb index 4b3e8881c..61e576c97 100644 --- a/plugins/provisioners/ansible/provisioner/host.rb +++ b/plugins/provisioners/ansible/provisioner/host.rb @@ -19,8 +19,7 @@ module VagrantPlugins @ssh_info = @machine.ssh_info warn_for_unsupported_platform - prepare_command_arguments - prepare_environment_variables + execute_ansible_galaxy_from_host if config.galaxy_role_file execute_ansible_playbook_from_host end @@ -72,33 +71,57 @@ module VagrantPlugins @environment_variables["ANSIBLE_SSH_ARGS"] = ansible_ssh_args unless ansible_ssh_args.empty? end - def execute_ansible_playbook_from_host - # Assemble the full ansible-playbook command - command = (%w(ansible-playbook) << @command_arguments << config.playbook).flatten - - # TODO: generic HOOK ??? - # Show the ansible command in use - if verbosity_is_enabled? - @machine.env.ui.detail(Helpers::stringify_ansible_playbook_command(@environment_variables, command)) + def execute_command_from_host(command) + begin + result = Vagrant::Util::Subprocess.execute(*command) do |type, data| + if type == :stdout || type == :stderr + @machine.env.ui.detail(data, new_line: false, prefix: false) + end + end + raise Ansible::Errors::AnsibleCommandFailed if result.exit_code != 0 + rescue Vagrant::Errors::CommandUnavailable + raise Ansible::Errors::AnsibleNotFoundOnHost end + end - # Write stdout and stderr data, since it's the regular Ansible output + def execute_ansible_galaxy_from_host + command_values = { + :ROLE_FILE => get_galaxy_role_file(machine.env.root_path), + :ROLES_PATH => get_galaxy_roles_path(machine.env.root_path) + } + arg_separator = '__VAGRANT_ARG_SEPARATOR__' + command_template = config.galaxy_command.gsub(' ', arg_separator) + str_command = command_template % command_values + + ui_running_ansible_command "galaxy", str_command.gsub(arg_separator, ' ') + + command = str_command.split(arg_separator) command << { - env: @environment_variables, + # Write stdout and stderr data, since it's the regular Ansible output notify: [:stdout, :stderr], workdir: @machine.env.root_path.to_s } - begin - result = Vagrant::Util::Subprocess.execute(*command) do |type, data| - if type == :stdout || type == :stderr - @machine.env.ui.info(data, new_line: false, prefix: false) - end - end - raise Ansible::Errors::AnsiblePlaybookAppFailed if result.exit_code != 0 - rescue Vagrant::Errors::CommandUnavailable - raise Ansible::Errors::AnsibleNotFoundOnHost - end + execute_command_from_host command + end + + def execute_ansible_playbook_from_host + prepare_command_arguments + prepare_environment_variables + + # Assemble the full ansible-playbook command + command = (%w(ansible-playbook) << @command_arguments << config.playbook).flatten + + ui_running_ansible_command "playbook", Helpers::stringify_ansible_playbook_command(@environment_variables, command) + + command << { + env: @environment_variables, + # Write stdout and stderr data, since it's the regular Ansible output + notify: [:stdout, :stderr], + workdir: @machine.env.root_path.to_s + } + + execute_command_from_host command end def ship_generated_inventory(inventory_content) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 886aec3c5..6bcb24689 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -2072,6 +2072,8 @@ en: for more information. extra_vars_invalid: |- `extra_vars` must be a hash or a path to an existing file. Received: %{value} (as %{type}) + galaxy_role_file_invalid: |- + `galaxy_role_file` does not exist on the %{system}: %{path} inventory_path_invalid: |- `inventory_path` does not exist on the %{system}: %{path} no_playbook: |- @@ -2081,6 +2083,7 @@ en: vault_password_file_invalid: |- `vault_password_file` does not exist on the %{system}: %{path} installing: "Installing Ansible..." + running_galaxy: "Running ansible-galaxy..." running_playbook: "Running ansible-playbook..." windows_not_supported_for_control_machine: |- Windows is not officially supported for the Ansible Control Machine. diff --git a/test/unit/plugins/provisioners/ansible/config_test.rb b/test/unit/plugins/provisioners/ansible/config_test.rb index 6cbd3f30d..0de4ac69e 100644 --- a/test/unit/plugins/provisioners/ansible/config_test.rb +++ b/test/unit/plugins/provisioners/ansible/config_test.rb @@ -20,6 +20,9 @@ describe VagrantPlugins::Ansible::Config::Host do ask_vault_pass extra_vars force_remote_user + galaxy_command + galaxy_role_file + galaxy_roles_path groups host_key_checking inventory_path @@ -176,6 +179,16 @@ describe VagrantPlugins::Ansible::Config::Host do expect(result["ansible remote provisioner"]).to eql([ I18n.t("vagrant.provisioners.ansible.errors.vault_password_file_invalid", path: non_existing_file, system: "host") + ]) + end + + it "returns an error if galaxy_role_file is specified, but does not exist" do + subject.galaxy_role_file = non_existing_file + subject.finalize! + + result = subject.validate(machine) + expect(result["ansible remote provisioner"]).to eql([ + I18n.t("vagrant.provisioners.ansible.errors.galaxy_role_file_invalid", path: non_existing_file, system: "host") ]) end diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index fcc99cccb..94626389a 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -208,7 +208,7 @@ VF config.finalize! Vagrant::Util::Subprocess.stub(execute: Vagrant::Util::Subprocess::Result.new(1, "", "")) - expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsiblePlaybookAppFailed) + expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCommandFailed) end end @@ -583,6 +583,33 @@ VF end end + describe "with galaxy support" do + + before do + config.galaxy_role_file = existing_file + end + + it "raises an error when ansible-galaxy command fails", skip_before: true, skip_after: true do + config.finalize! + Vagrant::Util::Subprocess.stub(execute: Vagrant::Util::Subprocess::Result.new(1, "", "")) + + expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCommandFailed) + end + + it "execute ansible-galaxy and ansible-playbook" do + # TODO: to be improved, but I'm currenty facing some issues, maybe only present in RSpec 2.14... + expect(Vagrant::Util::Subprocess).to receive(:execute).twice + end + + describe "with verbose option enabled" do + before do + config.verbose = true + end + + xit "shows the ansible-galaxy command in use" + end + end + # The Vagrant Ansible provisioner does not validate the coherency of # argument combinations, and let ansible-playbook complain. describe "with a maximum of options" do diff --git a/website/docs/source/v2/provisioning/ansible_common.html.md b/website/docs/source/v2/provisioning/ansible_common.html.md index 3b6d7fbc1..aca7c2c14 100644 --- a/website/docs/source/v2/provisioning/ansible_common.html.md +++ b/website/docs/source/v2/provisioning/ansible_common.html.md @@ -51,6 +51,30 @@ Some of these options are for advanced usage only and should not be used unless By default, this option is disabled and Vagrant generates an inventory based on the `Vagrantfile` information. +- `galaxy_command` (template string) - The command pattern used to install Galaxy roles when `galaxy_role_file` is set. + + The following placeholders can be used in this command pattern: + - `%{ROLE_FILE}` is replaced by the absolute path to the `galaxy_role_file` option + - `%{ROLES_PATH}` is + * replaced by the absolute path to the `galaxy_roles_path` option when such option is defined + * replaced by the absolute path to a `roles` subdirectory sitting in the parent directory of the configured `playbook` file otherwise. + + By default, this option is set to + + ``` + ansible-galaxy install --role-file=%{ROLE_FILE} --roles-path=%{ROLES_PATH} --force + ``` + +- `galaxy_role_file` (string) - The path to the Ansible Galaxy role file. + + By default, this option is set to `nil` and Galaxy support is then disabled. + + Note: if an absolute path is given, the `ansible_local` provisioner will assume that it corresponds to the exact location on the guest system. + +- `galaxy_roles_path` (string) - The path to the directory where Ansible Galaxy roles must be installed + + By default, this option is set to `nil`, which means that the Galaxy roles will be installed in a `roles` subdirectory located in the parent directory of the `playbook` file. + - `limit` (string or array of strings) - Set of machines or groups from the inventory file to further control which hosts [are affected](http://docs.ansible.com/glossary.html#limit-groups). The default value is set to the machine name (taken from `Vagrantfile`) to ensure that `vagrant provision` command only affect the expected machine.