diff --git a/CHANGELOG.md b/CHANGELOG.md index 346e2a131..66d0c00d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,54 @@ FEATURES: IMPROVEMENTS: -- commands/ssh-config: Properly display windows path if invoked from msys2 or cygwin [GH-8915] -- providers/salt: Remove duplicate stdout, stderr output from salt [GH-8767] +BUG FIXES: + +## 2.0.0 (September 7, 2017) + +IMPROVEMENTS: + + - commands/login: Add support for two-factor authentication [GH-8935] + - commands/ssh-config: Properly display windows path if invoked from msys2 or + cygwin [GH-8915] + - guests/alt: Add support for ALT Linux [GH-8746] + - guests/kali: Fix file permissions on guest plugin ruby files [GH-8950] + - hosts/linux: Provide common systemd detection for services interaction, fix NFS + host interactions [GH-8938] + - providers/salt: Remove duplicate stdout, stderr output from salt [GH-8767] + - providers/salt: Introduce salt_call_args and salt_args option for salt provisioner + [GH-8927] + - providers/virtualbox: Improving resilience of some VirtualBox commands [GH-8951] + - provisioners/ansible(both): Add the compatibility_mode option, with auto-detection + enabled by default [GH-8913, GH-6570] + - provisioners/ansible: Add the version option to the host-based provisioner + [GH-8913, GH-8914] + - provisioners/ansible(both): Add the become and become_user options with deprecation + of sudo and sudo_user options [GH-8913, GH-6570] + - provisioners/ansible: Add the ask_become_pass option with deprecation of the + ask_sudo_pass option [GH-8913, GH-6570] BUG FIXES: -- guests/shell_expand_guest_path : Properly expand guest paths that include relative path alias [GH-8918] + - guests/shell_expand_guest_path : Properly expand guest paths that include relative + path alias [GH-8918] + - hosts/linux: Remove duplicate export folders before writing /etc/exports [GH-8945] + - provisioners/ansible(both): Add single quotes to the inventory host variables, only + when necessary [GH-8597] + - provisioners/ansible(both): Add the "all:vars" section to the inventory when defined + in `groups` option [GH-7730] + - provisioners/ansible_local: Extra variables are no longer truncated when a dollar ($) + character is present [GH-7735] + - provisioners/file: Align file provisioner functionality on all platforms [GH-8939] + - util/ssh: Properly quote key path for IdentityFile option to allow for spaces [GH-8924] + +BREAKING CHANGES: + + - Both Ansible provisioners are now capable of automatically setting the compatibility_mode that + best fits with the Ansible version in use. You may encounter some compatibility issues when + upgrading. If you were using Ansible 2.x and referring to the _ssh-prefixed variables present + in the generated inventory (e.g. `ansible_ssh_host`). In this case, you can fix your Vagrant + setup by setting compatibility_mode = "1.8", or by migrating to the new variable names (e.g. + ansible_host). ## 1.9.8 (August 23, 2017) diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index ab05f27ab..ee0d8ea25 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -468,6 +468,10 @@ module Vagrant error_key(:nfs_bad_exports) end + class NFSDupePerms < VagrantError + error_key(:nfs_dupe_permissions) + end + class NFSExportsFailed < VagrantError error_key(:nfs_exports_failed) end diff --git a/lib/vagrant/util/guest_inspection.rb b/lib/vagrant/util/guest_inspection.rb index fdfc1a632..033bb3e97 100644 --- a/lib/vagrant/util/guest_inspection.rb +++ b/lib/vagrant/util/guest_inspection.rb @@ -8,7 +8,7 @@ module Vagrant ## systemd helpers - # systemd is in used + # systemd is in use # # @return [Boolean] def systemd?(comm) diff --git a/lib/vagrant/util/platform.rb b/lib/vagrant/util/platform.rb index 2fbcac838..778f3a7ab 100644 --- a/lib/vagrant/util/platform.rb +++ b/lib/vagrant/util/platform.rb @@ -448,6 +448,19 @@ module Vagrant end end + # systemd is in use + def systemd? + if !defined?(@_systemd) + if !windows? + result = Vagrant::Util::Subprocess.execute("ps", "-o", "comm=", "1") + @_systemd = result.stdout.chomp == "systemd" + else + @_systemd = false + end + end + @_systemd + end + # @private # Reset the cached values for platform. This is not considered a public # API and should only be used for testing. diff --git a/lib/vagrant/util/ssh.rb b/lib/vagrant/util/ssh.rb index 31356b094..a32125505 100644 --- a/lib/vagrant/util/ssh.rb +++ b/lib/vagrant/util/ssh.rb @@ -139,7 +139,8 @@ module Vagrant # Use '-o' instead of '-i' because '-i' does not call # percent_expand in misc.c, but '-o' does. when passing the path, # replace '%' in the path with '%%' to escape the '%' - command_options += ["-o", "IdentityFile=%s" % [path.to_s.gsub('%', '%%')]] + path = path.to_s.gsub('%', '%%') + command_options += ["-o", "IdentityFile=\"#{path}\""] end end diff --git a/plugins/commands/login/client.rb b/plugins/commands/login/client.rb index f9b0b037e..ebfe717b3 100644 --- a/plugins/commands/login/client.rb +++ b/plugins/commands/login/client.rb @@ -5,8 +5,15 @@ require "vagrant/util/presence" module VagrantPlugins module LoginCommand class Client + APP = "app".freeze + include Vagrant::Util::Presence + attr_accessor :username_or_email + attr_accessor :password + attr_reader :two_factor_default_delivery_method + attr_reader :two_factor_delivery_methods + # Initializes a login client with the given Vagrant::Environment. # # @param [Vagrant::Environment] env @@ -35,29 +42,67 @@ module VagrantPlugins RestClient.get(url, content_type: :json) true end + rescue Errors::Unauthorized + false end # Login logs a user in and returns the token for that user. The token # is _not_ stored unless {#store_token} is called. # - # @param [String] username_or_email - # @param [String] password # @param [String] description + # @param [String] code # @return [String] token The access token, or nil if auth failed. - def login(username_or_email, password, description: nil) + def login(description: nil, code: nil) @logger.info("Logging in '#{username_or_email}'") - with_error_handling do - url = "#{Vagrant.server_url}/api/v1/authenticate" - request = { + response = post( + "/api/v1/authenticate", { user: { login: username_or_email, password: password }, token: { description: description + }, + two_factor: { + code: code } } + ) + + response["token"] + end + + # Requests a 2FA code + # @param [String] delivery_method + def request_code(delivery_method) + @env.ui.warn("Requesting 2FA code via #{delivery_method.upcase}...") + + response = post( + "/api/v1/two-factor/request-code", { + user: { + login: username_or_email, + password: password + }, + two_factor: { + delivery_method: delivery_method.downcase + } + } + ) + + two_factor = response['two_factor'] + obfuscated_destination = two_factor['obfuscated_destination'] + + @env.ui.success("2FA code sent to #{obfuscated_destination}.") + end + + # Issues a post to a Vagrant Cloud path with the given payload. + # @param [String] path + # @param [Hash] payload + # @return [Hash] response data + def post(path, payload) + with_error_handling do + url = File.join(Vagrant.server_url, path) proxy = nil proxy ||= ENV["HTTPS_PROXY"] || ENV["https_proxy"] @@ -67,7 +112,7 @@ module VagrantPlugins response = RestClient::Request.execute( method: :post, url: url, - payload: JSON.dump(request), + payload: JSON.dump(payload), proxy: proxy, headers: { accept: :json, @@ -76,8 +121,7 @@ module VagrantPlugins }, ) - data = JSON.load(response.to_s) - data["token"] + JSON.load(response.to_s) end end @@ -138,14 +182,33 @@ EOH yield rescue RestClient::Unauthorized @logger.debug("Unauthorized!") - false + raise Errors::Unauthorized + rescue RestClient::BadRequest => e + @logger.debug("Bad request:") + @logger.debug(e.message) + @logger.debug(e.backtrace.join("\n")) + parsed_response = JSON.parse(e.response) + errors = parsed_response["errors"].join("\n") + raise Errors::ServerError, errors: errors rescue RestClient::NotAcceptable => e @logger.debug("Got unacceptable response:") @logger.debug(e.message) @logger.debug(e.backtrace.join("\n")) + parsed_response = JSON.parse(e.response) + + if two_factor = parsed_response['two_factor'] + store_two_factor_information two_factor + + if two_factor_default_delivery_method != APP + request_code two_factor_default_delivery_method + end + + raise Errors::TwoFactorRequired + end + begin - errors = JSON.parse(e.response)["errors"].join("\n") + errors = parsed_response["errors"].join("\n") raise Errors::ServerError, errors: errors rescue JSON::ParserError; end @@ -158,6 +221,33 @@ EOH def token_path @env.data_dir.join("vagrant_login_token") end + + def store_two_factor_information(two_factor) + @two_factor_default_delivery_method = + two_factor['default_delivery_method'] + + @two_factor_delivery_methods = + two_factor['delivery_methods'] + + @env.ui.warn "2FA is enabled for your account." + if two_factor_default_delivery_method == APP + @env.ui.info "Enter the code from your authenticator." + else + @env.ui.info "Default method is " \ + "'#{two_factor_default_delivery_method}'." + end + + other_delivery_methods = + two_factor_delivery_methods - [APP] + + if other_delivery_methods.any? + other_delivery_methods_sentence = other_delivery_methods + .map { |word| "'#{word}'" } + .join(' or ') + @env.ui.info "You can also type #{other_delivery_methods_sentence} " \ + "to request a new code." + end + end end end end diff --git a/plugins/commands/login/command.rb b/plugins/commands/login/command.rb index c6700e960..10a8ef13f 100644 --- a/plugins/commands/login/command.rb +++ b/plugins/commands/login/command.rb @@ -17,6 +17,10 @@ module VagrantPlugins options[:check] = c end + o.on("-d", "--description DESCRIPTION", String, "Description for the Vagrant Cloud token") do |t| + options[:description] = t + end + o.on("-k", "--logout", "Logs you out if you're logged in") do |k| options[:logout] = k end @@ -24,6 +28,10 @@ module VagrantPlugins o.on("-t", "--token TOKEN", String, "Set the Vagrant Cloud token") do |t| options[:token] = t end + + o.on("-u", "--username USERNAME_OR_EMAIL", String, "Specify your Vagrant Cloud username or email address") do |t| + options[:login] = t + end end # Parse the options @@ -31,6 +39,7 @@ module VagrantPlugins return if !argv @client = Client.new(@env) + @client.username_or_email = options[:login] # Determine what task we're actually taking based on flags if options[:check] @@ -50,28 +59,44 @@ module VagrantPlugins end # Ask for the username - login = nil - password = nil - description = nil - while !login - login = @env.ui.ask("Vagrant Cloud Username: ") + if @client.username_or_email + @env.ui.output("Vagrant Cloud username or email: #{@client.username_or_email}") + end + until @client.username_or_email + @client.username_or_email = @env.ui.ask("Vagrant Cloud username or email: ") end - while !password - password = @env.ui.ask("Password (will be hidden): ", echo: false) + until @client.password + @client.password = @env.ui.ask("Password (will be hidden): ", echo: false) end - description_default = "Vagrant login from #{Socket.gethostname}" - while !description - description = - @env.ui.ask("Token description (Defaults to #{description_default.inspect}): ") + description = options[:description] + if description + @env.ui.output("Token description: #{description}") + else + description_default = "Vagrant login from #{Socket.gethostname}" + until description + description = + @env.ui.ask("Token description (Defaults to #{description_default.inspect}): ") + end + description = description_default if description.empty? end - description = description_default if description.empty? - token = @client.login(login, password, description: description) - if !token - @env.ui.error(I18n.t("login_command.invalid_login")) - return 1 + code = nil + + begin + token = @client.login(description: description, code: code) + rescue Errors::TwoFactorRequired + until code + code = @env.ui.ask("2FA code: ") + + if @client.two_factor_delivery_methods.include?(code.downcase) + delivery_method, code = code, nil + @client.request_code delivery_method + end + end + + retry end @client.store_token(token) diff --git a/plugins/commands/login/errors.rb b/plugins/commands/login/errors.rb index 614c37cf6..4d56612bd 100644 --- a/plugins/commands/login/errors.rb +++ b/plugins/commands/login/errors.rb @@ -12,6 +12,13 @@ module VagrantPlugins class ServerUnreachable < Error error_key(:server_unreachable) end + + class Unauthorized < Error + error_key(:unauthorized) + end + + class TwoFactorRequired < Error + end end end end diff --git a/plugins/commands/login/locales/en.yml b/plugins/commands/login/locales/en.yml index d7decafd3..76fa01281 100644 --- a/plugins/commands/login/locales/en.yml +++ b/plugins/commands/login/locales/en.yml @@ -2,13 +2,16 @@ en: login_command: errors: server_error: |- - The Vagrant Cloud server responded with an not-OK response: + The Vagrant Cloud server responded with a not-OK response: %{errors} server_unreachable: |- The Vagrant Cloud server is not currently accepting connections. Please check your network connection and try again later. + unauthorized: |- + Invalid username or password. Please try again. + check_logged_in: |- You are already logged in. check_not_logged_in: |- diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index 7a4444a8f..949908a79 100644 --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -83,9 +83,22 @@ module VagrantPlugins raise_winrm_exception(e, "run_wql", query) end + # @param from [Array, String] a single path or folder, or an + # array of paths and folders to upload to the guest + # @param to [String] a path or folder on the guest to upload to + # @return [FixNum] Total size transfered from host to guest def upload(from, to) file_manager = WinRM::FS::FileManager.new(connection) - file_manager.upload(from, to) + if from.is_a?(Array) + # Preserve return FixNum of bytes transfered + return_bytes = 0 + from.each do |file| + return_bytes += file_manager.upload(file, to) + end + return return_bytes + else + file_manager.upload(from, to) + end end def download(from, to) diff --git a/plugins/guests/alt/cap/change_host_name.rb b/plugins/guests/alt/cap/change_host_name.rb new file mode 100644 index 000000000..a2c7b8453 --- /dev/null +++ b/plugins/guests/alt/cap/change_host_name.rb @@ -0,0 +1,46 @@ +module VagrantPlugins + module GuestALT + module Cap + class ChangeHostName + def self.change_host_name(machine, name) + comm = machine.communicate + + if !comm.test("hostname -f | grep '^#{name}$'", sudo: false) + basename = name.split('.', 2)[0] + comm.sudo <<-EOH.gsub(/^ {14}/, '') + # Save current hostname saved in /etc/hosts + CURRENT_HOSTNAME_FULL="$(hostname -f)" + CURRENT_HOSTNAME_SHORT="$(hostname -s)" + + # New hostname to be saved in /etc/hosts + NEW_HOSTNAME_FULL='#{name}' + NEW_HOSTNAME_SHORT="${NEW_HOSTNAME_FULL%%.*}" + + # Update sysconfig + sed -i 's/\\(HOSTNAME=\\).*/\\1#{name}/' /etc/sysconfig/network + + # Set the hostname - use hostnamectl if available + if command -v hostnamectl; then + hostnamectl set-hostname --static '#{name}' + hostnamectl set-hostname --transient '#{name}' + else + hostname '#{name}' + fi + + # Update ourselves in /etc/hosts + if grep -w "$CURRENT_HOSTNAME_FULL" /etc/hosts; then + sed -i -e "s/\(\s\)$CURRENT_HOSTNAME_FULL\(\s\)/\1$NEW_HOSTNAME_FULL\2/g" -e "s/\(\s\)$CURRENT_HOSTNAME_FULL$/\1$NEW_HOSTNAME_FULL/g" /etc/hosts + fi + if grep -w "$CURRENT_HOSTNAME_SHORT" /etc/hosts; then + sed -i -e "s/\(\s\)$CURRENT_HOSTNAME_SHORT\(\s\)/\1$NEW_HOSTNAME_SHORT\2/g" -e "s/\(\s\)$CURRENT_HOSTNAME_SHORT$/\1$NEW_HOSTNAME_SHORT/g" /etc/hosts + fi + + # Restart network + service network restart + EOH + end + end + end + end + end +end diff --git a/plugins/guests/alt/cap/configure_networks.rb b/plugins/guests/alt/cap/configure_networks.rb new file mode 100644 index 000000000..851849700 --- /dev/null +++ b/plugins/guests/alt/cap/configure_networks.rb @@ -0,0 +1,126 @@ +require "tempfile" + +require_relative "../../../../lib/vagrant/util/template_renderer" + +module VagrantPlugins + module GuestALT + module Cap + class ConfigureNetworks + include Vagrant::Util + extend Vagrant::Util::GuestInspection::Linux + + def self.configure_networks(machine, networks) + comm = machine.communicate + + network_scripts_dir = machine.guest.capability(:network_scripts_dir) + + commands = {:start => [], :middle => [], :end => []} + interfaces = machine.guest.capability(:network_interfaces) + + # Check if NetworkManager is installed on the system + nmcli_installed = nmcli?(comm) + networks.each.with_index do |network, i| + network[:device] = interfaces[network[:interface]] + extra_opts = machine.config.vm.networks[i].last.dup + + if nmcli_installed + # Now check if the device is actively being managed by NetworkManager + nm_controlled = nm_controlled?(comm, network[:device]) + end + + if !extra_opts.key?(:nm_controlled) + extra_opts[:nm_controlled] = !!nm_controlled + end + + extra_opts[:nm_controlled] = case extra_opts[:nm_controlled] + when true + "yes" + when false, nil + "no" + else + extra_opts[:nm_controlled].to_s + end + + if extra_opts[:nm_controlled] == "yes" && !nmcli_installed + raise Vagrant::Errors::NetworkManagerNotInstalled, device: network[:device] + end + + # Render a new configuration + template_options = network.merge(extra_opts) + + # ALT expects netmasks to be in the CIDR notation, but users may + # specify IPV4 netmasks like "255.255.255.0". This magic converts + # the netmask to the proper value. + if template_options[:netmask] && template_options[:netmask].to_s.include?(".") + template_options[:netmask] = (32-Math.log2((IPAddr.new(template_options[:netmask], Socket::AF_INET).to_i^0xffffffff)+1)).to_i + end + + options_entry = TemplateRenderer.render("guests/alt/network_#{network[:type]}", options: template_options) + + # Upload the new configuration + options_remote_path = "/tmp/vagrant-network-entry-#{network[:device]}-#{Time.now.to_i}-#{i}" + ipv4_address_remote_path = "/tmp/vagrant-network-ipv4-address-entry-#{network[:device]}-#{Time.now.to_i}-#{i}" + ipv4_route_remote_path = "/tmp/vagrant-network-ipv4-route-entry-#{network[:device]}-#{Time.now.to_i}-#{i}" + + Tempfile.open("vagrant-alt-configure-networks") do |f| + f.binmode + f.write(options_entry) + f.fsync + f.close + machine.communicate.upload(f.path, options_remote_path) + end + + # Add the new interface and bring it back up + iface_path = "#{network_scripts_dir}/ifaces/#{network[:device]}" + + if network[:type].to_sym == :static + ipv4_address_entry = TemplateRenderer.render("guests/alt/network_ipv4address", options: template_options) + + # Upload the new ipv4address configuration + Tempfile.open("vagrant-alt-configure-ipv4-address") do |f| + f.binmode + f.write(ipv4_address_entry) + f.fsync + f.close + machine.communicate.upload(f.path, ipv4_address_remote_path) + end + + ipv4_route_entry = TemplateRenderer.render("guests/alt/network_ipv4route", options: template_options) + + # Upload the new ipv4route configuration + Tempfile.open("vagrant-alt-configure-ipv4-route") do |f| + f.binmode + f.write(ipv4_route_entry) + f.fsync + f.close + machine.communicate.upload(f.path, ipv4_route_remote_path) + end + end + + if nm_controlled and extra_opts[:nm_controlled] == "yes" + commands[:start] << "nmcli d disconnect iface '#{network[:device]}'" + else + commands[:start] << "/sbin/ifdown '#{network[:device]}'" + end + commands[:middle] << "mkdir -p '#{iface_path}'" + commands[:middle] << "mv -f '#{options_remote_path}' '#{iface_path}/options'" + if network[:type].to_sym == :static + commands[:middle] << "mv -f '#{ipv4_address_remote_path}' '#{iface_path}/ipv4address'" + commands[:middle] << "mv -f '#{ipv4_route_remote_path}' '#{iface_path}/ipv4route'" + end + if extra_opts[:nm_controlled] == "no" + commands[:end] << "/sbin/ifup '#{network[:device]}'" + end + end + if nmcli_installed + commands[:middle] << "((systemctl | grep NetworkManager.service) && systemctl restart NetworkManager) || " \ + "(test -f /etc/init.d/NetworkManager && /etc/init.d/NetworkManager restart)" + end + commands = commands[:start] + commands[:middle] + commands[:end] + comm.sudo(commands.join("\n")) + comm.wait_for_ready(5) + end + end + end + end +end diff --git a/plugins/guests/alt/cap/flavor.rb b/plugins/guests/alt/cap/flavor.rb new file mode 100644 index 000000000..9cbff908c --- /dev/null +++ b/plugins/guests/alt/cap/flavor.rb @@ -0,0 +1,63 @@ +module VagrantPlugins + module GuestALT + module Cap + class Flavor + def self.flavor(machine) + comm = machine.communicate + + # Read the version file + if comm.test("test -f /etc/os-release") + name = nil + comm.sudo("grep NAME /etc/os-release") do |type, data| + if type == :stdout + name = data.split("=")[1].gsub!(/\A"|"\Z/, '') + end + end + + if !name.nil? and name == "Sisyphus" + return :alt + end + + version = nil + comm.sudo("grep VERSION_ID /etc/os-release") do |type, data| + if type == :stdout + verstr = data.split("=")[1] + if verstr == "p8" + version = 8 + elsif verstr =~ /^[[\d]]/ + version = verstr.chomp.to_i + subversion = verstr.chomp.split(".")[1].to_i + if subversion > 90 + version += 1 + end + end + end + end + + if version.nil? or version == 0 + return :alt + else + return :"alt_#{version}" + end + else + output = "" + comm.sudo("cat /etc/altlinux-release") do |_, data| + output = data + end + + # Detect various flavors we care about + if output =~ /(ALT SP|ALT Education|ALT Workstation|ALT Workstation K|ALT Linux starter kit)\s*8(\.[1-9])?( .+)?/i + return :alt_8 + elsif output =~ /ALT\s+8(\.[1-9])?( .+)?\s.+/i + return :alt_8 + elsif output =~ /ALT Linux p8( .+)?/i + return :alt_8 + else + return :alt + end + end + end + end + end + end +end diff --git a/plugins/guests/alt/cap/network_scripts_dir.rb b/plugins/guests/alt/cap/network_scripts_dir.rb new file mode 100644 index 000000000..1a867c4e9 --- /dev/null +++ b/plugins/guests/alt/cap/network_scripts_dir.rb @@ -0,0 +1,11 @@ +module VagrantPlugins + module GuestALT + module Cap + class NetworkScriptsDir + def self.network_scripts_dir(machine) + "/etc/net" + end + end + end + end +end diff --git a/plugins/guests/alt/cap/rsync.rb b/plugins/guests/alt/cap/rsync.rb new file mode 100644 index 000000000..998315142 --- /dev/null +++ b/plugins/guests/alt/cap/rsync.rb @@ -0,0 +1,13 @@ +module VagrantPlugins + module GuestALT + module Cap + class RSync + def self.rsync_install(machine) + machine.communicate.sudo <<-EOH.gsub(/^ {12}/, '') + apt-get install -y -qq install rsync + EOH + end + end + end + end +end diff --git a/plugins/guests/alt/guest.rb b/plugins/guests/alt/guest.rb new file mode 100644 index 000000000..08525d5e6 --- /dev/null +++ b/plugins/guests/alt/guest.rb @@ -0,0 +1,9 @@ +module VagrantPlugins + module GuestALT + class Guest < Vagrant.plugin("2", :guest) + def detect?(machine) + machine.communicate.test("cat /etc/altlinux-release") + end + end + end +end diff --git a/plugins/guests/alt/plugin.rb b/plugins/guests/alt/plugin.rb new file mode 100644 index 000000000..d6375f5ea --- /dev/null +++ b/plugins/guests/alt/plugin.rb @@ -0,0 +1,40 @@ +require "vagrant" + +module VagrantPlugins + module GuestALT + class Plugin < Vagrant.plugin("2") + name "ALT Platform guest" + description "ALT Platform guest support." + + guest(:alt, :redhat) do + require_relative "guest" + Guest + end + + guest_capability(:alt, :change_host_name) do + require_relative "cap/change_host_name" + Cap::ChangeHostName + end + + guest_capability(:alt, :configure_networks) do + require_relative "cap/configure_networks" + Cap::ConfigureNetworks + end + + guest_capability(:alt, :flavor) do + require_relative "cap/flavor" + Cap::Flavor + end + + guest_capability(:alt, :network_scripts_dir) do + require_relative "cap/network_scripts_dir" + Cap::NetworkScriptsDir + end + + guest_capability(:alt, :rsync_install) do + require_relative "cap/rsync" + Cap::RSync + end + end + end +end diff --git a/plugins/guests/kali/guest.rb b/plugins/guests/kali/guest.rb old mode 100755 new mode 100644 diff --git a/plugins/guests/kali/plugin.rb b/plugins/guests/kali/plugin.rb old mode 100755 new mode 100644 diff --git a/plugins/hosts/alt/cap/nfs.rb b/plugins/hosts/alt/cap/nfs.rb new file mode 100644 index 000000000..ed68f4a92 --- /dev/null +++ b/plugins/hosts/alt/cap/nfs.rb @@ -0,0 +1,43 @@ +require "vagrant/util/subprocess" +require "vagrant/util/which" + +module VagrantPlugins + module HostALT + module Cap + class NFS + def self.nfs_check_command(env) + if systemd? + return "systemctl status --no-pager nfs-server.service" + else + return "/etc/init.d/nfs status" + end + end + + def self.nfs_start_command(env) + if systemd? + return "systemctl start rpcbind nfs-server.service" + else + return "/etc/init.d/nfs restart" + end + end + + def self.nfs_installed(environment) + if systemd? + system("systemctl --no-pager --no-legend --plain list-unit-files --all --type=service | grep --fixed-strings --quiet nfs-server.service") + else + system("rpm -q nfs-server --quiet 2>&1") + end + end + + protected + + # This tests to see if systemd is used on the system. This is used + # in newer versions of ALT, and requires a change in behavior. + def self.systemd? + result = Vagrant::Util::Subprocess.execute("ps", "-o", "comm=", "1") + return result.stdout.chomp == "systemd" + end + end + end + end +end diff --git a/plugins/hosts/alt/host.rb b/plugins/hosts/alt/host.rb new file mode 100644 index 000000000..2719f2b7e --- /dev/null +++ b/plugins/hosts/alt/host.rb @@ -0,0 +1,11 @@ +require "vagrant" + +module VagrantPlugins + module HostALT + class Host < Vagrant.plugin("2", :host) + def detect?(env) + File.exist?("/etc/altlinux-release") + end + end + end +end diff --git a/plugins/hosts/alt/plugin.rb b/plugins/hosts/alt/plugin.rb new file mode 100644 index 000000000..b8c8886b7 --- /dev/null +++ b/plugins/hosts/alt/plugin.rb @@ -0,0 +1,32 @@ +require "vagrant" + +module VagrantPlugins + module HostALT + class Plugin < Vagrant.plugin("2") + name "ALT Platform host" + description "ALT Platform host support." + + host("alt", "linux") do + require_relative "host" + Host + end + + host_capability("alt", "nfs_installed") do + require_relative "cap/nfs" + Cap::NFS + end + + # Linux-specific helpers we need to determine paths that can + # be overriden. + host_capability("alt", "nfs_check_command") do + require_relative "cap/nfs" + Cap::NFS + end + + host_capability("alt", "nfs_start_command") do + require_relative "cap/nfs" + Cap::NFS + end + end + end +end diff --git a/plugins/hosts/gentoo/cap/nfs.rb b/plugins/hosts/gentoo/cap/nfs.rb index 995e9b43c..bc4902206 100644 --- a/plugins/hosts/gentoo/cap/nfs.rb +++ b/plugins/hosts/gentoo/cap/nfs.rb @@ -6,30 +6,23 @@ module VagrantPlugins module Cap class NFS def self.nfs_check_command(env) - if systemd? - return "#{systemctl_path} status --no-pager nfs-server.service" + if Vagrant::Util::Platform.systemd? + "#{systemctl_path} status --no-pager nfs-server.service" else - return "/etc/init.d/nfs status" + "/etc/init.d/nfs status" end end def self.nfs_start_command(env) - if systemd? - return "#{systemctl_path} start rpcbind nfs-server.service" + if Vagrant::Util::Platform.systemd? + "#{systemctl_path} start rpcbind nfs-server.service" else - return "/etc/init.d/nfs restart" + "/etc/init.d/nfs restart" end end protected - # This tests to see if systemd is used on the system. This is used - # in newer versions of Arch, and requires a change in behavior. - def self.systemd? - result = Vagrant::Util::Subprocess.execute("ps", "-o", "comm=", "1") - return result.stdout.chomp == "systemd" - end - def self.systemctl_path path = Vagrant::Util::Which.which("systemctl") return path if path diff --git a/plugins/hosts/linux/cap/nfs.rb b/plugins/hosts/linux/cap/nfs.rb index 731399b8e..c4743c774 100644 --- a/plugins/hosts/linux/cap/nfs.rb +++ b/plugins/hosts/linux/cap/nfs.rb @@ -1,3 +1,4 @@ +require "shellwords" require "vagrant/util" require "vagrant/util/shell_quote" require "vagrant/util/retryable" @@ -15,11 +16,19 @@ module VagrantPlugins end def self.nfs_check_command(env) - "/etc/init.d/nfs-kernel-server status" + if Vagrant::Util::Platform.systemd? + "systemctl status --no-pager nfs-server.service" + else + "/etc/init.d/nfs-kernel-server status" + end end def self.nfs_start_command(env) - "/etc/init.d/nfs-kernel-server start" + if Vagrant::Util::Platform.systemd? + "systemctl start nfs-server.service" + else + "/etc/init.d/nfs-kernel-server start" + end end def self.nfs_export(env, ui, id, ips, folders) @@ -29,6 +38,7 @@ module VagrantPlugins nfs_start_command = env.host.capability(:nfs_start_command) nfs_opts_setup(folders) + folders = folder_dupe_check(folders) output = Vagrant::Util::TemplateRenderer.render('nfs/exports_linux', uuid: id, ips: ips, @@ -43,16 +53,20 @@ module VagrantPlugins nfs_write_exports(output) if nfs_running?(nfs_check_command) - system("sudo #{nfs_apply_command}") + Vagrant::Util::Subprocess.execute("sudo", *Shellwords.split(nfs_apply_command)).exit_code == 0 else - system("sudo #{nfs_start_command}") + Vagrant::Util::Subprocess.execute("sudo", *Shellwords.split(nfs_start_command)).exit_code == 0 end end def self.nfs_installed(environment) - retryable(tries: 10, on: TypeError) do - # Check procfs to see if NFSd is a supported filesystem - system("cat /proc/filesystems | grep nfsd > /dev/null 2>&1") + if Vagrant::Util::Platform.systemd? + Vagrant::Util::Subprocess.execute("/bin/sh", "-c", + "systemctl --no-pager --no-legend --plain list-unit-files --all --type=service " \ + "| grep nfs-server.service").exit_code == 0 + else + Vagrant::Util::Subprocess.execute("modinfo", "nfsd").exit_code == 0 || + Vagrant::Util::Subprocess.execute("grep", "nfsd", "/proc/filesystems").exit_code == 0 end end @@ -84,6 +98,37 @@ module VagrantPlugins protected + # Takes a hash of folders and removes any duplicate exports that + # share the same hostpath to avoid duplicate entries in /etc/exports + # ref: GH-4666 + def self.folder_dupe_check(folders) + return_folders = {} + # Group by hostpath to see if there are multiple exports coming + # from the same folder + export_groups = folders.values.group_by { |h| h[:hostpath] } + + # We need to check that each group key only has 1 value, + # and if not, check each nfs option. If all nfs options are the same + # we're good, otherwise throw an exception + export_groups.each do |path,group| + if group.size > 1 + # if the linux nfs options aren't all the same throw an exception + group1_opts = group.first[:linux__nfs_options] + + if !group.all? {|g| g[:linux__nfs_options] == group1_opts} + raise Vagrant::Errors::NFSDupePerms, hostpath: group.first[:hostpath] + else + # if they're the same just pick the first one + return_folders[path] = group.first + end + else + # just return folder, there are no duplicates + return_folders[path] = group.first + end + end + return_folders + end + def self.nfs_cleanup(remove_ids) return if !File.exist?(NFS_EXPORTS_PATH) @@ -186,7 +231,7 @@ module VagrantPlugins end def self.nfs_running?(check_command) - system(check_command) + Vagrant::Util::Subprocess.execute(*Shellwords.split(check_command)).exit_code == 0 end end end diff --git a/plugins/hosts/redhat/cap/nfs.rb b/plugins/hosts/redhat/cap/nfs.rb index edcabe21c..8bc4e5411 100644 --- a/plugins/hosts/redhat/cap/nfs.rb +++ b/plugins/hosts/redhat/cap/nfs.rb @@ -5,11 +5,19 @@ module VagrantPlugins module Cap class NFS def self.nfs_check_command(env) - "#{nfs_server_binary} status" + if Vagrant::Util::Platform.systemd? + "systemctl status --no-pager nfs-server.service" + else + "#{nfs_server_binary} status" + end end def self.nfs_start_command(env) - "#{nfs_server_binary} start" + if Vagrant::Util::Platform.systemd? + "systemctl start nfs-server.service" + else + "#{nfs_server_binary} start" + end end protected diff --git a/plugins/providers/virtualbox/driver/version_4_0.rb b/plugins/providers/virtualbox/driver/version_4_0.rb index 66e9a37be..43db68f0d 100644 --- a/plugins/providers/virtualbox/driver/version_4_0.rb +++ b/plugins/providers/virtualbox/driver/version_4_0.rb @@ -85,11 +85,18 @@ module VagrantPlugins execute("list", "vms").split("\n").each do |line| if vm_name = line[/^".+?"\s+\{(.+?)\}$/, 1] - info = execute("showvminfo", vm_name, "--machinereadable", retryable: true) - info.split("\n").each do |line| - if network_name = line[/^hostonlyadapter\d+="(.+?)"$/, 1] - networks.delete(network_name) + begin + info = execute("showvminfo", vm_name, "--machinereadable", retryable: true) + info.split("\n").each do |line| + if network_name = line[/^hostonlyadapter\d+="(.+?)"$/, 1] + networks.delete(network_name) + end end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end @@ -422,8 +429,15 @@ module VagrantPlugins # Ignore our own used ports next if uuid == @uuid - read_forwarded_ports(uuid, true).each do |_, _, hostport, _| - ports << hostport + begin + read_forwarded_ports(uuid, true).each do |_, _, hostport, _| + ports << hostport + end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end diff --git a/plugins/providers/virtualbox/driver/version_4_1.rb b/plugins/providers/virtualbox/driver/version_4_1.rb index 79f4174ad..ac07efd3f 100644 --- a/plugins/providers/virtualbox/driver/version_4_1.rb +++ b/plugins/providers/virtualbox/driver/version_4_1.rb @@ -176,11 +176,18 @@ module VagrantPlugins execute("list", "vms").split("\n").each do |line| if vm = line[/^".+?"\s+\{(.+?)\}$/, 1] - info = execute("showvminfo", vm, "--machinereadable", retryable: true) - info.split("\n").each do |line| - if adapter = line[/^hostonlyadapter\d+="(.+?)"$/, 1] - networks.delete(adapter) + begin + info = execute("showvminfo", vm, "--machinereadable", retryable: true) + info.split("\n").each do |line| + if adapter = line[/^hostonlyadapter\d+="(.+?)"$/, 1] + networks.delete(adapter) + end end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end @@ -525,8 +532,15 @@ module VagrantPlugins # Ignore our own used ports next if uuid == @uuid - read_forwarded_ports(uuid, true).each do |_, _, hostport, _| - ports << hostport + begin + read_forwarded_ports(uuid, true).each do |_, _, hostport, _| + ports << hostport + end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end diff --git a/plugins/providers/virtualbox/driver/version_4_2.rb b/plugins/providers/virtualbox/driver/version_4_2.rb index 6ece1d91e..2cae69d43 100644 --- a/plugins/providers/virtualbox/driver/version_4_2.rb +++ b/plugins/providers/virtualbox/driver/version_4_2.rb @@ -83,11 +83,18 @@ module VagrantPlugins execute("list", "vms").split("\n").each do |line| if line =~ /^".+?"\s+\{(.+?)\}$/ - info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) - info.split("\n").each do |inner_line| - if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ - networks.delete($1.to_s) + begin + info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) + info.split("\n").each do |inner_line| + if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ + networks.delete($1.to_s) + end end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end @@ -458,8 +465,15 @@ module VagrantPlugins # Ignore our own used ports next if uuid == @uuid - read_forwarded_ports(uuid, true).each do |_, _, hostport, _| - ports << hostport + begin + read_forwarded_ports(uuid, true).each do |_, _, hostport, _| + ports << hostport + end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index 4f2caccf8..8a1377055 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -180,11 +180,18 @@ module VagrantPlugins execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^".+?"\s+\{(.+?)\}$/ - info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) - info.split("\n").each do |inner_line| - if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ - networks.delete($1.to_s) + begin + info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) + info.split("\n").each do |inner_line| + if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ + networks.delete($1.to_s) + end end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end @@ -569,8 +576,15 @@ module VagrantPlugins # Ignore our own used ports next if uuid == @uuid - read_forwarded_ports(uuid, true).each do |_, _, hostport, _| - ports << hostport + begin + read_forwarded_ports(uuid, true).each do |_, _, hostport, _| + ports << hostport + end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index 7c5b9f16f..3efb31f38 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -17,19 +17,23 @@ module VagrantPlugins end def clear_forwarded_ports - args = [] - read_forwarded_ports(@uuid).each do |nic, name, _, _| - args.concat(["--natpf#{nic}", "delete", name]) - end + retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do + args = [] + read_forwarded_ports(@uuid).each do |nic, name, _, _| + args.concat(["--natpf#{nic}", "delete", name]) + end - execute("modifyvm", @uuid, *args) if !args.empty? + execute("modifyvm", @uuid, *args) if !args.empty? + end end def clear_shared_folders - info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) - info.split("\n").each do |line| - if line =~ /^SharedFolderNameMachineMapping\d+="(.+?)"$/ - execute("sharedfolder", "remove", @uuid, "--name", $1.to_s) + retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do + info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) + info.split("\n").each do |line| + if line =~ /^SharedFolderNameMachineMapping\d+="(.+?)"$/ + execute("sharedfolder", "remove", @uuid, "--name", $1.to_s) + end end end end @@ -41,22 +45,29 @@ module VagrantPlugins args += ["--snapshot", snapshot_name, "--options", "link"] end - execute("clonevm", master_id, *args) + execute("clonevm", master_id, *args, retryable: true) return get_machine_id(machine_name) end def create_dhcp_server(network, options) - execute("dhcpserver", "add", "--ifname", network, - "--ip", options[:dhcp_ip], - "--netmask", options[:netmask], - "--lowerip", options[:dhcp_lower], - "--upperip", options[:dhcp_upper], - "--enable") + retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do + begin + execute("dhcpserver", "add", "--ifname", network, + "--ip", options[:dhcp_ip], + "--netmask", options[:netmask], + "--lowerip", options[:dhcp_lower], + "--upperip", options[:dhcp_upper], + "--enable") + rescue Vagrant::Errors::VBoxManageError => e + return if e.extra_data[:stderr] == 'VBoxManage: error: DHCP server already exists' + raise + end + end end def create_host_only_network(options) # Create the interface - execute("hostonlyif", "create") =~ /^Interface '(.+?)' was successfully created$/ + execute("hostonlyif", "create", retryable: true) =~ /^Interface '(.+?)' was successfully created$/ name = $1.to_s # Get the IP so we can determine v4 vs v6 @@ -66,11 +77,13 @@ module VagrantPlugins if ip.ipv4? execute("hostonlyif", "ipconfig", name, "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + "--netmask", options[:netmask], + retryable: true) elsif ip.ipv6? execute("hostonlyif", "ipconfig", name, "--ipv6", options[:adapter_ip], - "--netmasklengthv6", options[:netmask].to_s) + "--netmasklengthv6", options[:netmask].to_s, + retryable: true) else raise "BUG: Unknown IP type: #{ip.inspect}" end @@ -85,7 +98,7 @@ module VagrantPlugins end def create_snapshot(machine_id, snapshot_name) - execute("snapshot", machine_id, "take", snapshot_name) + execute("snapshot", machine_id, "take", snapshot_name, retryable: true) end def delete_snapshot(machine_id, snapshot_name) @@ -95,7 +108,7 @@ module VagrantPlugins yield 0 if block_given? # Snapshot and report the % progress - execute("snapshot", machine_id, "delete", snapshot_name) do |type, data| + execute("snapshot", machine_id, "delete", snapshot_name, retryable: true) do |type, data| if type == :stderr # Append the data so we can see the full view total << data.gsub("\r", "") @@ -142,7 +155,7 @@ module VagrantPlugins total = "" yield 0 if block_given? - execute("snapshot", machine_id, "restore", snapshot_name) do |type, data| + execute("snapshot", machine_id, "restore", snapshot_name, retryable: true) do |type, data| if type == :stderr # Append the data so we can see the full view total << data.gsub("\r", "") @@ -165,7 +178,7 @@ module VagrantPlugins end def delete - execute("unregistervm", @uuid, "--delete") + execute("unregistervm", @uuid, "--delete", retryable: true) end def delete_unused_host_only_networks @@ -176,11 +189,18 @@ module VagrantPlugins execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^".+?"\s+\{(.+?)\}$/ - info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) - info.split("\n").each do |inner_line| - if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ - networks.delete($1.to_s) + begin + info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) + info.split("\n").each do |inner_line| + if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ + networks.delete($1.to_s) + end end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end @@ -192,12 +212,12 @@ module VagrantPlugins raw("dhcpserver", "remove", "--ifname", name) # Delete the actual host only network interface. - execute("hostonlyif", "remove", name) + execute("hostonlyif", "remove", name, retryable: true) end end def discard_saved_state - execute("discardstate", @uuid) + execute("discardstate", @uuid, retryable: true) end def enable_adapters(adapters) @@ -230,7 +250,7 @@ module VagrantPlugins end end - execute("modifyvm", @uuid, *args) + execute("modifyvm", @uuid, *args, retryable: true) end def execute_command(command) @@ -238,7 +258,17 @@ module VagrantPlugins end def export(path) - execute("export", @uuid, "--output", path.to_s) + retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do + begin + execute("export", @uuid, "--output", path.to_s) + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VERR_E_FILE_ERROR") + + # If the file already exists we'll throw a custom error + raise Vagrant::Errors::VirtualBoxFileExists, + stderr: e.extra_data[:stderr] + end + end end def forward_ports(ports) @@ -255,7 +285,7 @@ module VagrantPlugins pf_builder.join(",")]) end - execute("modifyvm", @uuid, *args) if !args.empty? + execute("modifyvm", @uuid, *args, retryable: true) if !args.empty? end def get_machine_id(machine_name) @@ -266,7 +296,7 @@ module VagrantPlugins end def halt - execute("controlvm", @uuid, "poweroff") + execute("controlvm", @uuid, "poweroff", retryable: true) end def import(ovf) @@ -315,7 +345,7 @@ module VagrantPlugins end end - execute("import", ovf , *name_params, *disk_params) do |type, data| + execute("import", ovf , *name_params, *disk_params, retryable: true) do |type, data| if type == :stdout # Keep track of the stdout so that we can get the VM name output << data @@ -567,9 +597,16 @@ module VagrantPlugins # Ignore our own used ports next if uuid == @uuid - read_forwarded_ports(uuid, true).each do |_, _, hostport, _, hostip| - hostip = '*' if hostip.nil? || hostip.empty? - used_ports[hostport].add?(hostip) + begin + read_forwarded_ports(uuid, true).each do |_, _, hostport, _, hostip| + hostip = '*' if hostip.nil? || hostip.empty? + used_ports[hostport].add?(hostip) + end + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") + + # VirtualBox could not find the vm. It may have been deleted + # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end @@ -590,26 +627,30 @@ module VagrantPlugins def reconfig_host_only(interface) execute("hostonlyif", "ipconfig", interface[:name], - "--ipv6", interface[:ipv6]) + "--ipv6", interface[:ipv6], retryable: true) end def remove_dhcp_server(network_name) - execute("dhcpserver", "remove", "--netname", network_name) + execute("dhcpserver", "remove", "--netname", network_name, retryable: true) end def set_mac_address(mac) - execute("modifyvm", @uuid, "--macaddress1", mac) + execute("modifyvm", @uuid, "--macaddress1", mac, retryable: true) end def set_name(name) - execute("modifyvm", @uuid, "--name", name, retryable: true) - rescue Vagrant::Errors::VBoxManageError => e - raise if !e.extra_data[:stderr].include?("VERR_ALREADY_EXISTS") + retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do + begin + execute("modifyvm", @uuid, "--name", name) + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VERR_ALREADY_EXISTS") - # We got VERR_ALREADY_EXISTS. This means that we're renaming to - # a VM name that already exists. Raise a custom error. - raise Vagrant::Errors::VirtualBoxNameExists, - stderr: e.extra_data[:stderr] + # We got VERR_ALREADY_EXISTS. This means that we're renaming to + # a VM name that already exists. Raise a custom error. + raise Vagrant::Errors::VirtualBoxNameExists, + stderr: e.extra_data[:stderr] + end + end end def share_folders(folders) @@ -633,10 +674,10 @@ module VagrantPlugins args << "--transient" if folder.key?(:transient) && folder[:transient] # Enable symlinks on the shared folder - execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1") + execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1", retryable: true) # Add the shared folder - execute("sharedfolder", "add", @uuid, *args) + execute("sharedfolder", "add", @uuid, *args, retryable: true) end end @@ -658,40 +699,40 @@ module VagrantPlugins def start(mode) command = ["startvm", @uuid, "--type", mode.to_s] - r = raw(*command) + retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do + r = raw(*command) - if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ - # Some systems return an exit code 1 for some reason. For that - # we depend on the output. - return true + if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ + # Some systems return an exit code 1 for some reason. For that + # we depend on the output. + return true + end + + # If we reached this point then it didn't work out. + raise Vagrant::Errors::VBoxManageError, + command: command.inspect, + stderr: r.stderr end - - # If we reached this point then it didn't work out. - raise Vagrant::Errors::VBoxManageError, - command: command.inspect, - stderr: r.stderr end def suspend - execute("controlvm", @uuid, "savestate") + execute("controlvm", @uuid, "savestate", retryable: true) end def unshare_folders(names) names.each do |name| - begin - execute( - "sharedfolder", "remove", @uuid, - "--name", name, - "--transient") + retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do + begin + execute( + "sharedfolder", "remove", @uuid, + "--name", name, + "--transient") - execute( - "setextradata", @uuid, - "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{name}") - rescue Vagrant::Errors::VBoxManageError => e - if e.extra_data[:stderr].include?("VBOX_E_FILE_ERROR") - # The folder doesn't exist. ignore. - else - raise + execute( + "setextradata", @uuid, + "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{name}") + rescue Vagrant::Errors::VBoxManageError => e + raise if !e.extra_data[:stderr].include?("VBOX_E_FILE_ERROR") end end end diff --git a/plugins/provisioners/ansible/config/base.rb b/plugins/provisioners/ansible/config/base.rb index 36fb5b903..201554bdf 100644 --- a/plugins/provisioners/ansible/config/base.rb +++ b/plugins/provisioners/ansible/config/base.rb @@ -1,3 +1,5 @@ +require_relative "../constants" + module VagrantPlugins module Ansible module Config @@ -6,6 +8,9 @@ module VagrantPlugins GALAXY_COMMAND_DEFAULT = "ansible-galaxy install --role-file=%{role_file} --roles-path=%{roles_path} --force".freeze PLAYBOOK_COMMAND_DEFAULT = "ansible-playbook".freeze + attr_accessor :become + attr_accessor :become_user + attr_accessor :compatibility_mode attr_accessor :config_file attr_accessor :extra_vars attr_accessor :galaxy_role_file @@ -20,13 +25,29 @@ module VagrantPlugins attr_accessor :raw_arguments attr_accessor :skip_tags attr_accessor :start_at_task - attr_accessor :sudo - attr_accessor :sudo_user attr_accessor :tags attr_accessor :vault_password_file attr_accessor :verbose + attr_accessor :version + + # + # Deprecated options + # + alias :sudo :become + def sudo=(value) + show_deprecation_info 'sudo', 'become' + @become = value + end + alias :sudo_user :become_user + def sudo_user=(value) + show_deprecation_info 'sudo_user', 'become_user' + @become_user = value + end def initialize + @become = UNSET_VALUE + @become_user = UNSET_VALUE + @compatibility_mode = Ansible::COMPATIBILITY_MODE_AUTO @config_file = UNSET_VALUE @extra_vars = UNSET_VALUE @galaxy_role_file = UNSET_VALUE @@ -41,14 +62,16 @@ module VagrantPlugins @raw_arguments = UNSET_VALUE @skip_tags = UNSET_VALUE @start_at_task = UNSET_VALUE - @sudo = UNSET_VALUE - @sudo_user = UNSET_VALUE @tags = UNSET_VALUE @vault_password_file = UNSET_VALUE @verbose = UNSET_VALUE + @version = UNSET_VALUE end def finalize! + @become = false if @become != true + @become_user = nil if @become_user == UNSET_VALUE + @compatibility_mode = nil unless Ansible::COMPATIBILITY_MODES.include?(@compatibility_mode) @config_file = nil if @config_file == UNSET_VALUE @extra_vars = nil if @extra_vars == UNSET_VALUE @galaxy_role_file = nil if @galaxy_role_file == UNSET_VALUE @@ -63,11 +86,10 @@ module VagrantPlugins @raw_arguments = nil if @raw_arguments == UNSET_VALUE @skip_tags = nil if @skip_tags == UNSET_VALUE @start_at_task = nil if @start_at_task == UNSET_VALUE - @sudo = false if @sudo != true - @sudo_user = nil if @sudo_user == UNSET_VALUE @tags = nil if @tags == UNSET_VALUE @vault_password_file = nil if @vault_password_file == UNSET_VALUE @verbose = false if @verbose == UNSET_VALUE + @version = "" if @version == UNSET_VALUE end # Just like the normal configuration "validate" method except that @@ -76,6 +98,12 @@ module VagrantPlugins def validate(machine) @errors = _detected_errors + # Validate that a compatibility mode was provided + if !compatibility_mode + @errors << I18n.t("vagrant.provisioners.ansible.errors.no_compatibility_mode", + valid_modes: Ansible::COMPATIBILITY_MODES.map { |s| "'#{s}'" }.join(', ')) + end + # Validate that a playbook path was provided if !playbook @errors << I18n.t("vagrant.provisioners.ansible.errors.no_playbook") @@ -112,6 +140,14 @@ module VagrantPlugins end end + + protected + + def show_deprecation_info(deprecated_option, new_option) + puts "DEPRECATION: The '#{deprecated_option}' option for the Ansible provisioner is deprecated." + puts "Please use the '#{new_option}' option instead." + puts "The '#{deprecated_option}' option will be removed in a future release of Vagrant.\n\n" + end end end end diff --git a/plugins/provisioners/ansible/config/guest.rb b/plugins/provisioners/ansible/config/guest.rb index 16992cb5a..a869779ae 100644 --- a/plugins/provisioners/ansible/config/guest.rb +++ b/plugins/provisioners/ansible/config/guest.rb @@ -11,7 +11,6 @@ module VagrantPlugins attr_accessor :install attr_accessor :install_mode attr_accessor :pip_args - attr_accessor :version def initialize super @@ -21,7 +20,6 @@ module VagrantPlugins @pip_args = UNSET_VALUE @provisioning_path = UNSET_VALUE @tmp_path = UNSET_VALUE - @version = UNSET_VALUE end def finalize! @@ -32,7 +30,6 @@ module VagrantPlugins @pip_args = "" if @pip_args == UNSET_VALUE @provisioning_path = "/vagrant" if provisioning_path == UNSET_VALUE @tmp_path = "/tmp/vagrant-ansible" if tmp_path == UNSET_VALUE - @version = "" if @version == UNSET_VALUE end def validate(machine) diff --git a/plugins/provisioners/ansible/config/host.rb b/plugins/provisioners/ansible/config/host.rb index 4e075ae64..06df0d1c4 100644 --- a/plugins/provisioners/ansible/config/host.rb +++ b/plugins/provisioners/ansible/config/host.rb @@ -5,16 +5,25 @@ module VagrantPlugins module Config class Host < Base - attr_accessor :ask_sudo_pass + attr_accessor :ask_become_pass attr_accessor :ask_vault_pass attr_accessor :force_remote_user attr_accessor :host_key_checking attr_accessor :raw_ssh_args + # + # Deprecated options + # + alias :ask_sudo_pass :ask_become_pass + def ask_sudo_pass=(value) + show_deprecation_warning 'ask_sudo_pass', 'ask_become_pass' + @ask_become_pass = value + end + def initialize super - @ask_sudo_pass = false + @ask_become_pass = false @ask_vault_pass = false @force_remote_user = true @host_key_checking = false @@ -24,7 +33,7 @@ module VagrantPlugins def finalize! super - @ask_sudo_pass = false if @ask_sudo_pass != true + @ask_become_pass = false if @ask_become_pass != true @ask_vault_pass = false if @ask_vault_pass != true @force_remote_user = true if @force_remote_user != false @host_key_checking = false if @host_key_checking != true diff --git a/plugins/provisioners/ansible/constants.rb b/plugins/provisioners/ansible/constants.rb new file mode 100644 index 000000000..17f72beb6 --- /dev/null +++ b/plugins/provisioners/ansible/constants.rb @@ -0,0 +1,14 @@ + +module VagrantPlugins + module Ansible + COMPATIBILITY_MODE_AUTO = "auto".freeze + COMPATIBILITY_MODE_V1_8 = "1.8".freeze + COMPATIBILITY_MODE_V2_0 = "2.0".freeze + SAFE_COMPATIBILITY_MODE = COMPATIBILITY_MODE_V1_8 + COMPATIBILITY_MODES = [ + COMPATIBILITY_MODE_AUTO, + COMPATIBILITY_MODE_V1_8, + COMPATIBILITY_MODE_V2_0, + ].freeze + end +end \ No newline at end of file diff --git a/plugins/provisioners/ansible/errors.rb b/plugins/provisioners/ansible/errors.rb index be9e55fb9..d23a2d84b 100644 --- a/plugins/provisioners/ansible/errors.rb +++ b/plugins/provisioners/ansible/errors.rb @@ -11,21 +11,30 @@ module VagrantPlugins error_key(:ansible_command_failed) end - class AnsibleNotFoundOnHost < AnsibleError - error_key(:ansible_not_found_on_host) + class AnsibleCompatibilityModeConflict < AnsibleError + error_key(:ansible_compatibility_mode_conflict) end class AnsibleNotFoundOnGuest < AnsibleError error_key(:ansible_not_found_on_guest) end + class AnsibleNotFoundOnHost < AnsibleError + error_key(:ansible_not_found_on_host) + end + class AnsiblePipInstallIsNotSupported < AnsibleError error_key(:cannot_support_pip_install) end - class AnsibleVersionNotFoundOnGuest < AnsibleError - error_key(:ansible_version_not_found_on_guest) + class AnsibleProgrammingError < AnsibleError + error_key(:ansible_programming_error) end + + class AnsibleVersionMismatch < AnsibleError + error_key(:ansible_version_mismatch) + end + end end end \ No newline at end of file diff --git a/plugins/provisioners/ansible/provisioner/base.rb b/plugins/provisioners/ansible/provisioner/base.rb index 8e52d751f..d4ebe5422 100644 --- a/plugins/provisioners/ansible/provisioner/base.rb +++ b/plugins/provisioners/ansible/provisioner/base.rb @@ -1,3 +1,4 @@ +require_relative "../constants" require_relative "../errors" require_relative "../helpers" @@ -14,15 +15,80 @@ module VagrantPlugins RANGE_PATTERN = %r{(?:\[[a-z]:[a-z]\]|\[[0-9]+?:[0-9]+?\])}.freeze + ANSIBLE_PARAMETER_NAMES = { + Ansible::COMPATIBILITY_MODE_V1_8 => { + ansible_host: "ansible_ssh_host", + ansible_password: "ansible_ssh_pass", + ansible_port: "ansible_ssh_port", + ansible_user: "ansible_ssh_user", + ask_become_pass: "ask-sudo-pass", + become: "sudo", + become_user: "sudo-user", + }, + Ansible::COMPATIBILITY_MODE_V2_0 => { + ansible_host: "ansible_host", + ansible_password: "ansible_password", + ansible_port: "ansible_port", + ansible_user: "ansible_user", + ask_become_pass: "ask-become-pass", + become: "become", + become_user: "become-user", + } + } + protected def initialize(machine, config) super + @control_machine = nil @command_arguments = [] @environment_variables = {} @inventory_machines = {} @inventory_path = nil + + @gathered_version_stdout = nil + @gathered_version_major = nil + @gathered_version = nil + end + + def set_and_check_compatibility_mode + begin + set_gathered_ansible_version(gather_ansible_version) + rescue StandardError => e + # Nothing to do here, as the fallback on safe compatibility_mode is done below + @logger.error("Error while gathering the ansible version: #{e.to_s}") + end + + if @gathered_version_major + if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO + detect_compatibility_mode + elsif @gathered_version_major.to_i < 2 && config.compatibility_mode == Ansible::COMPATIBILITY_MODE_V2_0 + # A better version comparator will be needed + # when more compatibility modes come... but so far let's keep it simple! + raise Ansible::Errors::AnsibleCompatibilityModeConflict, + ansible_version: @gathered_version, + system: @control_machine, + compatibility_mode: config.compatibility_mode + end + end + + if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO + config.compatibility_mode = Ansible::SAFE_COMPATIBILITY_MODE + + @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.compatibility_mode_not_detected", + compatibility_mode: config.compatibility_mode, + gathered_version: @gathered_version_stdout) + + "\n") + end + + unless Ansible::COMPATIBILITY_MODES.slice(1..-1).include?(config.compatibility_mode) + raise Ansible::Errors::AnsibleProgrammingError, + message: "The config.compatibility_mode must be correctly set at this stage!", + details: "config.compatibility_mode: '#{config.compatibility_mode}'" + end + + @lexicon = ANSIBLE_PARAMETER_NAMES[config.compatibility_mode] end def check_files_existence @@ -70,7 +136,7 @@ module VagrantPlugins if arg =~ /(--start-at-task|--limit)=(.+)/ shell_args << %Q(#{$1}="#{$2}") elsif arg =~ /(--extra-vars)=(.+)/ - shell_args << %Q(%s="%s") % [$1, $2.gsub('\\', '\\\\\\').gsub('"', %Q(\\"))] + shell_args << %Q(%s=%s) % [$1, $2.shellescape] else shell_args << arg end @@ -97,8 +163,8 @@ module VagrantPlugins @command_arguments << "--inventory-file=#{inventory_path}" @command_arguments << "--extra-vars=#{extra_vars_argument}" if config.extra_vars - @command_arguments << "--sudo" if config.sudo - @command_arguments << "--sudo-user=#{config.sudo_user}" if config.sudo_user + @command_arguments << "--#{@lexicon[:become]}" if config.become + @command_arguments << "--#{@lexicon[:become_user]}=#{config.become_user}" if config.become_user @command_arguments << "#{verbosity_argument}" if verbosity_is_enabled? @command_arguments << "--vault-password-file=#{config.vault_password_file}" if config.vault_password_file @command_arguments << "--tags=#{Helpers::as_list_argument(config.tags)}" if config.tags @@ -148,7 +214,13 @@ module VagrantPlugins end s = nil if vars.is_a?(Hash) - s = vars.each.collect{ |k, v| "#{k}=#{v}" }.join(" ") + s = vars.each.collect { + |k, v| + if v.is_a?(String) && v.include?(' ') && !v.match(/^('|")[^'"]+('|")$/) + v = %Q('#{v}') + end + "#{k}=#{v}" + }.join(" ") elsif vars.is_a?(Array) s = vars.join(" ") elsif vars.is_a?(String) @@ -228,7 +300,7 @@ module VagrantPlugins end group_vars.each_pair do |gname, gmembers| - if defined_groups.include?(gname.sub(/:vars$/, "")) + if defined_groups.include?(gname.sub(/:vars$/, "")) || gname == "all:vars" inventory_groups += "\n[#{gname}]\n" + gmembers.join("\n") + "\n" end end @@ -285,6 +357,44 @@ module VagrantPlugins end end + private + + def detect_compatibility_mode + if !@gathered_version_major || config.compatibility_mode != Ansible::COMPATIBILITY_MODE_AUTO + raise Ansible::Errors::AnsibleProgrammingError, + message: "The detect_compatibility_mode() function shouldn't have been called!", + details: %Q(config.compatibility_mode: '#{config.compatibility_mode}' +gathered version major number: '#{@gathered_version_major}' +gathered version stdout version: +#{@gathered_version_stdout}) + end + + if @gathered_version_major.to_i <= 1 + config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V1_8 + else + config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V2_0 + end + + @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.compatibility_mode_warning", + compatibility_mode: config.compatibility_mode, + ansible_version: @gathered_version) + + "\n") + end + + def set_gathered_ansible_version(stdout_output) + @gathered_version_stdout = stdout_output + if !@gathered_version_stdout.empty? + first_line = @gathered_version_stdout.lines[0] + ansible_version_pattern = first_line.match(/(^ansible\s+)(.+)$/) + if ansible_version_pattern + _, @gathered_version, _ = ansible_version_pattern.captures + if @gathered_version + @gathered_version_major = @gathered_version.match(/^(\d)\..+$/).captures[0].to_i + end + end + end + end + end end end diff --git a/plugins/provisioners/ansible/provisioner/guest.rb b/plugins/provisioners/ansible/provisioner/guest.rb index cdf50d9bc..1d03ff9b4 100644 --- a/plugins/provisioners/ansible/provisioner/guest.rb +++ b/plugins/provisioners/ansible/provisioner/guest.rb @@ -10,12 +10,14 @@ module VagrantPlugins def initialize(machine, config) super + @control_machine = "guest" @logger = Log4r::Logger.new("vagrant::provisioners::ansible_guest") end def provision check_files_existence check_and_install_ansible + execute_ansible_galaxy_on_guest if config.galaxy_role_file execute_ansible_playbook_on_guest end @@ -25,14 +27,14 @@ module VagrantPlugins # # This handles verifying the Ansible installation, installing it if it was # requested, and so on. This method will raise exceptions if things are wrong. + # The compatibility mode checks are also performed here in order to fetch the + # Ansible version information only once. # # Current limitations: - # - The installation of a specific Ansible version is not supported. - # Such feature is difficult to systematically provide via package repositories (apt, yum, ...). - # Installing via pip python packaging or directly from github source would be appropriate, - # but these approaches require more dependency burden. - # - There is no guarantee that the automated installation will replace - # a previous Ansible installation. + # - The installation of a specific Ansible version is only supported by + # the "pip" install_mode. + # - There is no absolute guarantee that the automated installation will replace + # a previous Ansible installation (although it works fine in many cases) # def check_and_install_ansible @logger.info("Checking for Ansible installation...") @@ -52,21 +54,39 @@ module VagrantPlugins @machine.guest.capability(:ansible_install, config.install_mode, config.version, config.pip_args) end - # Check that Ansible Playbook command is available on the guest - @machine.communicate.execute( - "test -x \"$(command -v #{config.playbook_command})\"", - error_class: Ansible::Errors::AnsibleNotFoundOnGuest, - error_key: :ansible_not_found_on_guest - ) + # This step will also fetch the Ansible version data into related instance variables + set_and_check_compatibility_mode # Check if requested ansible version is available if (!config.version.empty? && config.version.to_s.to_sym != :latest && - !@machine.guest.capability(:ansible_installed, config.version)) - raise Ansible::Errors::AnsibleVersionNotFoundOnGuest, required_version: config.version.to_s + config.version != @gathered_version) + raise Ansible::Errors::AnsibleVersionMismatch, + system: @control_machine, + required_version: config.version, + current_version: @gathered_version end end + def gather_ansible_version + raw_output = "" + + result = @machine.communicate.execute( + "ansible --version", + error_class: Ansible::Errors::AnsibleNotFoundOnGuest, + error_key: :ansible_not_found_on_guest) do |type, output| + if type == :stdout && output.lines[0] + raw_output = output.lines[0] + end + end + + if result != 0 + raw_output = "" + end + + raw_output + end + def get_provisioning_working_directory config.provisioning_path end @@ -159,7 +179,7 @@ module VagrantPlugins error_key: :config_file_not_found, config_option: option_name, path: remote_path, - system: "guest" + system: @control_machine ) end diff --git a/plugins/provisioners/ansible/provisioner/host.rb b/plugins/provisioners/ansible/provisioner/host.rb index 352a13664..3e594b83c 100644 --- a/plugins/provisioners/ansible/provisioner/host.rb +++ b/plugins/provisioners/ansible/provisioner/host.rb @@ -11,6 +11,7 @@ module VagrantPlugins def initialize(machine, config) super + @control_machine = "host" @logger = Log4r::Logger.new("vagrant::provisioners::ansible_host") end @@ -18,8 +19,9 @@ module VagrantPlugins # At this stage, the SSH access is guaranteed to be ready @ssh_info = @machine.ssh_info - check_files_existence warn_for_unsupported_platform + check_files_existence + check_ansible_version_and_compatibility execute_ansible_galaxy_from_host if config.galaxy_role_file execute_ansible_playbook_from_host @@ -31,7 +33,24 @@ module VagrantPlugins def warn_for_unsupported_platform if Vagrant::Util::Platform.windows? - @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.windows_not_supported_for_control_machine")) + @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.windows_not_supported_for_control_machine") + "\n") + end + end + + def check_ansible_version_and_compatibility + # This step will also fetch the Ansible version data into related instance variables + set_and_check_compatibility_mode + + # Skip this check when not required, nor possible + if !@gathered_version || config.version.empty? || config.version.to_s.to_sym == :latest + return + end + + if config.version != @gathered_version + raise Ansible::Errors::AnsibleVersionMismatch, + system: @control_machine, + required_version: config.version, + current_version: @gathered_version end end @@ -49,15 +68,15 @@ module VagrantPlugins if !config.force_remote_user # Pass the vagrant ssh username as Ansible default remote user, because - # the ansible_ssh_user parameter won't be added to the auto-generated inventory. + # the ansible_ssh_user/ansible_user parameter won't be added to the auto-generated inventory. @command_arguments << "--user=#{@ssh_info[:username]}" elsif config.inventory_path # Using an extra variable is the only way to ensure that the Ansible remote user # is overridden (as the ansible inventory is not under vagrant control) - @command_arguments << "--extra-vars=ansible_ssh_user='#{@ssh_info[:username]}'" + @command_arguments << "--extra-vars=#{@lexicon[:ansible_user]}='#{@ssh_info[:username]}'" end - @command_arguments << "--ask-sudo-pass" if config.ask_sudo_pass + @command_arguments << "--#{@lexicon[:ask_become_pass]}" if config.ask_become_pass @command_arguments << "--ask-vault-pass" if config.ask_vault_pass prepare_common_command_arguments @@ -88,6 +107,30 @@ module VagrantPlugins end end + def gather_ansible_version + raw_output = "" + command = %w(ansible --version) + + command << { + notify: [:stdout, :stderr] + } + + begin + result = Vagrant::Util::Subprocess.execute(*command) do |type, output| + if type == :stdout && output.lines[0] + raw_output = output + end + end + if result.exit_code != 0 + raw_output = "" + end + rescue Vagrant::Errors::CommandUnavailable + raise Ansible::Errors::AnsibleNotFoundOnHost + end + + raw_output + end + def execute_ansible_galaxy_from_host prepare_ansible_config_environment_variable @@ -199,19 +242,19 @@ module VagrantPlugins def get_inventory_ssh_machine(machine, ssh_info) forced_remote_user = "" if config.force_remote_user - forced_remote_user = "ansible_ssh_user='#{ssh_info[:username]}' " + forced_remote_user = "#{@lexicon[:ansible_user]}='#{ssh_info[:username]}' " end - "#{machine.name} ansible_ssh_host=#{ssh_info[:host]} ansible_ssh_port=#{ssh_info[:port]} #{forced_remote_user}ansible_ssh_private_key_file='#{ssh_info[:private_key_path][0]}'\n" + "#{machine.name} #{@lexicon[:ansible_host]}=#{ssh_info[:host]} #{@lexicon[:ansible_port]}=#{ssh_info[:port]} #{forced_remote_user}ansible_ssh_private_key_file='#{ssh_info[:private_key_path][0]}'\n" end def get_inventory_winrm_machine(machine, winrm_net_info) forced_remote_user = "" if config.force_remote_user - forced_remote_user = "ansible_ssh_user='#{machine.config.winrm.username}' " + forced_remote_user = "#{@lexicon[:ansible_user]}='#{machine.config.winrm.username}' " end - "#{machine.name} ansible_connection=winrm ansible_ssh_host=#{winrm_net_info[:host]} ansible_ssh_port=#{winrm_net_info[:port]} #{forced_remote_user}ansible_ssh_pass='#{machine.config.winrm.password}'\n" + "#{machine.name} ansible_connection=winrm #{@lexicon[:ansible_host]}=#{winrm_net_info[:host]} #{@lexicon[:ansible_port]}=#{winrm_net_info[:port]} #{forced_remote_user}#{@lexicon[:ansible_password]}='#{machine.config.winrm.password}'\n" end def ansible_ssh_args @@ -287,7 +330,7 @@ module VagrantPlugins _key: :config_file_not_found, config_option: option_name, path: expanded_path, - system: "host" + system: @control_machine end end diff --git a/plugins/provisioners/file/provisioner.rb b/plugins/provisioners/file/provisioner.rb index 46355c11b..8391a4503 100644 --- a/plugins/provisioners/file/provisioner.rb +++ b/plugins/provisioners/file/provisioner.rb @@ -11,15 +11,28 @@ module VagrantPlugins if File.directory?(source) # We need to make sure the actual destination folder # also exists before uploading, otherwise - # you will get nested folders. We also need to append - # a './' to the source folder so we copy the contents - # rather than the folder itself, in case a users destination - # folder differs from its source. + # you will get nested folders # # https://serverfault.com/questions/538368/make-scp-always-overwrite-or-create-directory # https://unix.stackexchange.com/questions/292641/get-scp-path-behave-like-rsync-path/292732 command = "mkdir -p \"%s\"" % destination - source << "/." + if !destination.end_with?(File::SEPARATOR) && + !source.end_with?("#{File::SEPARATOR}.") + # We also need to append a '/.' to the source folder so we copy + # the contents rather than the folder itself, in case a users + # destination folder differs from its source + # + # If source is set as `source/` it will lose the trailing + # slash due to how `File.expand_path` works, so we don't need + # a conditional for that case. + if @machine.config.vm.communicator == :winrm + # windows needs an array of paths because of the + # winrm-fs function Vagrant is using to upload file/folder. + source = Dir["#{source}#{File::SEPARATOR}*"] + else + source << "#{File::SEPARATOR}." + end + end else command = "mkdir -p \"%s\"" % File.dirname(destination) end diff --git a/plugins/provisioners/salt/config.rb b/plugins/provisioners/salt/config.rb index 4e15a7f3f..c10bdcbf0 100644 --- a/plugins/provisioners/salt/config.rb +++ b/plugins/provisioners/salt/config.rb @@ -24,6 +24,8 @@ module VagrantPlugins attr_accessor :log_level attr_accessor :masterless attr_accessor :minion_id + attr_accessor :salt_call_args + attr_accessor :salt_args ## bootstrap options attr_accessor :temp_config_dir @@ -68,6 +70,8 @@ module VagrantPlugins @python_version = UNSET_VALUE @run_service = UNSET_VALUE @master_id = UNSET_VALUE + @salt_call_args = UNSET_VALUE + @salt_args = UNSET_VALUE end def finalize! @@ -94,6 +98,8 @@ module VagrantPlugins @python_version = nil if @python_version == UNSET_VALUE @run_service = nil if @run_service == UNSET_VALUE @master_id = nil if @master_id == UNSET_VALUE + @salt_call_args = nil if @salt_call_args == UNSET_VALUE + @salt_args = nil if @salt_args == UNSET_VALUE # NOTE: Optimistic defaults are set in the provisioner. UNSET_VALUEs # are converted there to allow proper detection of unset values. @@ -149,6 +155,14 @@ module VagrantPlugins errors << I18n.t("vagrant.provisioners.salt.must_accept_keys") end + if @salt_call_args && !@salt_call_args.is_a?(Array) + errors << I18n.t("vagrant.provisioners.salt.args_array") + end + + if @salt_args && !@salt_args.is_a?(Array) + errors << I18n.t("vagrant.provisioners.salt.args_array") + end + return {"salt provisioner" => errors} end end diff --git a/plugins/provisioners/salt/provisioner.rb b/plugins/provisioners/salt/provisioner.rb index 57db562ad..c703c4cc1 100644 --- a/plugins/provisioners/salt/provisioner.rb +++ b/plugins/provisioners/salt/provisioner.rb @@ -198,6 +198,16 @@ module VagrantPlugins return options end + # Append additional arguments to the salt command + def get_salt_args + " " + Array(@config.salt_args).join(" ") + end + + # Append additional arguments to the salt-call command + def get_call_args + " " + Array(@config.salt_call_args).join(" ") + end + # Copy master and minion configs to VM def upload_configs if @config.minion_config @@ -368,7 +378,8 @@ module VagrantPlugins unless @config.masterless? @machine.communicate.sudo("salt '*' saltutil.sync_all") end - @machine.communicate.sudo("salt '*' state.highstate --verbose#{get_masterless}#{get_loglevel}#{get_colorize}#{get_pillar}", ssh_opts) do |type, data| + options = "#{get_masterless}#{get_loglevel}#{get_colorize}#{get_pillar}#{get_salt_args}" + @machine.communicate.sudo("salt '*' state.highstate --verbose#{options}", ssh_opts) do |type, data| if @config.verbose @machine.env.ui.info(data.rstrip) end @@ -380,7 +391,8 @@ module VagrantPlugins @machine.communicate.execute("C:\\salt\\salt-call.bat saltutil.sync_all", opts) end # TODO: something equivalent to { error_key: :ssh_bad_exit_status_muted }? - @machine.communicate.execute("C:\\salt\\salt-call.bat state.highstate --retcode-passthrough#{get_masterless}#{get_loglevel}#{get_colorize}#{get_pillar}", opts) do |type, data| + options = "#{get_masterless}#{get_loglevel}#{get_colorize}#{get_pillar}#{get_call_args}" + @machine.communicate.execute("C:\\salt\\salt-call.bat state.highstate --retcode-passthrough#{options}", opts) do |type, data| if @config.verbose @machine.env.ui.info(data.rstrip) end @@ -389,7 +401,8 @@ module VagrantPlugins unless @config.masterless? @machine.communicate.sudo("salt-call saltutil.sync_all") end - @machine.communicate.sudo("salt-call state.highstate --retcode-passthrough#{get_masterless}#{get_loglevel}#{get_colorize}#{get_pillar}", ssh_opts) do |type, data| + options = "#{get_masterless}#{get_loglevel}#{get_colorize}#{get_pillar}#{get_call_args}" + @machine.communicate.sudo("salt-call state.highstate --retcode-passthrough#{options}", ssh_opts) do |type, data| if @config.verbose @machine.env.ui.info(data.rstrip) end diff --git a/templates/guests/alt/network_dhcp.erb b/templates/guests/alt/network_dhcp.erb new file mode 100644 index 000000000..8d9f1a93a --- /dev/null +++ b/templates/guests/alt/network_dhcp.erb @@ -0,0 +1,7 @@ +#VAGRANT-BEGIN +# The contents below are automatically generated by Vagrant. Do not modify. +TYPE=eth +NM_CONTROLLED=<%= options.fetch(:nm_controlled, "no") %> +BOOTPROTO=dhcp +ONBOOT=yes +#VAGRANT-END diff --git a/templates/guests/alt/network_ipv4address.erb b/templates/guests/alt/network_ipv4address.erb new file mode 100644 index 000000000..dbd3340b4 --- /dev/null +++ b/templates/guests/alt/network_ipv4address.erb @@ -0,0 +1,3 @@ +#VAGRANT-BEGIN +<%= options[:ip] %>/<%= options[:netmask] %> +#VAGRANT-END diff --git a/templates/guests/alt/network_ipv4route.erb b/templates/guests/alt/network_ipv4route.erb new file mode 100644 index 000000000..fe0bea446 --- /dev/null +++ b/templates/guests/alt/network_ipv4route.erb @@ -0,0 +1,5 @@ +#VAGRANT-BEGIN +<% if options[:gateway] %> +default via <%= options[:gateway] %> +<% end %> +#VAGRANT-END diff --git a/templates/guests/alt/network_static.erb b/templates/guests/alt/network_static.erb new file mode 100644 index 000000000..84ceac55c --- /dev/null +++ b/templates/guests/alt/network_static.erb @@ -0,0 +1,7 @@ +#VAGRANT-BEGIN +# The contents below are automatically generated by Vagrant. Do not modify. +TYPE=eth +NM_CONTROLLED=<%= options.fetch(:nm_controlled, "no") %> +BOOTPROTO=static +ONBOOT=yes +#VAGRANT-END diff --git a/templates/locales/en.yml b/templates/locales/en.yml index ccab103f5..a953478c8 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -912,6 +912,9 @@ en: command: %{command} stdout: %{stdout} stderr: %{stderr} + nfs_dupe_permissions: |- + You have attempted to export the same nfs host path at %{hostpath} with + different nfs permissions. Please pick one permission and reload your guest. nfs_cant_read_exports: |- Vagrant can't read your current NFS exports! The exports file should be readable by any user. This is usually caused by invalid permissions @@ -2338,14 +2341,19 @@ en: ansible_command_failed: |- Ansible failed to complete successfully. Any error output should be visible above. Please fix these errors and try again. + ansible_compatibility_mode_conflict: |- + The requested Ansible compatibility mode (%{compatibility_mode}) is in conflict with + the Ansible installation on your Vagrant %{system} system (currently: %{ansible_version}). + See https://docs.vagrantup.com/v2/provisioning/ansible_common.html#compatibility_mode + for more information. ansible_not_found_on_guest: |- The Ansible software could not be found! Please verify that Ansible is correctly installed on your guest system. If you haven't installed Ansible yet, please install Ansible on your Vagrant basebox, or enable the automated setup with the - `install` option of this provisioner. Please check - https://docs.vagrantup.com/v2/provisioning/ansible_local.html + ansible_local provisioner `install` option. Please check + https://docs.vagrantup.com/v2/provisioning/ansible_local.html#install for more information. ansible_not_found_on_host: |- The Ansible software could not be found! Please verify @@ -2355,6 +2363,18 @@ en: on your host system. Vagrant can't do this for you in a safe and automated way. Please check https://docs.ansible.com for more information. + ansible_programming_error: |- + Ansible Provisioner Programming Error: + + %{message} + + Internal Details: + + %{details} + + Sorry, but this Vagrant error should never occur. + Please check https://github.com/mitchellh/vagrant/issues for any + existing bug report. If needed, please create a new issue. Thank you! cannot_support_pip_install: |- Unfortunately Vagrant does not support yet installing Ansible from pip for the guest OS running in the machine. @@ -2364,16 +2384,18 @@ en: to contribute back support. Thank you! https://github.com/mitchellh/vagrant - ansible_version_not_found_on_guest: |- - The requested Ansible version (%{required_version}) was not found on the guest. - Please check the ansible installation on your guest system, - or adapt the `version` option of this provisioner in your Vagrantfile. - See https://docs.vagrantup.com/v2/provisioning/ansible_local.html + ansible_version_mismatch: |- + The requested Ansible version (%{required_version}) was not found on the %{system}. + Please check the Ansible installation on your Vagrant %{system} system (currently: %{current_version}), + or adapt the provisioner `version` option in your Vagrantfile. + See https://docs.vagrantup.com/v2/provisioning/ansible_common.html#version for more information. config_file_not_found: |- `%{config_option}` does not exist on the %{system}: %{path} extra_vars_invalid: |- `extra_vars` must be a hash or a path to an existing file. Received: %{value} (as %{type}) + no_compatibility_mode: |- + `compatibility_mode` must be a valid mode (possible values: %{valid_modes}). no_playbook: |- `playbook` file path must be set. raw_arguments_invalid: |- @@ -2387,6 +2409,20 @@ en: windows_not_supported_for_control_machine: |- Windows is not officially supported for the Ansible Control Machine. Please check https://docs.ansible.com/intro_installation.html#control-machine-requirements + compatibility_mode_not_detected: |- + Vagrant gathered an unknown Ansible version: + + %{gathered_version} + and falls back on the compatibility mode '%{compatibility_mode}'. + + Alternatively, the compatibility mode can be specified in your Vagrantfile: + https://www.vagrantup.com/docs/provisioning/ansible_common.html#compatibility_mode + compatibility_mode_warning: |- + Vagrant has automatically selected the compatibility mode '%{compatibility_mode}' + according to the Ansible version installed (%{ansible_version}). + + Alternatively, the compatibility mode can be specified in your Vagrantfile: + https://www.vagrantup.com/docs/provisioning/ansible_common.html#compatibility_mode docker: wrong_provisioner: |- @@ -2406,6 +2442,8 @@ en: You must include both public and private keys. must_accept_keys: |- You must accept keys when running highstate with master! + args_array: |- + You must set this value as an array. pushes: file: diff --git a/test/unit/plugins/commands/login/client_test.rb b/test/unit/plugins/commands/login/client_test.rb index 33436dfd9..06a110706 100644 --- a/test/unit/plugins/commands/login/client_test.rb +++ b/test/unit/plugins/commands/login/client_test.rb @@ -7,7 +7,12 @@ describe VagrantPlugins::LoginCommand::Client do let(:env) { isolated_environment.create_vagrant_env } - subject { described_class.new(env) } + subject(:client) { described_class.new(env) } + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/commands/login/locales/en.yml") + I18n.reload! + end before do stub_env("ATLAS_TOKEN" => nil) @@ -38,7 +43,7 @@ describe VagrantPlugins::LoginCommand::Client do expect(subject.logged_in?).to be(true) end - it "returns false if the endpoint returns a non-200" do + it "raises an error if the endpoint returns a non-200" do stub_request(:get, url) .with(headers: headers) .to_return(body: JSON.pretty_generate("bad" => true), status: 401) @@ -55,47 +60,159 @@ describe VagrantPlugins::LoginCommand::Client do end describe "#login" do - it "returns the access token after successful login" do - request = { - "user" => { - "login" => "foo", - "password" => "bar", + let(:request) { + { + user: { + login: login, + password: password, }, - "token" => { - "description" => "Token description" + token: { + description: description, + }, + two_factor: { + code: nil } } + } - response = { - "token" => "baz", - } + let(:login) { "foo" } + let(:password) { "bar" } + let(:description) { "Token description" } - headers = { + let(:headers) { + { "Accept" => "application/json", "Content-Type" => "application/json", } + } + let(:response) { + { + token: "baz" + } + } + it "returns the access token after successful login" do stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). with(body: JSON.dump(request), headers: headers). to_return(status: 200, body: JSON.dump(response)) - expect(subject.login("foo", "bar", description: "Token description")) - .to eq("baz") + client.username_or_email = login + client.password = password + + expect(client.login(description: "Token description")).to eq("baz") end - it "returns nil on bad login" do - stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). - to_return(status: 401, body: "") + context "when 2fa is required" do + let(:response) { + { + two_factor: { + default_delivery_method: default_delivery_method, + delivery_methods: delivery_methods + } + } + } + let(:default_delivery_method) { "app" } + let(:delivery_methods) { ["app"] } - expect(subject.login("foo", "bar")).to be(false) + before do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_return(status: 406, body: JSON.dump(response)) + end + + it "raises a two-factor required error" do + expect { + client.login + }.to raise_error(VagrantPlugins::LoginCommand::Errors::TwoFactorRequired) + end + + context "when the default delivery method is not app" do + let(:default_delivery_method) { "sms" } + let(:delivery_methods) { ["app", "sms"] } + + it "requests a code and then raises a two-factor required error" do + expect(client) + .to receive(:request_code) + .with(default_delivery_method) + + expect { + client.login + }.to raise_error(VagrantPlugins::LoginCommand::Errors::TwoFactorRequired) + end + end end - it "raises an exception if it can't reach the sever" do - stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). - to_raise(SocketError) + context "on bad login" do + before do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_return(status: 401, body: "") + end - expect { subject.login("foo", "bar") }. - to raise_error(VagrantPlugins::LoginCommand::Errors::ServerUnreachable) + it "raises an error" do + expect { + client.login + }.to raise_error(VagrantPlugins::LoginCommand::Errors::Unauthorized) + end + end + + context "if it can't reach the server" do + before do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_raise(SocketError) + end + + it "raises an exception" do + expect { + subject.login + }.to raise_error(VagrantPlugins::LoginCommand::Errors::ServerUnreachable) + end + end + end + + describe "#request_code" do + let(:request) { + { + user: { + login: login, + password: password, + }, + two_factor: { + delivery_method: delivery_method + } + } + } + + let(:login) { "foo" } + let(:password) { "bar" } + let(:delivery_method) { "sms" } + + let(:headers) { + { + "Accept" => "application/json", + "Content-Type" => "application/json" + } + } + + let(:response) { + { + two_factor: { + obfuscated_destination: "SMS number ending in 1234" + } + } + } + + it "displays that the code was sent" do + expect(env.ui) + .to receive(:success) + .with("2FA code sent to SMS number ending in 1234.") + + stub_request(:post, "#{Vagrant.server_url}/api/v1/two-factor/request-code"). + with(body: JSON.dump(request), headers: headers). + to_return(status: 201, body: JSON.dump(response)) + + client.username_or_email = login + client.password = password + + client.request_code delivery_method end end diff --git a/test/unit/plugins/communicators/winrm/shell_test.rb b/test/unit/plugins/communicators/winrm/shell_test.rb index 988c07a97..4d2d53b21 100644 --- a/test/unit/plugins/communicators/winrm/shell_test.rb +++ b/test/unit/plugins/communicators/winrm/shell_test.rb @@ -31,6 +31,35 @@ describe VagrantPlugins::CommunicatorWinRM::WinRMShell do end end + describe "#upload" do + let(:fm) { double("file_manager") } + it "should call file_manager.upload for each passed in path" do + from = ["/path", "/path/folder", "/path/folder/file.py"] + to = "/destination" + size = 80 + + allow(WinRM::FS::FileManager).to receive(:new).with(connection) + .and_return(fm) + allow(fm).to receive(:upload).and_return(size) + + expect(fm).to receive(:upload).exactly(from.size).times + expect(subject.upload(from, to)).to eq(size*from.size) + end + + it "should call file_manager.upload once for a single path" do + from = "/path/folder/file.py" + to = "/destination" + size = 80 + + allow(WinRM::FS::FileManager).to receive(:new).with(connection) + .and_return(fm) + allow(fm).to receive(:upload).and_return(size) + + expect(fm).to receive(:upload).exactly(1).times + expect(subject.upload(from, to)).to eq(size) + end + end + describe ".powershell" do it "should call winrm powershell" do expect(shell).to receive(:run).with("dir").and_return(output) @@ -66,7 +95,7 @@ describe VagrantPlugins::CommunicatorWinRM::WinRMShell do end end - describe ".cmd" do + describe ".cmd" do it "should call winrm cmd" do expect(connection).to receive(:shell).with(:cmd, { }) expect(shell).to receive(:run).with("dir").and_return(output) diff --git a/test/unit/plugins/guests/alt/cap/change_host_name_test.rb b/test/unit/plugins/guests/alt/cap/change_host_name_test.rb new file mode 100644 index 000000000..51eb814b6 --- /dev/null +++ b/test/unit/plugins/guests/alt/cap/change_host_name_test.rb @@ -0,0 +1,42 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestALT::Cap::ChangeHostName" do + let(:caps) do + VagrantPlugins::GuestALT::Plugin + .components + .guest_capabilities[:alt] + end + + let(:machine) { double("machine") } + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".change_host_name" do + let(:cap) { caps.get(:change_host_name) } + + let(:name) { "banana-rama.example.com" } + + it "sets the hostname" do + comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) + + cap.change_host_name(machine, name) + expect(comm.received_commands[1]).to match(/\/etc\/sysconfig\/network/) + expect(comm.received_commands[1]).to match(/hostnamectl set-hostname --static '#{name}'/) + expect(comm.received_commands[1]).to match(/hostnamectl set-hostname --transient '#{name}'/) + expect(comm.received_commands[1]).to match(/service network restart/) + end + + it "does not change the hostname if already set" do + comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) + cap.change_host_name(machine, name) + expect(comm.received_commands.size).to eq(1) + end + end +end diff --git a/test/unit/plugins/guests/alt/cap/configure_networks_test.rb b/test/unit/plugins/guests/alt/cap/configure_networks_test.rb new file mode 100644 index 000000000..f19924d0f --- /dev/null +++ b/test/unit/plugins/guests/alt/cap/configure_networks_test.rb @@ -0,0 +1,213 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestALT::Cap::ConfigureNetworks" do + let(:caps) do + VagrantPlugins::GuestALT::Plugin + .components + .guest_capabilities[:alt] + end + + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + let(:config) { double("config", vm: vm) } + let(:guest) { double("guest") } + let(:machine) { double("machine", guest: guest, config: config) } + let(:networks){ [[{}], [{}]] } + let(:vm){ double("vm", networks: networks) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".configure_networks" do + let(:cap) { caps.get(:configure_networks) } + + before do + allow(guest).to receive(:capability) + .with(:flavor) + .and_return(:alt) + + allow(guest).to receive(:capability) + .with(:network_scripts_dir) + .and_return("/etc/net") + + allow(guest).to receive(:capability) + .with(:network_interfaces) + .and_return(["eth1", "eth2"]) + end + + let(:network_1) do + { + interface: 0, + type: "dhcp", + } + end + + let(:network_2) do + { + interface: 1, + type: "static", + ip: "33.33.33.10", + netmask: "255.255.0.0", + gateway: "33.33.0.1", + } + end + + context "with NetworkManager installed" do + before do + allow(cap).to receive(:nmcli?).and_return true + end + + context "with devices managed by NetworkManager" do + before do + allow(cap).to receive(:nm_controlled?).and_return true + end + + context "with nm_controlled option omitted" do + it "downs networks via nmcli, creates ifaces and restart NetworksManager" do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/nmcli.*disconnect/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/NetworkManager/) + expect(comm.received_commands[0]).to_not match(/ifdown|ifup/) + end + end + + context "with nm_controlled option set to true" do + let(:networks){ [[{nm_controlled: true}], [{nm_controlled: true}]] } + + it "downs networks via nmcli, creates ifaces and restart NetworksManager" do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/nmcli.*disconnect/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/NetworkManager/) + expect(comm.received_commands[0]).to_not match(/(ifdown|ifup)/) + end + end + + context "with nm_controlled option set to false" do + let(:networks){ [[{nm_controlled: false}], [{nm_controlled: false}]] } + + it "downs networks manually, creates ifaces, starts networks manually and restart NetworksManager" do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/ifdown/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/ifup/) + expect(comm.received_commands[0]).to match(/NetworkManager/) + expect(comm.received_commands[0]).to_not match(/nmcli/) + end + end + + context "with nm_controlled option set to false on first device" do + let(:networks){ [[{nm_controlled: false}], [{nm_controlled: true}]] } + + it "downs networks, creates ifaces and starts the networks with one managed manually and one NetworkManager controlled" do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/ifdown/) + expect(comm.received_commands[0]).to match(/nmcli.*disconnect/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/ifup/) + expect(comm.received_commands[0]).to match(/NetworkManager/) + end + end + end + + context "with devices not managed by NetworkManager" do + before do + allow(cap).to receive(:nm_controlled?).and_return false + end + + context "with nm_controlled option omitted" do + it "creates and starts the networks manually" do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/ifdown/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/ifup/) + expect(comm.received_commands[0]).to match(/NetworkManager/) + expect(comm.received_commands[0]).to_not match(/nmcli/) + end + end + + context "with nm_controlled option set to true" do + let(:networks){ [[{nm_controlled: true}], [{nm_controlled: true}]] } + + it "creates and starts the networks via nmcli" do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/ifdown/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/NetworkManager/) + expect(comm.received_commands[0]).to_not match(/ifup/) + end + end + + context "with nm_controlled option set to false" do + let(:networks){ [[{nm_controlled: false}], [{nm_controlled: false}]] } + + it "creates and starts the networks via ifup " do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/ifdown/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/ifup/) + expect(comm.received_commands[0]).to match(/NetworkManager/) + expect(comm.received_commands[0]).to_not match(/nmcli/) + end + end + + context "with nm_controlled option set to false on first device" do + let(:networks){ [[{nm_controlled: false}], [{nm_controlled: true}]] } + + it "creates and starts the networks with one managed manually and one NetworkManager controlled" do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/ifdown/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/ifup/) + expect(comm.received_commands[0]).to match(/NetworkManager/) + expect(comm.received_commands[0]).to_not match(/nmcli.*disconnect/) + end + end + end + end + + context "without NetworkManager installed" do + before do + allow(cap).to receive(:nmcli?).and_return false + end + + context "with nm_controlled option omitted" do + + it "creates and starts the networks manually" do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/ifdown/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/ifup/) + expect(comm.received_commands[0]).to_not match(/nmcli/) + expect(comm.received_commands[0]).to_not match(/NetworkManager/) + end + end + + context "with nm_controlled option omitted" do + let(:networks){ [[{nm_controlled: false}], [{nm_controlled: false}]] } + + it "creates and starts the networks manually" do + cap.configure_networks(machine, [network_1, network_2]) + expect(comm.received_commands[0]).to match(/ifdown/) + expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) + expect(comm.received_commands[0]).to match(/ifup/) + expect(comm.received_commands[0]).to_not match(/nmcli/) + expect(comm.received_commands[0]).to_not match(/NetworkManager/) + end + end + + context "with nm_controlled option set" do + let(:networks){ [[{nm_controlled: false}], [{nm_controlled: true}]] } + + it "raises an error" do + expect{ cap.configure_networks(machine, [network_1, network_2]) }.to raise_error(Vagrant::Errors::NetworkManagerNotInstalled) + end + end + end + end +end diff --git a/test/unit/plugins/guests/alt/cap/flavor_test.rb b/test/unit/plugins/guests/alt/cap/flavor_test.rb new file mode 100644 index 000000000..65538f0fe --- /dev/null +++ b/test/unit/plugins/guests/alt/cap/flavor_test.rb @@ -0,0 +1,72 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestALT::Cap::Flavor" do + let(:caps) do + VagrantPlugins::GuestALT::Plugin + .components + .guest_capabilities[:alt] + end + + let(:machine) { double("machine") } + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".flavor" do + let(:cap) { caps.get(:flavor) } + + context "without /etc/os-release file" do + { + "ALT 8.1 Server" => :alt_8, + "ALT Education 8.1" => :alt_8, + "ALT Workstation 8.1" => :alt_8, + "ALT Workstation K 8.1 (Centaurea Ruthenica)" => :alt_8, + "ALT Linux p8 (Hypericum)" => :alt_8, + + "ALT Sisyphus (unstable) (sisyphus)" => :alt, + "ALT Linux Sisyphus (unstable)" => :alt, + "ALT Linux 6.0.1 Spt (separator)" => :alt, + "ALT Linux 7.0.5 School Master" => :alt, + "ALT starter kit (Hypericum)" => :alt, + + "ALT" => :alt, + "Simply" => :alt, + }.each do |str, expected| + it "returns #{expected} for #{str} in /etc/altlinux-release" do + comm.stub_command("test -f /etc/os-release", exit_code: 1) + comm.stub_command("cat /etc/altlinux-release", stdout: str) + expect(cap.flavor(machine)).to be(expected) + end + end + end + + context "with /etc/os-release file" do + { + [ "NAME=\"Sisyphus\"", "VERSION_ID=20161130" ] => :alt, + + [ "NAME=\"ALT Education\"", "VERSION_ID=8.1" ] => :alt_8, + [ "NAME=\"ALT Server\"", "VERSION_ID=8.1" ] => :alt_8, + [ "NAME=\"ALT SPServer\"", "VERSION_ID=8.0" ] => :alt_8, + [ "NAME=\"starter kit\"", "VERSION_ID=p8" ] => :alt_8, + [ "NAME=\"ALT Linux\"", "VERSION_ID=8.0.0" ] => :alt_8, + [ "NAME=\"Simply Linux\"", "VERSION_ID=7.95.0" ] => :alt_8, + + [ "NAME=\"ALT Linux\"", "VERSION_ID=7.0.5" ] => :alt_7, + [ "NAME=\"School Junior\"", "VERSION_ID=7.0.5" ] => :alt_7, + }.each do |strs, expected| + it "returns #{expected} for #{strs[0]} and #{strs[1]} in /etc/os-release" do + comm.stub_command("test -f /etc/os-release", exit_code: 0) + comm.stub_command("grep NAME /etc/os-release", stdout: strs[0]) + comm.stub_command("grep VERSION_ID /etc/os-release", stdout: strs[1]) + expect(cap.flavor(machine)).to be(expected) + end + end + end + end +end diff --git a/test/unit/plugins/guests/alt/cap/network_scripts_dir_test.rb b/test/unit/plugins/guests/alt/cap/network_scripts_dir_test.rb new file mode 100644 index 000000000..4aac58069 --- /dev/null +++ b/test/unit/plugins/guests/alt/cap/network_scripts_dir_test.rb @@ -0,0 +1,21 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestALT::Cap::NetworkScriptsDir" do + let(:caps) do + VagrantPlugins::GuestALT::Plugin + .components + .guest_capabilities[:alt] + end + + let(:machine) { double("machine") } + + describe ".network_scripts_dir" do + let(:cap) { caps.get(:network_scripts_dir) } + + let(:name) { "banana-rama.example.com" } + + it "is /etc/net" do + expect(cap.network_scripts_dir(machine)).to eq("/etc/net") + end + end +end diff --git a/test/unit/plugins/guests/alt/cap/rsync_test.rb b/test/unit/plugins/guests/alt/cap/rsync_test.rb new file mode 100644 index 000000000..126db9963 --- /dev/null +++ b/test/unit/plugins/guests/alt/cap/rsync_test.rb @@ -0,0 +1,29 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestALT::Cap:RSync" do + let(:caps) do + VagrantPlugins::GuestALT::Plugin + .components + .guest_capabilities[:alt] + end + + let(:machine) { double("machine") } + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".rsync_install" do + let(:cap) { caps.get(:rsync_install) } + + it "installs rsync" do + cap.rsync_install(machine) + expect(comm.received_commands[0]).to match(/install rsync/) + end + end +end diff --git a/test/unit/plugins/hosts/linux/cap/nfs_test.rb b/test/unit/plugins/hosts/linux/cap/nfs_test.rb index eed8b7221..bb8ca9079 100644 --- a/test/unit/plugins/hosts/linux/cap/nfs_test.rb +++ b/test/unit/plugins/hosts/linux/cap/nfs_test.rb @@ -33,6 +33,44 @@ describe VagrantPlugins::HostLinux::Cap::NFS do @tmp_exports = nil end + describe ".nfs_check_command" do + let(:cap){ caps.get(:nfs_check_command) } + + context "without systemd" do + before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(false) } + + it "should use init.d script" do + expect(cap.nfs_check_command(env)).to include("init.d") + end + end + context "with systemd" do + before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(true) } + + it "should use systemctl" do + expect(cap.nfs_check_command(env)).to include("systemctl") + end + end + end + + describe ".nfs_start_command" do + let(:cap){ caps.get(:nfs_start_command) } + + context "without systemd" do + before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(false) } + + it "should use init.d script" do + expect(cap.nfs_start_command(env)).to include("init.d") + end + end + context "with systemd" do + before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(true) } + + it "should use systemctl" do + expect(cap.nfs_start_command(env)).to include("systemctl") + end + end + end + describe ".nfs_export" do let(:cap){ caps.get(:nfs_export) } @@ -43,8 +81,9 @@ describe VagrantPlugins::HostLinux::Cap::NFS do allow(host).to receive(:capability).with(:nfs_check_command).and_return("/bin/true") allow(host).to receive(:capability).with(:nfs_start_command).and_return("/bin/true") allow(ui).to receive(:info) - allow(cap).to receive(:system).with("sudo /bin/true").and_return(true) - allow(cap).to receive(:system).with("/bin/true").and_return(true) + allow(Vagrant::Util::Subprocess).to receive(:execute).and_call_original + allow(Vagrant::Util::Subprocess).to receive(:execute).with("sudo", "/bin/true").and_return(double(:result, exit_code: 0)) + allow(Vagrant::Util::Subprocess).to receive(:execute).with("/bin/true").and_return(double(:result, exit_code: 0)) end it "should export new entries" do @@ -79,6 +118,42 @@ EOH expect(exports_content).to include("/tmp") expect(exports_content).not_to include("/var") end + + it "throws an exception with at least 2 different nfs options" do + folders = {"/vagrant"=> + {:hostpath=>"/home/vagrant", + :linux__nfs_options=>["rw","all_squash"]}, + "/var/www/project"=> + {:hostpath=>"/home/vagrant", + :linux__nfs_options=>["rw","sync"]}} + + expect { cap.nfs_export(env, ui, SecureRandom.uuid, ["127.0.0.1"], folders) }. + to raise_error Vagrant::Errors::NFSDupePerms + end + + it "writes only 1 hostpath for multiple exports" do + folders = {"/vagrant"=> + {:hostpath=>"/home/vagrant", + :linux__nfs_options=>["rw","all_squash"]}, + "/var/www/otherproject"=> + {:hostpath=>"/newhome/otherproject", + :linux__nfs_options=>["rw","all_squash"]}, + "/var/www/project"=> + {:hostpath=>"/home/vagrant", + :linux__nfs_options=>["rw","all_squash"]}} + valid_id = SecureRandom.uuid + content =<<-EOH +\n# VAGRANT-BEGIN: #{Process.uid} #{valid_id} +"/home/vagrant" 127.0.0.1(rw,all_squash,anonuid=,anongid=,fsid=) +"/newhome/otherproject" 127.0.0.1(rw,all_squash,anonuid=,anongid=,fsid=) +# VAGRANT-END: #{Process.uid} #{valid_id} +EOH + + cap.nfs_export(env, ui, valid_id, ["127.0.0.1"], folders) + exports_content = File.read(exports_path) + expect(exports_content).to eq(content) + end + end describe ".nfs_prune" do diff --git a/test/unit/plugins/provisioners/ansible/config/guest_test.rb b/test/unit/plugins/provisioners/ansible/config/guest_test.rb index 92b7851c5..19010ba94 100644 --- a/test/unit/plugins/provisioners/ansible/config/guest_test.rb +++ b/test/unit/plugins/provisioners/ansible/config/guest_test.rb @@ -16,7 +16,11 @@ describe VagrantPlugins::Ansible::Config::Guest do let(:existing_file) { "this/path/is/a/stub" } it "supports a list of options" do - supported_options = %w( config_file + supported_options = %w( + become + become_user + compatibility_mode + config_file extra_vars galaxy_command galaxy_role_file @@ -40,7 +44,8 @@ describe VagrantPlugins::Ansible::Config::Guest do tmp_path vault_password_file verbose - version ) + version + ) expect(get_provisioner_option_names(described_class)).to eql(supported_options) end @@ -55,7 +60,6 @@ describe VagrantPlugins::Ansible::Config::Guest do expect(subject.install_mode).to eql(:default) expect(subject.provisioning_path).to eql("/vagrant") expect(subject.tmp_path).to eql("/tmp/vagrant-ansible") - expect(subject.version).to be_empty end end diff --git a/test/unit/plugins/provisioners/ansible/config/host_test.rb b/test/unit/plugins/provisioners/ansible/config/host_test.rb index 1a23fecfe..b868d2648 100644 --- a/test/unit/plugins/provisioners/ansible/config/host_test.rb +++ b/test/unit/plugins/provisioners/ansible/config/host_test.rb @@ -13,8 +13,13 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do let(:existing_file) { File.expand_path(__FILE__) } it "supports a list of options" do - supported_options = %w( ask_sudo_pass + supported_options = %w( + ask_become_pass + ask_sudo_pass ask_vault_pass + become + become_user + compatibility_mode config_file extra_vars force_remote_user @@ -36,7 +41,9 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do sudo_user tags vault_password_file - verbose ) + verbose + version + ) expect(get_provisioner_option_names(described_class)).to eql(supported_options) end @@ -47,7 +54,8 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do it "assigns default values to unset host-specific options" do subject.finalize! - expect(subject.ask_sudo_pass).to be(false) + expect(subject.ask_become_pass).to be(false) + expect(subject.ask_sudo_pass).to be(false) # deprecated expect(subject.ask_vault_pass).to be(false) expect(subject.force_remote_user).to be(true) expect(subject.host_key_checking).to be(false) @@ -61,7 +69,14 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do describe "host_key_checking option" do it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :host_key_checking, false end + describe "ask_become_pass option" do + it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :ask_become_pass, false + end describe "ask_sudo_pass option" do + before do + # Filter the deprecation notice + allow($stdout).to receive(:puts) + end it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :ask_sudo_pass, false end describe "ask_vault_pass option" do diff --git a/test/unit/plugins/provisioners/ansible/config/shared.rb b/test/unit/plugins/provisioners/ansible/config/shared.rb index d9bd8cca3..16902a138 100644 --- a/test/unit/plugins/provisioners/ansible/config/shared.rb +++ b/test/unit/plugins/provisioners/ansible/config/shared.rb @@ -3,6 +3,9 @@ shared_examples_for 'options shared by both Ansible provisioners' do it "assigns default values to unset common options" do subject.finalize! + expect(subject.become).to be(false) + expect(subject.become_user).to be_nil + expect(subject.compatibility_mode).to eql(VagrantPlugins::Ansible::COMPATIBILITY_MODE_AUTO) expect(subject.config_file).to be_nil expect(subject.extra_vars).to be_nil expect(subject.galaxy_command).to eql("ansible-galaxy install --role-file=%{role_file} --roles-path=%{roles_path} --force") @@ -17,11 +20,12 @@ shared_examples_for 'options shared by both Ansible provisioners' do expect(subject.raw_arguments).to be_nil expect(subject.skip_tags).to be_nil expect(subject.start_at_task).to be_nil - expect(subject.sudo).to be(false) - expect(subject.sudo_user).to be_nil + expect(subject.sudo).to be(false) # deprecated + expect(subject.sudo_user).to be_nil # deprecated expect(subject.tags).to be_nil expect(subject.vault_password_file).to be_nil expect(subject.verbose).to be(false) + expect(subject.version).to be_empty end end @@ -41,6 +45,44 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup | ]) end + describe "compatibility_mode option" do + + VagrantPlugins::Ansible::COMPATIBILITY_MODES.each do |valid_mode| + it "supports compatibility mode '#{valid_mode}'" do + subject.compatibility_mode = valid_mode + subject.finalize! + + result = subject.validate(machine) + expect(subject.compatibility_mode).to eql(valid_mode) + end + end + + it "returns an error if the compatibility mode is not set" do + subject.compatibility_mode = nil + subject.finalize! + + result = subject.validate(machine) + expect(result[provisioner_label]).to eql([ + I18n.t("vagrant.provisioners.ansible.errors.no_compatibility_mode", + valid_modes: "'auto', '1.8', '2.0'") + ]) + end + + %w(invalid 1.9 2.3).each do |invalid_mode| + it "returns an error if the compatibility mode is invalid (e.g. '#{invalid_mode}')" do + subject.compatibility_mode = invalid_mode + subject.finalize! + + result = subject.validate(machine) + expect(result[provisioner_label]).to eql([ + I18n.t("vagrant.provisioners.ansible.errors.no_compatibility_mode", + valid_modes: "'auto', '1.8', '2.0'") + ]) + end + end + + end + it "passes if the extra_vars option is a hash" do subject.extra_vars = { var1: 1, var2: "foo" } subject.finalize! @@ -82,6 +124,7 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup | end it "it collects and returns all detected errors" do + subject.compatibility_mode = nil subject.playbook = nil subject.extra_vars = ["var1", 3, "var2", 5] subject.raw_arguments = { arg1: 1, arg2: "foo" } @@ -89,7 +132,10 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup | result = subject.validate(machine) - expect(result[provisioner_label].size).to eql(3) + expect(result[provisioner_label].size).to eql(4) + expect(result[provisioner_label]).to include( + I18n.t("vagrant.provisioners.ansible.errors.no_compatibility_mode", + valid_modes: "'auto', '1.8', '2.0'")) expect(result[provisioner_label]).to include( I18n.t("vagrant.provisioners.ansible.errors.no_playbook")) expect(result[provisioner_label]).to include( @@ -102,7 +148,15 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup | value: subject.raw_arguments.to_s)) end + describe "become option" do + it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :become, false + end + describe "sudo option" do + before do + # Filter the deprecation notice + allow($stdout).to receive(:puts) + end it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :sudo, false end diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index 4a68993db..3b552f323 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -60,6 +60,8 @@ VF stubbed_ui = Vagrant::UI::Colored.new allow(stubbed_ui).to receive(:detail).and_return("") + allow(stubbed_ui).to receive(:warn).and_return("") + allow(machine.env).to receive(:ui).and_return(stubbed_ui) config.playbook = 'playbook.yml' @@ -69,6 +71,15 @@ VF # Class methods for code reuse across examples # + def self.it_should_check_ansible_version() + it "execute 'ansible --version' before executing 'ansible-playbook'" do + expect(Vagrant::Util::Subprocess).to receive(:execute). + once.with('ansible', '--version', { :notify => [:stdout, :stderr] }) + expect(Vagrant::Util::Subprocess).to receive(:execute). + once.with('ansible-playbook', any_args) + end + end + def self.it_should_set_arguments_and_environment_variables( expected_args_count = 5, expected_vars_count = 4, @@ -76,9 +87,7 @@ VF expected_transport_mode = "ssh") it "sets implicit arguments in a specific order" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| - - expect(args[0]).to eq("ansible-playbook") + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args[1]).to eq("--connection=ssh") expect(args[2]).to eq("--timeout=30") @@ -90,7 +99,7 @@ VF end it "sets --limit argument" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| all_limits = args.select { |x| x =~ /^(--limit=|-l)/ } if config.raw_arguments raw_limits = config.raw_arguments.select { |x| x =~ /^(--limit=|-l)/ } @@ -108,7 +117,7 @@ VF end it "exports environment variables" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last if expected_host_key_checking @@ -116,6 +125,7 @@ VF else expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o UserKnownHostsFile=/dev/null") end + expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentitiesOnly=yes") expect(cmd_opts[:env]['ANSIBLE_FORCE_COLOR']).to eql("true") expect(cmd_opts[:env]).to_not include("ANSIBLE_NOCOLOR") @@ -126,14 +136,14 @@ VF # "roughly" verify that only expected args/vars have been defined by the provisioner it "sets the expected number of arguments and environment variables" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| - expect(args.length-2).to eq(expected_args_count) + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| + expect(args.length - 2).to eq(expected_args_count) expect(args.last[:env].length).to eq(expected_vars_count) }.and_return(default_execute_result) end it "enables '#{expected_transport_mode}' as default transport mode" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| index = args.rindex("--connection=#{expected_transport_mode}") expect(index).to be > 0 expect(find_last_argument_after(index, args, /--connection=\w+/)).to be(false) @@ -144,7 +154,7 @@ VF def self.it_should_set_optional_arguments(arg_map) it "sets optional arguments" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| arg_map.each_pair do |vagrant_option, ansible_argument| index = args.index(ansible_argument) if config.send(vagrant_option) @@ -159,7 +169,7 @@ VF def self.it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "configures ControlPersist (like Ansible defaults) via ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ControlMaster=auto") expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ControlPersist=60s") @@ -167,23 +177,24 @@ VF end end - def self.it_should_create_and_use_generated_inventory(with_ssh_user = true) + def self.it_should_create_and_use_generated_inventory(with_user = true) it "generates an inventory with all active machines" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(config.inventory_path).to be_nil expect(File.exists?(generated_inventory_file)).to be(true) inventory_content = File.read(generated_inventory_file) - if with_ssh_user - expect(inventory_content).to include("#{machine.name} ansible_ssh_host=#{machine.ssh_info[:host]} ansible_ssh_port=#{machine.ssh_info[:port]} ansible_ssh_user='#{machine.ssh_info[:username]}' ansible_ssh_private_key_file='#{machine.ssh_info[:private_key_path][0]}'\n") + _ssh = config.compatibility_mode == VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 ? "" : "_ssh" + if with_user + expect(inventory_content).to include("#{machine.name} ansible#{_ssh}_host=#{machine.ssh_info[:host]} ansible#{_ssh}_port=#{machine.ssh_info[:port]} ansible#{_ssh}_user='#{machine.ssh_info[:username]}' ansible_ssh_private_key_file='#{machine.ssh_info[:private_key_path][0]}'\n") else - expect(inventory_content).to include("#{machine.name} ansible_ssh_host=#{machine.ssh_info[:host]} ansible_ssh_port=#{machine.ssh_info[:port]} ansible_ssh_private_key_file='#{machine.ssh_info[:private_key_path][0]}'\n") + expect(inventory_content).to include("#{machine.name} ansible#{_ssh}_host=#{machine.ssh_info[:host]} ansible#{_ssh}_port=#{machine.ssh_info[:port]} ansible_ssh_private_key_file='#{machine.ssh_info[:private_key_path][0]}'\n") end expect(inventory_content).to include("# MISSING: '#{iso_env.machine_names[1]}' machine was probably removed without using Vagrant. This machine should be recreated.\n") }.and_return(default_execute_result) end it "sets as ansible inventory the directory containing the auto-generated inventory file" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_index = args.rindex("--inventory-file=#{generated_inventory_dir}") expect(inventory_index).to be > 0 expect(find_last_argument_after(inventory_index, args, /--inventory-file=\w+/)).to be(false) @@ -260,11 +271,12 @@ VF end describe "with default options" do + it_should_check_ansible_version it_should_set_arguments_and_environment_variables it_should_create_and_use_generated_inventory it "does not add any group section to the generated inventory" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to_not match(/^\s*\[^\\+\]\s*$/) }.and_return(default_execute_result) @@ -275,14 +287,152 @@ VF end end + describe "deprecated 'sudo' options are aliases for equivalent 'become' options" do + before do + # Filter the deprecation notices + allow($stdout).to receive(:puts) + + config.sudo = true + config.sudo_user = 'deployer' + config.ask_sudo_pass = true + end + + it_should_set_optional_arguments({"sudo" => "--sudo", + "sudo_user" => "--sudo-user=deployer", + "ask_sudo_pass" => "--ask-sudo-pass", + "become" => "--sudo", + "become_user" => "--sudo-user=deployer", + "ask_become_pass" => "--ask-sudo-pass"}) + end + + context "with compatibility_mode 'auto'" do + before do + config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_AUTO + end + + valid_versions = { + "0.6": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8, + "1.9.4": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8, + "2.5.0.0-rc1": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, + "2.x.y.z": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, + "4.3.2.1": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, + } + valid_versions.each_pair do |ansible_version, mode| + describe "and ansible version #{ansible_version}" do + before do + allow(subject).to receive(:gather_ansible_version).and_return("ansible #{ansible_version}\n...\n") + end + + it "detects the compatibility mode #{mode}" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| + expect(config.compatibility_mode).to eq(mode) + }.and_return(default_execute_result) + end + + it "warns about compatibility mode auto-detection being used" do + expect(machine.env.ui).to receive(:warn).with( + I18n.t("vagrant.provisioners.ansible.compatibility_mode_warning", + compatibility_mode: mode, ansible_version: ansible_version) + + "\n") + end + end + end + + invalid_versions = [ + "ansible devel", + "anything 1.2", + "2.9.2.1", + ] + invalid_versions.each do |unknown_ansible_version| + describe "and `ansible --version` returning '#{unknown_ansible_version}'" do + before do + allow(subject).to receive(:gather_ansible_version).and_return(unknown_ansible_version) + end + + it "applies the safest compatibility mode ('#{VagrantPlugins::Ansible::SAFE_COMPATIBILITY_MODE}')" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| + expect(config.compatibility_mode).to eq(VagrantPlugins::Ansible::SAFE_COMPATIBILITY_MODE) + }.and_return(default_execute_result) + end + + it "warns about not being able to detect the best compatibility mode" do + expect(machine.env.ui).to receive(:warn).with( + I18n.t("vagrant.provisioners.ansible.compatibility_mode_not_detected", + compatibility_mode: VagrantPlugins::Ansible::SAFE_COMPATIBILITY_MODE, + gathered_version: unknown_ansible_version) + + "\n") + end + end + end + + end + + context "with compatibility_mode '#{VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8}'" do + before do + config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8 + end + + it_should_check_ansible_version + it_should_create_and_use_generated_inventory + + it "doesn't warn about compatibility mode auto-detection" do + expect(machine.env.ui).to_not receive(:warn) + end + end + + context "with compatibility_mode '#{VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0}'" do + before do + config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 + allow(subject).to receive(:gather_ansible_version).and_return("ansible 2.3.0.0\n...\n") + end + + it_should_create_and_use_generated_inventory + + it "doesn't warn about compatibility mode auto-detection" do + expect(machine.env.ui).to_not receive(:warn) + end + + describe "and an incompatible ansible version" do + before do + allow(subject).to receive(:gather_ansible_version).and_return("ansible 1.9.3\n...\n") + end + + it "raises a compatibility conflict error", skip_before: false, skip_after: true do + expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCompatibilityModeConflict) + end + end + + describe "deprecated 'sudo' options are aliases for equivalent 'become' options" do + before do + # Filter the deprecation notices + allow($stdout).to receive(:puts) + + config.sudo = true + config.sudo_user = 'deployer' + config.ask_sudo_pass = true + end + + it_should_set_optional_arguments({"sudo" => "--become", + "sudo_user" => "--become-user=deployer", + "ask_sudo_pass" => "--ask-become-pass", + "become" => "--become", + "become_user" => "--become-user=deployer", + "ask_become_pass" => "--ask-become-pass"}) + end + end + describe "with playbook_command option" do before do config.playbook_command = "custom-ansible-playbook" + + # set the compatibility mode to ensure that only ansible-playbook is excuted + config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8 end it "uses custom playbook_command to run playbooks" do expect(Vagrant::Util::Subprocess).to receive(:execute) .with("custom-ansible-playbook", any_args) + .and_return(default_execute_result) end end @@ -291,11 +441,15 @@ VF it "adds host variables (given in Hash format) to the generated inventory" do config.host_vars = { - machine1: {"http_port" => 80, "comments" => "'some text with spaces'"} + machine1: { + "http_port" => 80, + "comments" => "'some text with spaces and quotes'", + "description" => "text with spaces but no quotes", + } } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) - expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 comments='some text with spaces'$") + expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 comments='some text with spaces and quotes' description='text with spaces but no quotes'") }.and_return(default_execute_result) end @@ -303,7 +457,8 @@ VF config.host_vars = { machine1: ["http_port=80", "maxRequestsPerChild=808"] } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { + + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") }.and_return(default_execute_result) @@ -313,7 +468,8 @@ VF config.host_vars = { :machine1 => "http_port=80 maxRequestsPerChild=808" } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { + + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") }.and_return(default_execute_result) @@ -323,7 +479,8 @@ VF config.host_vars = { "machine1" => "http_port=80 maxRequestsPerChild=808" } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") }.and_return(default_execute_result) @@ -345,7 +502,7 @@ VF "bar:children" => ["group1", "group2", "group3", "group5"], } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) # Accept String instead of Array for group member list @@ -383,7 +540,7 @@ VF "group3:vars" => "stringvar1=stringvalue1 stringvar2=stringvalue2", } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) # Hash syntax @@ -396,6 +553,19 @@ VF expect(inventory_content).to include("[group3:vars]\nstringvar1=stringvalue1\nstringvar2=stringvalue2\n") }.and_return(default_execute_result) end + + it "adds 'all:vars' section to the generated inventory" do + config.groups = { + "all:vars" => { "var1" => "value1", "var2" => "value2" } + } + + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| + inventory_content = File.read(generated_inventory_file) + + expect(inventory_content).to include("[all:vars]\nvar1=value1\nvar2=value2\n") + + }.and_return(default_execute_result) + end end describe "with host_key_checking option enabled" do @@ -408,18 +578,18 @@ VF describe "with boolean (flag) options disabled" do before do - config.sudo = false - config.ask_sudo_pass = false + config.become = false + config.ask_become_pass = false config.ask_vault_pass = false - config.sudo_user = 'root' + config.become_user = 'root' end it_should_set_arguments_and_environment_variables 6 - it_should_set_optional_arguments({ "sudo_user" => "--sudo-user=root" }) + it_should_set_optional_arguments({ "become_user" => "--sudo-user=root" }) it "it does not set boolean flag when corresponding option is set to false" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args.index("--sudo")).to be_nil expect(args.index("--ask-sudo-pass")).to be_nil expect(args.index("--ask-vault-pass")).to be_nil @@ -429,7 +599,7 @@ VF describe "with raw_arguments option" do before do - config.sudo = false + config.become = false config.force_remote_user = false config.skip_tags = %w(foo bar) config.limit = "all" @@ -448,7 +618,7 @@ VF it_should_set_arguments_and_environment_variables 17, 4, false, "paramiko" it "sets all raw arguments" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| config.raw_arguments.each do |raw_arg| expect(args).to include(raw_arg) end @@ -456,7 +626,7 @@ VF end it "sets raw arguments after arguments related to supported options" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args.index("--user=lion")).to be > args.index("--user=testuser") expect(args.index("--inventory-file=/forget/it/my/friend")).to be > args.index("--inventory-file=#{generated_inventory_dir}") expect(args.index("--limit=bar")).to be > args.index("--limit=all") @@ -465,7 +635,7 @@ VF end it "sets boolean flag (e.g. --sudo) defined in raw_arguments, even if corresponding option is set to false" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include('--sudo') }.and_return(default_execute_result) end @@ -490,7 +660,7 @@ VF it_should_set_arguments_and_environment_variables 6 it "uses a --user argument to set a default remote user" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") expect(args).to include("--user=#{machine.ssh_info[:username]}") }.and_return(default_execute_result) @@ -521,8 +691,7 @@ VF it_should_set_arguments_and_environment_variables it "generates an inventory with winrm connection settings" do - - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(config.inventory_path).to be_nil expect(File.exists?(generated_inventory_file)).to be(true) inventory_content = File.read(generated_inventory_file) @@ -537,7 +706,7 @@ VF end it "doesn't set the ansible remote user in inventory and use '--user' argument with the vagrant ssh username" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) expect(inventory_content).to include("machine1 ansible_connection=winrm ansible_ssh_host=127.0.0.1 ansible_ssh_port=55986 ansible_ssh_pass='winword'\n") @@ -555,7 +724,7 @@ VF it_should_set_arguments_and_environment_variables 6 it "does not generate the inventory and uses given inventory path instead" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--inventory-file=#{existing_file}") expect(args).not_to include("--inventory-file=#{generated_inventory_file}") expect(File.exists?(generated_inventory_file)).to be(false) @@ -563,7 +732,7 @@ VF end it "uses an --extra-vars argument to force ansible_ssh_user parameter" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).not_to include("--user=#{machine.ssh_info[:username]}") expect(args).to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") }.and_return(default_execute_result) @@ -575,7 +744,7 @@ VF end it "uses a --user argument to set a default remote user" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") expect(args).to include("--user=#{machine.ssh_info[:username]}") }.and_return(default_execute_result) @@ -589,7 +758,7 @@ VF end it "sets ANSIBLE_CONFIG environment variable" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to include("ANSIBLE_CONFIG") expect(cmd_opts[:env]['ANSIBLE_CONFIG']).to eql(existing_file) @@ -605,7 +774,7 @@ VF it_should_set_arguments_and_environment_variables 6 it "should ask the vault password" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--ask-vault-pass") }.and_return(default_execute_result) end @@ -619,7 +788,7 @@ VF it_should_set_arguments_and_environment_variables 6 it "uses the given vault password file" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--vault-password-file=#{existing_file}") }.and_return(default_execute_result) end @@ -634,7 +803,7 @@ VF it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "passes custom SSH options via ANSIBLE_SSH_ARGS with the highest priority" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last raw_opt_index = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ControlMaster=no") default_opt_index = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ControlMaster=auto") @@ -648,7 +817,7 @@ VF end it "sets '-o ForwardAgent=yes' via ANSIBLE_SSH_ARGS with higher priority than raw_ssh_args values" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last forwardAgentYes = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ForwardAgent=yes") forwardAgentNo = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ForwardAgent=no") @@ -668,7 +837,7 @@ VF it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "passes additional Identity Files via ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/an/other/identity") expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/yet/an/other/key") @@ -682,7 +851,7 @@ VF end it "replaces `%` with `%%`" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/foo%%bar/key") expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/bar%%%%buz/key") @@ -699,7 +868,7 @@ VF it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "enables SSH-Forwarding via ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ForwardAgent=yes") }.and_return(default_execute_result) @@ -712,7 +881,7 @@ VF end it "sets '-o ProxyCommand' via ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ProxyCommand='ssh -W %h:%p -q user@remote_libvirt_host'") }.and_return(default_execute_result) @@ -782,10 +951,12 @@ VF describe "without colorized output" do before do allow(machine.env).to receive(:ui).and_return(Vagrant::UI::Basic.new) + + allow(machine.env.ui).to receive(:warn).and_return("") # hide the breaking change warning end it "disables ansible-playbook colored output" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to_not include("ANSIBLE_FORCE_COLOR") expect(cmd_opts[:env]['ANSIBLE_NOCOLOR']).to eql("true") @@ -793,6 +964,54 @@ VF end end + + context "with version option set" do + before do + config.version = "2.3.4.5" + end + + describe "and the installed ansible version is correct" do + before do + allow(subject).to receive(:gather_ansible_version).and_return("ansible #{config.version}\n...\n") + end + + it "executes ansible-playbook command" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args).and_return(default_execute_result) + end + end + + describe "and there is an ansible version mismatch" do + before do + allow(subject).to receive(:gather_ansible_version).and_return("ansible 1.9.6\n...\n") + end + + it "raises an error about the ansible version mismatch", skip_before: false, skip_after: true do + expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleVersionMismatch) + end + end + + describe "and the installed ansible version cannot be detected" do + before do + allow(subject).to receive(:gather_ansible_version).and_return("") + end + + it "skips the ansible version check and executes ansible-playbook command" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args).and_return(default_execute_result) + end + end + + describe "with special value: 'latest'" do + before do + config.version = :latest + allow(subject).to receive(:gather_ansible_version).and_return("ansible 2.2.0.1\n...\n") + end + + it "skips the ansible version check and executes ansible-playbook command" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args).and_return(default_execute_result) + end + end + end + describe "with galaxy support" do before do @@ -809,7 +1028,11 @@ VF expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCommandFailed) end - it "execute ansible-galaxy, and then ansible-playbook" do + it "execute three commands: ansible --version, ansible-galaxy, and ansible-playbook" do + expect(Vagrant::Util::Subprocess).to receive(:execute) + .once + .with('ansible', '--version', { :notify => [:stdout, :stderr] }) + .and_return(default_execute_result) expect(Vagrant::Util::Subprocess).to receive(:execute) .once .with('ansible-galaxy', any_args) @@ -843,7 +1066,7 @@ VF end it "sets ANSIBLE_ROLES_PATH with corresponding absolute path" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to include("ANSIBLE_ROLES_PATH") expect(cmd_opts[:env]['ANSIBLE_ROLES_PATH']).to eql(File.join(machine.env.root_path, "my-roles")) @@ -854,10 +1077,10 @@ VF context "with extra_vars option defined" do describe "with a hash value" do before do - config.extra_vars = { var1: %Q(string with 'apostrophes', \\, " and =), var2: { x: 42 } } + config.extra_vars = { var1: %Q(string with 'apo$trophe$', \\, " and =), var2: { x: 42 } } end - it_should_set_optional_arguments({ "extra_vars" => "--extra-vars={\"var1\":\"string with 'apostrophes', \\\\, \\\" and =\",\"var2\":{\"x\":42}}" }) + it_should_set_optional_arguments({ "extra_vars" => "--extra-vars={\"var1\":\"string with 'apo$trophe$', \\\\, \\\" and =\",\"var2\":{\"x\":42}}" }) end describe "with a string value referring to file path (with the '@' prefix)" do @@ -879,11 +1102,11 @@ VF # command line arguments config.galaxy_roles_path = "/up/to the stars" - config.extra_vars = { var1: %Q(string with 'apostrophes', \\, " and =), var2: { x: 42 } } - config.sudo = true - config.sudo_user = 'deployer' + config.extra_vars = { var1: %Q(string with 'apo$trophe$', \\, " and =), var2: { x: 42 } } + config.become = true + config.become_user = 'deployer' config.verbose = "vvv" - config.ask_sudo_pass = true + config.ask_become_pass = true config.ask_vault_pass = true config.vault_password_file = existing_file config.tags = %w(db www) @@ -900,11 +1123,11 @@ VF it_should_set_arguments_and_environment_variables 21, 6, true it_should_explicitly_enable_ansible_ssh_control_persist_defaults - it_should_set_optional_arguments({ "extra_vars" => "--extra-vars={\"var1\":\"string with 'apostrophes', \\\\, \\\" and =\",\"var2\":{\"x\":42}}", - "sudo" => "--sudo", - "sudo_user" => "--sudo-user=deployer", + it_should_set_optional_arguments({ "extra_vars" => "--extra-vars={\"var1\":\"string with 'apo$trophe$', \\\\, \\\" and =\",\"var2\":{\"x\":42}}", + "become" => "--sudo", + "become_user" => "--sudo-user=deployer", "verbose" => "-vvv", - "ask_sudo_pass" => "--ask-sudo-pass", + "ask_become_pass" => "--ask-sudo-pass", "ask_vault_pass" => "--ask-vault-pass", "vault_password_file" => "--vault-password-file=#{File.expand_path(__FILE__)}", "tags" => "--tags=db,www", @@ -914,7 +1137,7 @@ VF }) it "also includes given raw arguments" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--why-not") expect(args).to include("--su-user=foot") expect(args).to include("--ask-su-pass") @@ -925,7 +1148,7 @@ VF it "shows the ansible-playbook command, with additional quotes when required" do expect(machine.env.ui).to receive(:detail) - .with(%Q(PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_ROLES_PATH='/up/to the stars' ANSIBLE_CONFIG='#{existing_file}' ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_SSH_ARGS='-o IdentitiesOnly=yes -o IdentityFile=/my/key1 -o IdentityFile=/my/key2 -o ForwardAgent=yes -o ControlMaster=no -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --connection=ssh --timeout=30 --ask-sudo-pass --ask-vault-pass --limit="machine*:&vagrant:!that_one" --inventory-file=#{generated_inventory_dir} --extra-vars="{\\"var1\\":\\"string with 'apostrophes', \\\\\\\\, \\\\\\" and =\\",\\"var2\\":{\\"x\\":42}}" --sudo --sudo-user=deployer -vvv --vault-password-file=#{existing_file} --tags=db,www --skip-tags=foo,bar --start-at-task="joe's awesome task" --why-not --su-user=foot --ask-su-pass --limit=all --private-key=./myself.key --extra-vars='{\"var3\":\"foo\"}' playbook.yml)) + .with(%Q(PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_ROLES_PATH='/up/to the stars' ANSIBLE_CONFIG='#{existing_file}' ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_SSH_ARGS='-o IdentitiesOnly=yes -o IdentityFile=/my/key1 -o IdentityFile=/my/key2 -o ForwardAgent=yes -o ControlMaster=no -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --connection=ssh --timeout=30 --ask-sudo-pass --ask-vault-pass --limit="machine*:&vagrant:!that_one" --inventory-file=#{generated_inventory_dir} --extra-vars=\\{\\"var1\\":\\"string\\ with\\ \\'apo\\$trophe\\$\\',\\ \\\\\\\\,\\ \\\\\\"\\ and\\ \\=\\",\\"var2\\":\\{\\"x\\":42\\}\\} --sudo --sudo-user=deployer -vvv --vault-password-file=#{existing_file} --tags=db,www --skip-tags=foo,bar --start-at-task="joe's awesome task" --why-not --su-user=foot --ask-su-pass --limit=all --private-key=./myself.key --extra-vars='{\"var3\":\"foo\"}' playbook.yml)) end end @@ -954,7 +1177,7 @@ VF end it "uses an SSH ProxyCommand to reach the VM" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ProxyCommand='ssh boot9docker@127.0.0.1 -p 2299 -i /path/to/docker/host/key -o Compression=yes -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no exec nc %h %p 2>/dev/null'") }.and_return(default_execute_result) @@ -969,11 +1192,14 @@ VF before do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) allow(machine.ui).to receive(:warn) + + # Set the compatibility mode to only get the Windows warning + config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8 end it "warns that Windows is not officially supported for the Ansible control machine" do expect(machine.env.ui).to receive(:warn) - .with(I18n.t("vagrant.provisioners.ansible.windows_not_supported_for_control_machine")) + .with(I18n.t("vagrant.provisioners.ansible.windows_not_supported_for_control_machine") + "\n") end end @@ -983,7 +1209,7 @@ VF end it "does not set IdentitiesOnly=yes in ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to_not include("-o IdentitiesOnly=yes") }.and_return(default_execute_result) @@ -993,7 +1219,7 @@ VF it "does not set ANSIBLE_SSH_ARGS environment variable" do config.host_key_checking = true - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to_not include('ANSIBLE_SSH_ARGS') }.and_return(Vagrant::Util::Subprocess::Result.new(0, "", "")) @@ -1006,7 +1232,7 @@ VF it 'does not set IdentitiesOnly=yes in ANSIBLE_SSH_ARGS' do ssh_info[:keys_only] = false - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to_not include("-o IdentitiesOnly=yes") }.and_return(default_execute_result) diff --git a/test/unit/plugins/provisioners/file/provisioner_test.rb b/test/unit/plugins/provisioners/file/provisioner_test.rb index 8da38e6d7..0aedb8fe8 100644 --- a/test/unit/plugins/provisioners/file/provisioner_test.rb +++ b/test/unit/plugins/provisioners/file/provisioner_test.rb @@ -89,5 +89,39 @@ describe VagrantPlugins::FileUpload::Provisioner do subject.provision end + + it "appends a '/.' if the destination doesnt end with a file separator" do + allow(config).to receive(:source).and_return("/source") + allow(config).to receive(:destination).and_return("/foo/bar") + allow(File).to receive(:directory?).with("/source").and_return(true) + + expect(guest).to receive(:capability?). + with(:shell_expand_guest_path).and_return(true) + expect(guest).to receive(:capability). + with(:shell_expand_guest_path, "/foo/bar").and_return("/foo/bar") + + expect(communicator).to receive(:upload).with("/source/.", "/foo/bar") + + subject.provision + end + + it "sends an array of files and folders if winrm and destination doesn't end with file separator" do + files = ["/source/file.py", "/source/folder"] + allow(Dir).to receive(:[]).and_return(files) + allow(config).to receive(:source).and_return("/source") + allow(config).to receive(:destination).and_return("/foo/bar") + allow(File).to receive(:directory?).with("/source").and_return(true) + allow(machine.config.vm).to receive(:communicator).and_return(:winrm) + + expect(guest).to receive(:capability?). + with(:shell_expand_guest_path).and_return(true) + expect(guest).to receive(:capability). + with(:shell_expand_guest_path, "/foo/bar").and_return("/foo/bar") + + expect(communicator).to receive(:upload) + .with(files, "/foo/bar") + + subject.provision + end end end diff --git a/test/unit/plugins/provisioners/salt/config_test.rb b/test/unit/plugins/provisioners/salt/config_test.rb index 24bf24796..2571bdbb2 100644 --- a/test/unit/plugins/provisioners/salt/config_test.rb +++ b/test/unit/plugins/provisioners/salt/config_test.rb @@ -78,5 +78,41 @@ describe VagrantPlugins::Salt::Config do expect(result[error_key]).to be_empty end end + + context "salt_call_args" do + it "fails if salt_call_args is not an array" do + subject.salt_call_args = "--flags" + subject.finalize! + + result = subject.validate(machine) + expect(result[error_key]).to_not be_empty + end + + it "is valid if is set and not missing" do + subject.salt_call_args = ["--flags"] + subject.finalize! + + result = subject.validate(machine) + expect(result[error_key]).to be_empty + end + end + + context "salt_args" do + it "fails if not an array" do + subject.salt_args = "--flags" + subject.finalize! + + result = subject.validate(machine) + expect(result[error_key]).to_not be_empty + end + + it "is valid if is set and not missing" do + subject.salt_args = ["--flags"] + subject.finalize! + + result = subject.validate(machine) + expect(result[error_key]).to be_empty + end + end end end diff --git a/test/unit/plugins/provisioners/salt/provisioner_test.rb b/test/unit/plugins/provisioners/salt/provisioner_test.rb index b203ea332..8ebf3b846 100644 --- a/test/unit/plugins/provisioners/salt/provisioner_test.rb +++ b/test/unit/plugins/provisioners/salt/provisioner_test.rb @@ -32,4 +32,89 @@ describe VagrantPlugins::Salt::Provisioner do describe "#provision" do end + + describe "#call_highstate" do + context "master" do + it "passes along extra cli flags" do + allow(config).to receive(:run_highstate).and_return(true) + allow(config).to receive(:verbose).and_return(true) + allow(config).to receive(:masterless?).and_return(false) + allow(config).to receive(:masterless).and_return(false) + allow(config).to receive(:minion_id).and_return(nil) + allow(config).to receive(:log_level).and_return(nil) + allow(config).to receive(:colorize).and_return(false) + allow(config).to receive(:pillar_data).and_return([]) + allow(config).to receive(:install_master).and_return(true) + + allow(config).to receive(:salt_args).and_return(["--async"]) + allow(machine.communicate).to receive(:sudo) + allow(machine.config.vm).to receive(:communicator).and_return(:notwinrm) + + expect(machine.communicate).to receive(:sudo).with("salt '*' state.highstate --verbose --log-level=debug --no-color --async", {:error_key=>:ssh_bad_exit_status_muted}) + subject.call_highstate() + end + + it "has no additional cli flags if not included" do + allow(config).to receive(:run_highstate).and_return(true) + allow(config).to receive(:verbose).and_return(true) + allow(config).to receive(:masterless?).and_return(false) + allow(config).to receive(:masterless).and_return(false) + allow(config).to receive(:minion_id).and_return(nil) + allow(config).to receive(:log_level).and_return(nil) + allow(config).to receive(:colorize).and_return(false) + allow(config).to receive(:pillar_data).and_return([]) + allow(config).to receive(:install_master).and_return(true) + + allow(config).to receive(:salt_args).and_return(nil) + allow(machine.communicate).to receive(:sudo) + allow(machine.config.vm).to receive(:communicator).and_return(:notwinrm) + + expect(machine.communicate).to receive(:sudo).with("salt '*' state.highstate --verbose --log-level=debug --no-color ", {:error_key=>:ssh_bad_exit_status_muted}) + subject.call_highstate() + end + end + + context "with masterless" do + it "passes along extra cli flags" do + allow(config).to receive(:run_highstate).and_return(true) + allow(config).to receive(:verbose).and_return(true) + allow(config).to receive(:masterless?).and_return(true) + allow(config).to receive(:masterless).and_return(true) + allow(config).to receive(:minion_id).and_return(nil) + allow(config).to receive(:log_level).and_return(nil) + allow(config).to receive(:colorize).and_return(false) + allow(config).to receive(:pillar_data).and_return([]) + + allow(config).to receive(:salt_args).and_return(["--async"]) + allow(config).to receive(:salt_call_args).and_return(["--output-dif"]) + allow(machine.communicate).to receive(:sudo) + allow(machine.config.vm).to receive(:communicator).and_return(:notwinrm) + allow(config).to receive(:install_master).and_return(false) + + expect(machine.communicate).to receive(:sudo).with("salt-call state.highstate --retcode-passthrough --local --log-level=debug --no-color --output-dif", {:error_key=>:ssh_bad_exit_status_muted}) + subject.call_highstate() + end + + it "has no additional cli flags if not included" do + allow(config).to receive(:run_highstate).and_return(true) + allow(config).to receive(:verbose).and_return(true) + allow(config).to receive(:masterless?).and_return(true) + allow(config).to receive(:masterless).and_return(true) + allow(config).to receive(:minion_id).and_return(nil) + allow(config).to receive(:log_level).and_return(nil) + allow(config).to receive(:colorize).and_return(false) + allow(config).to receive(:pillar_data).and_return([]) + + allow(config).to receive(:salt_call_args).and_return(nil) + allow(config).to receive(:salt_args).and_return(nil) + allow(machine.communicate).to receive(:sudo) + allow(machine.config.vm).to receive(:communicator).and_return(:notwinrm) + allow(config).to receive(:install_master).and_return(false) + + expect(machine.communicate).to receive(:sudo).with("salt-call state.highstate --retcode-passthrough --local --log-level=debug --no-color ", {:error_key=>:ssh_bad_exit_status_muted}) + subject.call_highstate() + end + end + end + end diff --git a/test/unit/vagrant/util/platform_test.rb b/test/unit/vagrant/util/platform_test.rb index 28511b2a1..b97f11ff9 100644 --- a/test/unit/vagrant/util/platform_test.rb +++ b/test/unit/vagrant/util/platform_test.rb @@ -133,4 +133,27 @@ describe Vagrant::Util::Platform do end end end + + describe ".systemd?" do + before{ allow(subject).to receive(:windows?).and_return(false) } + after{ subject.reset! } + + context "on windows" do + before{ expect(subject).to receive(:windows?).and_return(true) } + + it "should return false" do + expect(subject.systemd?).to be_falsey + end + end + + it "should return true if systemd is in use" do + expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double(:result, stdout: "systemd")) + expect(subject.systemd?).to be_truthy + end + + it "should return false if systemd is not in use" do + expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double(:result, stdout: "other")) + expect(subject.systemd?).to be_falsey + end + end end diff --git a/test/unit/vagrant/util/ssh_test.rb b/test/unit/vagrant/util/ssh_test.rb index eb9da43d8..97de4f379 100644 --- a/test/unit/vagrant/util/ssh_test.rb +++ b/test/unit/vagrant/util/ssh_test.rb @@ -67,7 +67,7 @@ describe Vagrant::Util::SSH do expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) - .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL","-o", "Compression=yes", "-o", "DSAAuthentication=yes", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=#{ssh_info[:private_key_path][0]}") + .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL","-o", "Compression=yes", "-o", "DSAAuthentication=yes", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=\"#{ssh_info[:private_key_path][0]}\"") end context "when disabling compression or dsa_authentication flags" do @@ -85,7 +85,7 @@ describe Vagrant::Util::SSH do expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) - .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=#{ssh_info[:private_key_path][0]}") + .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=\"#{ssh_info[:private_key_path][0]}\"") end end @@ -103,7 +103,7 @@ describe Vagrant::Util::SSH do expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) - .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "IdentityFile=#{ssh_info[:private_key_path][0]}") + .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "IdentityFile=\"#{ssh_info[:private_key_path][0]}\"") end end @@ -140,7 +140,7 @@ describe Vagrant::Util::SSH do expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) - .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=#{ssh_info[:private_key_path][0]}","-o", "ForwardX11=yes", "-o", "ForwardX11Trusted=yes") + .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=\"#{ssh_info[:private_key_path][0]}\"","-o", "ForwardX11=yes", "-o", "ForwardX11Trusted=yes") end end @@ -158,7 +158,7 @@ describe Vagrant::Util::SSH do expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) - .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=#{ssh_info[:private_key_path][0]}","-o", "ForwardAgent=yes") + .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=\"#{ssh_info[:private_key_path][0]}\"","-o", "ForwardAgent=yes") end end @@ -176,7 +176,7 @@ describe Vagrant::Util::SSH do expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) - .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=#{ssh_info[:private_key_path][0]}", "-L", "8008:localhost:80") + .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=\"#{ssh_info[:private_key_path][0]}\"", "-L", "8008:localhost:80") end end @@ -194,7 +194,7 @@ describe Vagrant::Util::SSH do expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) - .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=#{ssh_info[:private_key_path][0]}", "-6") + .with("ssh", "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "IdentityFile=\"#{ssh_info[:private_key_path][0]}\"", "-6") end end diff --git a/version.txt b/version.txt index 2a64ecf1b..335195d0f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.9.9.dev +2.0.1.dev diff --git a/website/config.rb b/website/config.rb index a765ec2d7..b9042d601 100644 --- a/website/config.rb +++ b/website/config.rb @@ -2,7 +2,7 @@ set :base_url, "https://www.vagrantup.com/" activate :hashicorp do |h| h.name = "vagrant" - h.version = "1.9.8" + h.version = "2.0.0" h.github_slug = "mitchellh/vagrant" h.website_root = "website" end diff --git a/website/source/docs/provisioning/ansible.html.md b/website/source/docs/provisioning/ansible.html.md index 3dda49b31..af13bb6f6 100644 --- a/website/source/docs/provisioning/ansible.html.md +++ b/website/source/docs/provisioning/ansible.html.md @@ -14,10 +14,8 @@ description: |- The Vagrant Ansible provisioner allows you to provision the guest using [Ansible](http://ansible.com) playbooks by executing **`ansible-playbook` from the Vagrant host**.
- Warning: If you are not familiar with Ansible and Vagrant already, - I recommend starting with the shell - provisioner. However, if you are comfortable with Vagrant already, Vagrant - is a great way to learn Ansible. + Warning: + If you are not familiar with Ansible and Vagrant already, I recommend starting with the shell provisioner. However, if you are comfortable with Vagrant already, Vagrant is a great way to learn Ansible.
## Setup Requirements @@ -53,10 +51,17 @@ end This section lists the _specific_ options for the Ansible (remote) provisioner. In addition to the options listed below, this provisioner supports the [**common options** for both Ansible provisioners](/docs/provisioning/ansible_common.html). -- `ask_sudo_pass` (boolean) - require Ansible to [prompt for a sudo password](https://docs.ansible.com/intro_getting_started.html#remote-connection-information). +- `ask_become_pass` (boolean) - require Ansible to [prompt for a password](https://docs.ansible.com/intro_getting_started.html#remote-connection-information) when switching to another user with the [become/sudo mechanism](http://docs.ansible.com/ansible/become.html). The default value is `false`. +- `ask_sudo_pass` (boolean) - Backwards compatible alias for the [ask_become_pass](#ask_become_pass) option. + +
+ Deprecation: + The `ask_sudo_pass` option is deprecated and will be removed in a future release. Please use the [**`ask_become_pass`**](#ask_become_pass) option instead. +
+ - `ask_vault_pass` (boolean) - require Ansible to [prompt for a vault password](https://docs.ansible.com/playbooks_vault.html#vault). The default value is `false`. @@ -67,7 +72,10 @@ This section lists the _specific_ options for the Ansible (remote) provisioner. The default value is `true`. - **Note:** This option was introduced in Vagrant 1.8.0. Previous Vagrant versions behave like if this option was set to `false`. +
+ Compatibility Note: + This option was introduced in Vagrant 1.8.0. Previous Vagrant versions behave like if this option was set to `false`. +
- `host_key_checking` (boolean) - require Ansible to [enable SSH host key checking](https://docs.ansible.com/intro_getting_started.html#host-key-checking). @@ -115,9 +123,10 @@ N = 3 end ``` -**Caveats:** - -If you apply this parallel provisioning pattern with a static Ansible inventory, you will have to organize the things so that [all the relevant private keys are provided to the `ansible-playbook` command](https://github.com/mitchellh/vagrant/pull/5765#issuecomment-120247738). The same kind of considerations applies if you are using multiple private keys for a same machine (see [`config.ssh.private_key_path` SSH setting](/docs/vagrantfile/ssh_settings.html)). +
+ Tip: + If you apply this parallel provisioning pattern with a static Ansible inventory, you will have to organize the things so that [all the relevant private keys are provided to the `ansible-playbook` command](https://github.com/mitchellh/vagrant/pull/5765#issuecomment-120247738). The same kind of considerations applies if you are using multiple private keys for a same machine (see [`config.ssh.private_key_path` SSH setting](/docs/vagrantfile/ssh_settings.html)). +
### Force Paramiko Connection Mode diff --git a/website/source/docs/provisioning/ansible_common.html.md b/website/source/docs/provisioning/ansible_common.html.md index f6fb84f07..a60a1c8d8 100644 --- a/website/source/docs/provisioning/ansible_common.html.md +++ b/website/source/docs/provisioning/ansible_common.html.md @@ -17,6 +17,36 @@ These options get passed to the `ansible-playbook` command that ships with Ansib Some of these options are for advanced usage only and should not be used unless you understand their purpose. +- `become` (boolean) - Perform all the Ansible playbook tasks [as another user](http://docs.ansible.com/ansible/become.html), different from the user used to log into the guest system. + + The default value is `false`. + +- `become_user` (string) - Set the default username to be used by the Ansible `become` [privilege escalation](http://docs.ansible.com/ansible/become.html) mechanism. + + By default this option is not set, and the Ansible default value (`root`) will be used. + +- `compatibility_mode` (string) - Set the **minimal** version of Ansible to be supported. Vagrant will only use parameters that are compatible with the given version. + + Possible values: + + - `"auto"` _(Vagrant will automatically select the optimal compatibilty mode by checking the Ansible version currently available)_ + - `"1.8"` _(Ansible versions prior to 1.8 should mostly work well, but some options might not be supported)_ + - `"2.0"` _(The generated Ansible inventory will be incompatible with Ansible 1.x)_ + + By default this option is set to `"auto"`. If Vagrant is not able to detect any supported Ansible version, it will fall back on the compatibility mode `"1.8"` with a warning. + + Vagrant will error if the specified compatibility mode is incompatible with the current Ansible version. + +
+ Attention: + Vagrant doesn't perform any validation between the `compatibility_mode` value and the value of the [`version`](#version) option. +
+ +
+ Compatibility Note: + This option was introduced in Vagrant 2.0. The behavior of previous Vagrant versions can be simulated by setting the `compatibility_mode` to `"1.8"`. +
+ - `config_file` (string) - The path to an [Ansible Configuration file](https://docs.ansible.com/intro_configuration.html). By default, this option is not set, and Ansible will [search for a possible configuration file in some default locations](/docs/provisioning/ansible_intro.html#ANSIBLE_CONFIG). @@ -93,7 +123,7 @@ Some of these options are for advanced usage only and should not be used unless ansible.host_vars = { "host1" => {"http_port" => 80, "maxRequestsPerChild" => 808}, - "comments" => "'text with spaces'", + "comments" => "text with spaces", "host2" => {"http_port" => 303, "maxRequestsPerChild" => 909} } @@ -123,17 +153,28 @@ Some of these options are for advanced usage only and should not be used unless - `['--check', '-M', '/my/modules']` - `["--connection=paramiko", "--forks=10"]` - **Caveat:** The `ansible` provisioner does not support whitespace characters in `raw_arguments` elements. Therefore **don't write** something like `["-c paramiko"]`, which will result with an invalid `" parmiko"` parameter value. +
+ Attention: + The `ansible` provisioner does not support whitespace characters in `raw_arguments` elements. Therefore **don't write** something like `["-c paramiko"]`, which will result with an invalid `" parmiko"` parameter value. +
- `skip_tags` (string or array of strings) - Only plays, roles and tasks that [*do not match* these values will be executed](https://docs.ansible.com/playbooks_tags.html). - `start_at_task` (string) - The task name where the [playbook execution will start](https://docs.ansible.com/playbooks_startnstep.html#start-at-task). -- `sudo` (boolean) - Cause Ansible to perform all the playbook tasks [using sudo](https://docs.ansible.com/glossary.html#sudo). +- `sudo` (boolean) - Backwards compatible alias for the [`become`](#become) option. - The default value is `false`. +
+ Deprecation: + The `sudo` option is deprecated and will be removed in a future release. Please use the [**`become`**](#become) option instead. +
-- `sudo_user` (string) - set the default username who should be used by the sudo command. +- `sudo_user` (string) - Backwards compatible alias for the [`become_user`](#become_user) option. + +
+ Deprecation: + The `sudo_user` option is deprecated and will be removed in a future release. Please use the [**`become_user`**](#become_user) option instead. +
- `tags` (string or array of strings) - Only plays, roles and tasks [tagged with these values will be executed](https://docs.ansible.com/playbooks_tags.html) . @@ -146,3 +187,16 @@ Some of these options are for advanced usage only and should not be used unless Examples: `true` (equivalent to `v`), `-vvv` (equivalent to `vvv`), `vvvv`. Note that when the `verbose` option is enabled, the `ansible-playbook` command used by Vagrant will be displayed. + +- `version` (string) - The expected Ansible version. + + This option is disabled by default. + + When an Ansible version is defined (e.g. `"2.1.6.0"`), the Ansible provisioner will be executed only if Ansible is installed at the requested version. + + When this option is set to `"latest"`, no version check is applied. + +
+ Tip: + With the `ansible_local` provisioner, it is currently possible to use this option to specify which version of Ansible must be automatically installed, but only in combination with the [**`install_mode`**](/docs/provisioning/ansible_local.html#install_mode) set to `:pip`. +
diff --git a/website/source/docs/provisioning/ansible_local.html.md b/website/source/docs/provisioning/ansible_local.html.md index c506fbd7e..224b2179c 100644 --- a/website/source/docs/provisioning/ansible_local.html.md +++ b/website/source/docs/provisioning/ansible_local.html.md @@ -14,10 +14,8 @@ description: |- The Vagrant Ansible Local provisioner allows you to provision the guest using [Ansible](http://ansible.com) playbooks by executing **`ansible-playbook` directly on the guest machine**.
- Warning: If you are not familiar with Ansible and Vagrant already, - I recommend starting with the shell - provisioner. However, if you are comfortable with Vagrant already, Vagrant - is a great way to learn Ansible. + Warning: + If you are not familiar with Ansible and Vagrant already, I recommend starting with the shell provisioner. However, if you are comfortable with Vagrant already, Vagrant is a great way to learn Ansible.
## Setup Requirements @@ -64,10 +62,13 @@ This section lists the _specific_ options for the Ansible Local provisioner. In Vagrant will try to install (or upgrade) Ansible when one of these conditions are met: - Ansible is not installed (or cannot be found). - - The `version` option is set to `"latest"`. - - The current Ansible version does not correspond to the `version` option. + - The [`version`](/docs/provisioning/ansible_common.html#version) option is set to `"latest"`. + - The current Ansible version does not correspond to the [`version`](/docs/provisioning/ansible_common.html#version) option. - **Attention:** There is no guarantee that this automated installation will replace a custom Ansible setup, that might be already present on the Vagrant box. +
+ Attention: + There is no guarantee that this automated installation will replace a custom Ansible setup, that might be already present on the Vagrant box. +
- `install_mode` (`:default`, `:pip`, or `:pip_args_only`) - Select the way to automatically install Ansible on the guest system. @@ -75,7 +76,7 @@ This section lists the _specific_ options for the Ansible Local provisioner. In - On Ubuntu-like systems, the latest Ansible release is installed from the `ppa:ansible/ansible` repository. - On RedHat-like systems, the latest Ansible release is installed from the [EPEL](http://fedoraproject.org/wiki/EPEL) repository. - - `:pip`: Ansible is installed from [PyPI](https://pypi.python.org/pypi) with [pip](https://pip.pypa.io) package installer. With this mode, Vagrant will systematically try to [install the latest pip version](https://pip.pypa.io/en/stable/installing/#installing-with-get-pip-py). With the `:pip` mode you can optionally install a specific Ansible release by setting the [`version`](#version) option. + - `:pip`: Ansible is installed from [PyPI](https://pypi.python.org/pypi) with [pip](https://pip.pypa.io) package installer. With this mode, Vagrant will systematically try to [install the latest pip version](https://pip.pypa.io/en/stable/installing/#installing-with-get-pip-py). With the `:pip` mode you can optionally install a specific Ansible release by setting the [`version`](/docs/provisioning/ansible_common.html#version) option. Example: @@ -140,16 +141,6 @@ This section lists the _specific_ options for the Ansible Local provisioner. In The default value is `/tmp/vagrant-ansible` -- `version` (string) - The expected Ansible version. - - This option is disabled by default. - - When an Ansible version is defined (e.g. `"1.8.2"`), the Ansible local provisioner will be executed only if Ansible is installed at the requested version. - - When this option is set to `"latest"`, no version check is applied. - - **Warning:** It is currently possible to use this option to specify which version of Ansible must be automatically installed, but only in combination with the `install_mode` set to `:pip`. - ## Tips and Tricks ### Ansible Parallel Execution from a Guest diff --git a/website/source/docs/provisioning/file.html.md b/website/source/docs/provisioning/file.html.md index d88d03184..4442b3a9e 100644 --- a/website/source/docs/provisioning/file.html.md +++ b/website/source/docs/provisioning/file.html.md @@ -25,6 +25,42 @@ new VM. config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig" end +If you want to upload a folder to your guest system, it can be accomplished by +using a file provisioner seen below. When copied, the resulting folder on the guest will +replace `folder` as `newfolder` and place its on the guest machine. Note that if +you'd like the same folder name on your guest machine, make sure that the destination +path has the same name as the folder on your host. + + Vagrant.configure("2") do |config| + # ... other configuration + + config.vm.provision "file", source: "~/path/to/host/folder", destination: "$HOME/remote/newfolder" + end + +Prior to copying `~/path/to/host/folder` to the guest machine: + + folder + ├── script.sh + ├── otherfolder + │   └── hello.sh + ├── goodbye.sh + ├── hello.sh + └── woot.sh + + 1 directory, 5 files + +After to copying `~/path/to/host/folder` into `$HOME/remote/newfolder` to the guest machine: + + newfolder + ├── script.sh + ├── otherfolder + │   └── hello.sh + ├── goodbye.sh + ├── hello.sh + └── woot.sh + + 1 directory, 5 files + Note that, unlike with synced folders, files or directories that are uploaded will not be kept in sync. Continuing with the example above, if you make further changes to your local ~/.gitconfig, they will not be immediately @@ -49,3 +85,36 @@ The file provisioner takes only two options, both of which are required: the source will be uploaded to. The file/folder is uploaded as the SSH user over SCP, so this location must be writable to that user. The SSH user can be determined by running `vagrant ssh-config`, and defaults to "vagrant". + +## Caveats + +While the file provisioner does support trailing slashes or "globing", this can +lead to some confusing results due to the underlying tool used to copy files and +folders between the host and guests. For example, if you have a source and +destination with a trailing slash defined below: + + config.vm.provision "file", source: "~/pathfolder", destination: "/remote/newlocation/" + +You are telling vagrant to upload `~/pathfolder` under the remote dir `/remote/newlocation`, +which will look like: + + newlocation + ├── pathfolder + │   └── file.sh + + 1 directory, 2 files + +This behavior can also be achieved by defining your file provisioner below: + + config.vm.provision "file", source: "~/pathfolder", destination: "/remote/newlocation/pathfolder" + +Another example is using globing on the host machine to grab all files within a +folder, but not the top level folder itself: + + config.vm.provision "file", source: "~/otherfolder/.", destination: "/remote/otherlocation" + +The file provisioner is defined to include all files under `~/otherfolder` +to the new location `/remote/otherlocation`. This idea can be achieved by simply +having your destination folder differ from the source folder: + + config.vm.provision "file", source: "/otherfolder", destination: "/remote/otherlocation" diff --git a/website/source/docs/provisioning/salt.html.md b/website/source/docs/provisioning/salt.html.md index 17c95bb4e..09cd71707 100644 --- a/website/source/docs/provisioning/salt.html.md +++ b/website/source/docs/provisioning/salt.html.md @@ -96,6 +96,8 @@ public key * `masterless` (boolean) - Calls state.highstate in local mode. Uses `minion_id` and `pillar_data` when provided. +* `salt_call_args` (array) - An array of additional command line flag arguments to be passed to the `salt-call` command when provisioning with masterless. + ## Master Options These only make sense when `install_master` is `true`. Not supported on Windows guest machines. @@ -109,6 +111,8 @@ These only make sense when `install_master` is `true`. Not supported on Windows * `seed_master` (dictionary) - Upload keys to master, thereby pre-seeding it before use. Example: `{minion_name:/path/to/key.pub}` +* `salt_args` (array) - An array of additional command line flag arguments to be passed to the `salt` command when provisioning with masterless. + ## Execute States Either of the following may be used to actually execute states