From 5a7e00c5b1deffa3329c43c9c22186cd6d49991e Mon Sep 17 00:00:00 2001 From: Ray Ruvinskiy Date: Sun, 7 Sep 2014 23:45:32 -0400 Subject: [PATCH 001/484] Add HTTPS download options to `box update` and `box outdated` Vagrant::Box.load_metadata did not provide a way to specify the HTTPS download options that could be specified when downloading boxes (ca cert, ca path, client cert, insecure). As a result, while it was possible to add a box whose metadata file needed to be downloaded with one of those options specified, it was impossible to check for updates. The following changes have been made to address the situation: 1. Create a DownloadMixins module to provide the --insecure, --cacert, --capth, and --cert command line options to all of `vagrant box add`, `vagrant box update`, and `vagrant box outdated`. 2. Extend `Vagrant::Box.has_update?` and `Vagrant::Box.load_metadata` to accept said download options. 3. Extend `box outdated` and `box update` commands to pass download options down. 4. Extend `Vagrant::Builtin::Action::BoxCheckOutdated` to honour download options. 5. Options specified on the command line take precedence over options specified in the machine configuration, if any. 6. Fix bug in `vagrant box add` where client cert was being passed down using the wrong environment key. 7. Unit test coverage in update_test and box_check_outdated_test. Resolves #4420 --- .../action/builtin/box_check_outdated.rb | 12 +- lib/vagrant/box.rb | 9 +- plugins/commands/box/command/add.rb | 23 +-- .../commands/box/command/download_mixins.rb | 29 +++ plugins/commands/box/command/outdated.rb | 15 +- plugins/commands/box/command/update.rb | 35 +++- .../commands/box/command/update_test.rb | 184 ++++++++++++++---- .../action/builtin/box_check_outdated_test.rb | 37 +++- 8 files changed, 275 insertions(+), 69 deletions(-) create mode 100644 plugins/commands/box/command/download_mixins.rb diff --git a/lib/vagrant/action/builtin/box_check_outdated.rb b/lib/vagrant/action/builtin/box_check_outdated.rb index cee8c6877..d39afd272 100644 --- a/lib/vagrant/action/builtin/box_check_outdated.rb +++ b/lib/vagrant/action/builtin/box_check_outdated.rb @@ -37,13 +37,23 @@ module Vagrant end constraints = machine.config.vm.box_version + # Have download options specified in the environment override + # options specified for the machine. + download_options = { + ca_cert: env[:ca_cert] || machine.config.vm.box_download_ca_cert, + ca_path: env[:ca_path] || machine.config.vm.box_download_ca_path, + client_cert: env[:client_cert] || + machine.config.vm.box_download_client_cert, + insecure: !env[:insecure].nil? ? + env[:insecure] : machine.config.vm.box_download_insecure + } env[:ui].output(I18n.t( "vagrant.box_outdated_checking_with_refresh", name: box.name)) update = nil begin - update = box.has_update?(constraints) + update = box.has_update?(constraints, download_options: download_options) rescue Errors::BoxMetadataDownloadError => e env[:ui].warn(I18n.t( "vagrant.box_outdated_metadata_download_error", diff --git a/lib/vagrant/box.rb b/lib/vagrant/box.rb index 537c94e2a..045b22fd3 100644 --- a/lib/vagrant/box.rb +++ b/lib/vagrant/box.rb @@ -115,8 +115,9 @@ module Vagrant # Loads the metadata URL and returns the latest metadata associated # with this box. # + # @param [Hash] download_options Options to pass to the downloader. # @return [BoxMetadata] - def load_metadata + def load_metadata(**download_options) tf = Tempfile.new("vagrant") tf.close @@ -127,7 +128,7 @@ module Vagrant url = "file:#{url}" end - opts = { headers: ["Accept: application/json"] } + opts = { headers: ["Accept: application/json"] }.merge(download_options) Util::Downloader.new(url, tf.path, **opts).download! BoxMetadata.new(File.open(tf.path, "r")) rescue Errors::DownloaderError => e @@ -148,7 +149,7 @@ module Vagrant # satisfy. If nil, the version constrain defaults to being a # larger version than this box. # @return [Array] - def has_update?(version=nil) + def has_update?(version=nil, download_options: {}) if !@metadata_url raise Errors::BoxUpdateNoMetadata, name: @name end @@ -156,7 +157,7 @@ module Vagrant version += ", " if version version ||= "" version += "> #{@version}" - md = self.load_metadata + md = self.load_metadata(download_options) newer = md.version(version, provider: @provider) return nil if !newer diff --git a/plugins/commands/box/command/add.rb b/plugins/commands/box/command/add.rb index 0356ee298..e9d429433 100644 --- a/plugins/commands/box/command/add.rb +++ b/plugins/commands/box/command/add.rb @@ -1,9 +1,13 @@ require 'optparse' +require_relative 'download_mixins' + module VagrantPlugins module CommandBox module Command class Add < Vagrant.plugin("2", :command) + include DownloadMixins + def execute options = {} @@ -21,22 +25,7 @@ module VagrantPlugins options[:force] = f end - o.on("--insecure", "Do not validate SSL certificates") do |i| - options[:insecure] = i - end - - o.on("--cacert FILE", String, "CA certificate for SSL download") do |c| - options[:ca_cert] = c - end - - o.on("--capath DIR", String, "CA certificate directory for SSL download") do |c| - options[:ca_path] = c - end - - o.on("--cert FILE", String, - "A client SSL cert, if needed") do |c| - options[:client_cert] = c - end + build_download_options(o, options) o.on("--provider PROVIDER", String, "Provider the box should satisfy") do |p| options[:provider] = p @@ -93,7 +82,7 @@ module VagrantPlugins box_force: options[:force], box_download_ca_cert: options[:ca_cert], box_download_ca_path: options[:ca_path], - box_download_client_cert: options[:client_cert], + box_client_cert: options[:client_cert], box_download_insecure: options[:insecure], ui: Vagrant::UI::Prefixed.new(@env.ui, "box"), }) diff --git a/plugins/commands/box/command/download_mixins.rb b/plugins/commands/box/command/download_mixins.rb new file mode 100644 index 000000000..eda31b6bf --- /dev/null +++ b/plugins/commands/box/command/download_mixins.rb @@ -0,0 +1,29 @@ +module VagrantPlugins + module CommandBox + module DownloadMixins + # This adds common download command line flags to the given + # OptionParser, storing the result in the `options` dictionary. + # + # @param [OptionParser] parser + # @param [Hash] options + def build_download_options(parser, options) + # Add the options + parser.on("--insecure", "Do not validate SSL certificates") do |i| + options[:insecure] = i + end + + parser.on("--cacert FILE", String, "CA certificate for SSL download") do |c| + options[:ca_cert] = c + end + + parser.on("--capath DIR", String, "CA certificate directory for SSL download") do |c| + options[:ca_path] = c + end + + parser.on("--cert FILE", String, "A client SSL cert, if needed") do |c| + options[:client_cert] = c + end + end + end + end +end diff --git a/plugins/commands/box/command/outdated.rb b/plugins/commands/box/command/outdated.rb index a686fa496..cde932e1a 100644 --- a/plugins/commands/box/command/outdated.rb +++ b/plugins/commands/box/command/outdated.rb @@ -1,11 +1,16 @@ require 'optparse' +require_relative 'download_mixins' + module VagrantPlugins module CommandBox module Command class Outdated < Vagrant.plugin("2", :command) + include DownloadMixins + def execute options = {} + download_options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant box outdated [options]" @@ -20,6 +25,8 @@ module VagrantPlugins o.on("--global", "Check all boxes installed") do |g| options[:global] = g end + + build_download_options(o, download_options) end argv = parse_options(opts) @@ -27,7 +34,7 @@ module VagrantPlugins # If we're checking the boxes globally, then do that. if options[:global] - outdated_global + outdated_global(download_options) return 0 end @@ -37,11 +44,11 @@ module VagrantPlugins box_outdated_refresh: true, box_outdated_success_ui: true, machine: machine, - }) + }.merge(download_options)) end end - def outdated_global + def outdated_global(download_options) boxes = {} @env.boxes.all.reverse.each do |name, version, provider| next if boxes[name] @@ -58,7 +65,7 @@ module VagrantPlugins md = nil begin - md = box.load_metadata + md = box.load_metadata(download_options) rescue Vagrant::Errors::DownloaderError => e @env.ui.error(I18n.t( "vagrant.box_outdated_metadata_error", diff --git a/plugins/commands/box/command/update.rb b/plugins/commands/box/command/update.rb index 5633b0aaf..67d07bcc0 100644 --- a/plugins/commands/box/command/update.rb +++ b/plugins/commands/box/command/update.rb @@ -1,11 +1,16 @@ require 'optparse' +require_relative 'download_mixins' + module VagrantPlugins module CommandBox module Command class Update < Vagrant.plugin("2", :command) + include DownloadMixins + def execute options = {} + download_options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant box update [options]" @@ -27,21 +32,23 @@ module VagrantPlugins o.on("--provider PROVIDER", String, "Update box with specific provider") do |p| options[:provider] = p.to_sym end + + build_download_options(o, download_options) end argv = parse_options(opts) return if !argv if options[:box] - update_specific(options[:box], options[:provider]) + update_specific(options[:box], options[:provider], download_options) else - update_vms(argv, options[:provider]) + update_vms(argv, options[:provider], download_options) end 0 end - def update_specific(name, provider) + def update_specific(name, provider, download_options) boxes = {} @env.boxes.all.each do |n, v, p| boxes[n] ||= {} @@ -74,11 +81,11 @@ module VagrantPlugins to_update.each do |n, p, v| box = @env.boxes.find(n, p, v) - box_update(box, "> #{v}", @env.ui) + box_update(box, "> #{v}", @env.ui, download_options) end end - def update_vms(argv, provider) + def update_vms(argv, provider, download_options) with_target_vms(argv, provider: provider) do |machine| if !machine.config.vm.box machine.ui.output(I18n.t( @@ -95,17 +102,25 @@ module VagrantPlugins box = machine.box version = machine.config.vm.box_version - box_update(box, version, machine.ui) + # Get download options from machine configuration if not specified + # on the command line. + download_options[:ca_cert] ||= machine.config.vm.box_download_ca_cert + download_options[:ca_path] ||= machine.config.vm.box_download_ca_path + download_options[:client_cert] ||= machine.config.vm.box_download_client_cert + if download_options[:insecure].nil? + download_options[:insecure] = machine.config.vm.box_download_insecure + end + box_update(box, version, machine.ui, download_options) end end - def box_update(box, version, ui) + def box_update(box, version, ui, download_options) ui.output(I18n.t("vagrant.box_update_checking", name: box.name)) ui.detail("Latest installed version: #{box.version}") ui.detail("Version constraints: #{version}") ui.detail("Provider: #{box.provider}") - update = box.has_update?(version) + update = box.has_update?(version, download_options: download_options) if !update ui.success(I18n.t( "vagrant.box_up_to_date_single", @@ -124,6 +139,10 @@ module VagrantPlugins box_provider: update[2].name, box_version: update[1].version, ui: ui, + box_client_cert: download_options[:client_cert], + box_download_ca_cert: download_options[:ca_cert], + box_download_ca_path: download_options[:ca_path], + box_download_insecure: download_options[:insecure] }) end end diff --git a/test/unit/plugins/commands/box/command/update_test.rb b/test/unit/plugins/commands/box/command/update_test.rb index 4477d0aa5..067d0dd35 100644 --- a/test/unit/plugins/commands/box/command/update_test.rb +++ b/test/unit/plugins/commands/box/command/update_test.rb @@ -19,6 +19,11 @@ describe VagrantPlugins::CommandBox::Command::Update do let(:action_runner) { double("action_runner") } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + let(:download_options) { ["--insecure", + "--cacert", "foo", + "--capath", "bar", + "--cert", "baz"] } + subject { described_class.new(argv, iso_env) } before do @@ -86,6 +91,10 @@ describe VagrantPlugins::CommandBox::Command::Update do expect(opts[:box_url]).to eq(metadata_url.to_s) expect(opts[:box_provider]).to eq("virtualbox") expect(opts[:box_version]).to eq("1.1") + expect(opts[:box_download_ca_path]).to be_nil + expect(opts[:box_download_ca_cert]).to be_nil + expect(opts[:box_client_cert]).to be_nil + expect(opts[:box_download_insecure]).to be_nil end opts @@ -158,6 +167,50 @@ describe VagrantPlugins::CommandBox::Command::Update do end end + context "download options are specified" do + let(:argv) { ["--box", "foo" ].concat(download_options) } + + it "passes down download options" do + metadata_url.open("w") do |f| + f.write(<<-RAW) + { + "name": "foo", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.1", + "providers": [ + { + "name": "virtualbox", + "url": "bar" + } + ] + } + ] + } + RAW + end + + action_called = false + allow(action_runner).to receive(:run) do |action, opts| + if opts[:box_provider] + action_called = true + expect(opts[:box_download_ca_cert]).to eq("foo") + expect(opts[:box_download_ca_path]).to eq("bar") + expect(opts[:box_client_cert]).to eq("baz") + expect(opts[:box_download_insecure]).to be_true + end + + opts + end + + subject.execute + expect(action_called).to be_true + end + end + context "with a box that doesn't exist" do let(:argv) { ["--box", "nope"] } @@ -192,7 +245,10 @@ describe VagrantPlugins::CommandBox::Command::Update do it "doesn't update boxes if they're up-to-date" do machine.stub(box: box) expect(box).to receive(:has_update?). - with(machine.config.vm.box_version). + with(machine.config.vm.box_version, + {download_options: + {ca_cert: nil, ca_path: nil, client_cert: nil, + insecure: false}}). and_return(nil) expect(action_runner).to receive(:run).never @@ -200,41 +256,101 @@ describe VagrantPlugins::CommandBox::Command::Update do subject.execute end - it "updates boxes if they have an update" do - md = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) - { - "name": "foo", - "versions": [ - { - "version": "1.0" - }, - { - "version": "1.1", - "providers": [ - { - "name": "virtualbox", - "url": "bar" - } - ] - } - ] - } - RAW - - machine.stub(box: box) - expect(box).to receive(:has_update?). - with(machine.config.vm.box_version). - and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")]) - - expect(action_runner).to receive(:run).with { |action, opts| - expect(opts[:box_url]).to eq(box.metadata_url) - expect(opts[:box_provider]).to eq("virtualbox") - expect(opts[:box_version]).to eq("1.1") - expect(opts[:ui]).to equal(machine.ui) - true + context "boxes have an update" do + let(:md) { + md = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) + { + "name": "foo", + "versions": [ + { + "version": "1.0" + }, + { + "version": "1.1", + "providers": [ + { + "name": "virtualbox", + "url": "bar" + } + ] + } + ] + } + RAW } - subject.execute + before { machine.stub(box: box) } + + it "updates boxes" do + expect(box).to receive(:has_update?). + with(machine.config.vm.box_version, + {download_options: + {ca_cert: nil, ca_path: nil, client_cert: nil, + insecure: false}}). + and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")]) + + expect(action_runner).to receive(:run).with { |action, opts| + expect(opts[:box_url]).to eq(box.metadata_url) + expect(opts[:box_provider]).to eq("virtualbox") + expect(opts[:box_version]).to eq("1.1") + expect(opts[:ui]).to equal(machine.ui) + true + } + + subject.execute + end + + context "machine has download options" do + before do + machine.config.vm.box_download_ca_cert = "oof" + machine.config.vm.box_download_ca_path = "rab" + machine.config.vm.box_download_client_cert = "zab" + machine.config.vm.box_download_insecure = false + end + + it "uses download options from machine" do + expect(box).to receive(:has_update?). + with(machine.config.vm.box_version, + {download_options: + {ca_cert: "oof", ca_path: "rab", client_cert: "zab", + insecure: false}}). + and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")]) + + expect(action_runner).to receive(:run).with { |action, opts| + expect(opts[:box_download_ca_cert]).to eq("oof") + expect(opts[:box_download_ca_path]).to eq("rab") + expect(opts[:box_client_cert]).to eq("zab") + expect(opts[:box_download_insecure]).to be_false + true + } + + subject.execute + end + + context "download options are specified on the command line" do + let(:argv) { download_options } + + it "overrides download options from machine with options from CLI" do + expect(box).to receive(:has_update?). + with(machine.config.vm.box_version, + {download_options: + {ca_cert: "foo", ca_path: "bar", client_cert: "baz", + insecure: true}}). + and_return([md, md.version("1.1"), + md.version("1.1").provider("virtualbox")]) + + expect(action_runner).to receive(:run).with { |action, opts| + expect(opts[:box_download_ca_cert]).to eq("foo") + expect(opts[:box_download_ca_path]).to eq("bar") + expect(opts[:box_client_cert]).to eq("baz") + expect(opts[:box_download_insecure]).to be_true + true + } + + subject.execute + end + end + end end end end diff --git a/test/unit/vagrant/action/builtin/box_check_outdated_test.rb b/test/unit/vagrant/action/builtin/box_check_outdated_test.rb index bcb313d1c..0bb175c59 100644 --- a/test/unit/vagrant/action/builtin/box_check_outdated_test.rb +++ b/test/unit/vagrant/action/builtin/box_check_outdated_test.rb @@ -117,7 +117,9 @@ describe Vagrant::Action::Builtin::BoxCheckOutdated do } RAW - expect(box).to receive(:has_update?).with(machine.config.vm.box_version). + expect(box).to receive(:has_update?).with(machine.config.vm.box_version, + {download_options: + {ca_cert: nil, ca_path: nil, client_cert: nil, insecure: false}}). and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")]) expect(app).to receive(:call).with(env).once @@ -180,5 +182,38 @@ describe Vagrant::Action::Builtin::BoxCheckOutdated do expect { subject.call(env) }.to_not raise_error end + + context "when machine download options are specified" do + before do + machine.config.vm.box_download_ca_cert = "foo" + machine.config.vm.box_download_ca_path = "bar" + machine.config.vm.box_download_client_cert = "baz" + machine.config.vm.box_download_insecure = true + end + + it "uses download options from machine" do + expect(box).to receive(:has_update?).with(machine.config.vm.box_version, + {download_options: + {ca_cert: "foo", ca_path: "bar", client_cert: "baz", insecure: true}}) + + expect(app).to receive(:call).with(env).once + + subject.call(env) + end + + it "overrides download options from machine with options from env" do + expect(box).to receive(:has_update?).with(machine.config.vm.box_version, + {download_options: + {ca_cert: "oof", ca_path: "rab", client_cert: "zab", insecure: false}}) + + env[:ca_cert] = "oof" + env[:ca_path] = "rab" + env[:client_cert] = "zab" + env[:insecure] = false + expect(app).to receive(:call).with(env).once + + subject.call(env) + end + end end end From c20624bfdc71e1d9bbc85ca8bfbbbac0fd0bf2c1 Mon Sep 17 00:00:00 2001 From: mpoeter Date: Tue, 9 Sep 2014 19:17:04 +0200 Subject: [PATCH 002/484] Add support for linked clones for VirtualBox. --- lib/vagrant/errors.rb | 8 +++ plugins/providers/virtualbox/action.rb | 10 ++- .../virtualbox/action/create_clone.rb | 51 +++++++++++++++ .../virtualbox/action/import_master.rb | 62 +++++++++++++++++++ plugins/providers/virtualbox/config.rb | 10 +++ plugins/providers/virtualbox/driver/meta.rb | 2 + .../virtualbox/driver/version_4_3.rb | 25 ++++++-- templates/locales/en.yml | 8 +++ 8 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 plugins/providers/virtualbox/action/create_clone.rb create mode 100644 plugins/providers/virtualbox/action/import_master.rb diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 8995f6505..b4a609aa1 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -740,6 +740,14 @@ module Vagrant error_key(:boot_timeout) end + class VMCloneFailure < VagrantError + error_key(:failure, "vagrant.actions.vm.clone") + end + + class VMCreateMasterFailure < VagrantError + error_key(:failure, "vagrant.actions.vm.clone.create_master") + end + class VMCustomizationFailed < VagrantError error_key(:failure, "vagrant.actions.vm.customize") end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 668cbe0ef..b0051ce3d 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -12,6 +12,7 @@ module VagrantPlugins autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__) autoload :ClearForwardedPorts, File.expand_path("../action/clear_forwarded_ports", __FILE__) autoload :ClearNetworkInterfaces, File.expand_path("../action/clear_network_interfaces", __FILE__) + autoload :CreateClone, File.expand_path("../action/create_clone", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) autoload :Customize, File.expand_path("../action/customize", __FILE__) autoload :Destroy, File.expand_path("../action/destroy", __FILE__) @@ -21,6 +22,7 @@ module VagrantPlugins autoload :ForcedHalt, File.expand_path("../action/forced_halt", __FILE__) autoload :ForwardPorts, File.expand_path("../action/forward_ports", __FILE__) autoload :Import, File.expand_path("../action/import", __FILE__) + autoload :ImportMaster, File.expand_path("../action/import_master", __FILE__) autoload :IsPaused, File.expand_path("../action/is_paused", __FILE__) autoload :IsRunning, File.expand_path("../action/is_running", __FILE__) autoload :IsSaved, File.expand_path("../action/is_saved", __FILE__) @@ -313,7 +315,13 @@ module VagrantPlugins if !env[:result] b2.use CheckAccessible b2.use Customize, "pre-import" - b2.use Import + + if env[:machine].provider_config.use_linked_clone + b2.use ImportMaster + b2.use CreateClone + else + b2.use Import + end b2.use MatchMACAddress end end diff --git a/plugins/providers/virtualbox/action/create_clone.rb b/plugins/providers/virtualbox/action/create_clone.rb new file mode 100644 index 000000000..e26b5721f --- /dev/null +++ b/plugins/providers/virtualbox/action/create_clone.rb @@ -0,0 +1,51 @@ +require "log4r" +#require "lockfile" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class CreateClone + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::vm::clone") + end + + def call(env) + @logger.info("Creating linked clone from master '#{env[:master_id]}'") + + env[:ui].info I18n.t("vagrant.actions.vm.clone.creating", name: env[:machine].box.name) + env[:machine].id = env[:machine].provider.driver.clonevm(env[:master_id], env[:machine].box.name, "base") do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + # Clear the line one last time since the progress meter doesn't disappear immediately. + env[:ui].clear_line + + # Flag as erroneous and return if clone failed + raise Vagrant::Errors::VMCloneFailure if !env[:machine].id + + # Continue + @app.call(env) + end + + def recover(env) + if env[:machine].state.id != :not_created + return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError) + + # If we're not supposed to destroy on error then just return + return if !env[:destroy_on_error] + + # Interrupted, destroy the VM. We note that we don't want to + # validate the configuration here, and we don't want to confirm + # we want to destroy. + destroy_env = env.clone + destroy_env[:config_validate] = false + destroy_env[:force_confirm_destroy] = true + env[:action_runner].run(Action.action_destroy, destroy_env) + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/import_master.rb b/plugins/providers/virtualbox/action/import_master.rb new file mode 100644 index 000000000..090b769c4 --- /dev/null +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -0,0 +1,62 @@ +require "log4r" +#require "lockfile" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class ImportMaster + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::vm::create_master") + end + + def call(env) + master_id_file = env[:machine].box.directory.join("master_id") + + env[:master_id] = master_id_file.read.chomp if master_id_file.file? + if env[:master_id] && env[:machine].provider.driver.vm_exists?(env[:master_id]) + # Master VM already exists -> nothing to do - continue. + @app.call(env) + end + + env[:ui].info I18n.t("vagrant.actions.vm.clone.importing", name: env[:machine].box.name) + + #TODO - prevent concurrent creation of master vms for the same box. + + # Import the virtual machine + ovf_file = env[:machine].box.directory.join("box.ovf").to_s + env[:master_id] = env[:machine].provider.driver.import(ovf_file) do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + # Clear the line one last time since the progress meter doesn't disappear immediately. + env[:ui].clear_line + + # Flag as erroneous and return if import failed + raise Vagrant::Errors::VMImportFailure if !env[:master_id] + + @logger.info("Imported box #{env[:machine].box.name} as master vm with id #{env[:master_id]}") + + @logger.info("Creating base snapshot for master VM.") + env[:machine].provider.driver.create_snapshot(env[:master_id], "base")do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + @logger.debug("Writing id of master VM '#{env[:master_id]}' to #{master_id_file}") + master_id_file.open("w+") do |f| + f.write(env[:master_id]) + end + + # If we got interrupted, then the import could have been + # interrupted and its not a big deal. Just return out. + return if env[:interrupted] + + # Import completed successfully. Continue the chain + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/config.rb b/plugins/providers/virtualbox/config.rb index aed1c692c..7a0e08d08 100644 --- a/plugins/providers/virtualbox/config.rb +++ b/plugins/providers/virtualbox/config.rb @@ -32,6 +32,12 @@ module VagrantPlugins # @return [Boolean] attr_accessor :gui + # If set to `true`, then a linked clone is created from a master + # VM generated from the specified box. + # + # @return [Boolean] + attr_accessor :use_linked_clone + # This should be set to the name of the machine in the VirtualBox # GUI. # @@ -59,6 +65,7 @@ module VagrantPlugins @name = UNSET_VALUE @network_adapters = {} @gui = UNSET_VALUE + @use_linked_clone = UNSET_VALUE # We require that network adapter 1 is a NAT device. network_adapter(1, :nat) @@ -136,6 +143,9 @@ module VagrantPlugins # Default is to not show a GUI @gui = false if @gui == UNSET_VALUE + # Do not create linked clone by default + @use_linked_clone = false if @use_linked_clone == UNSET_VALUE + # The default name is just nothing, and we default it @name = nil if @name == UNSET_VALUE end diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index 1da74d5d3..5f0700c32 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -79,8 +79,10 @@ module VagrantPlugins def_delegators :@driver, :clear_forwarded_ports, :clear_shared_folders, + :clonevm, :create_dhcp_server, :create_host_only_network, + :create_snapshot, :delete, :delete_unused_host_only_networks, :discard_saved_state, diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index cc700c0fa..76ccdcd6a 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -34,6 +34,15 @@ module VagrantPlugins end end + def clonevm(master_id, box_name, snapshot_name) + @logger.debug("Creating linked clone from master vm with id #{master_id} from snapshot '#{snapshot_name}'") + + machine_name = "#{box_name}_#{snapshot_name}_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" + execute("clonevm", master_id, "--snapshot", snapshot_name, "--options", "link", "--register", "--name", machine_name) + + return get_machine_id machine_name + end + def create_dhcp_server(network, options) execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], @@ -62,6 +71,10 @@ module VagrantPlugins } end + def create_snapshot(machine_id, snapshot_name) + execute("snapshot", machine_id, "take", snapshot_name) + end + def delete execute("unregistervm", @uuid, "--delete") end @@ -156,6 +169,13 @@ module VagrantPlugins execute("modifyvm", @uuid, *args) if !args.empty? end + def get_machine_id(machine_name) + output = execute("list", "vms", retryable: true) + match = /^"#{Regexp.escape(machine_name)}" \{(.+?)\}$/.match(output) + return match[1].to_s if match + nil + end + def halt execute("controlvm", @uuid, "poweroff") end @@ -231,10 +251,7 @@ module VagrantPlugins end end - output = execute("list", "vms", retryable: true) - match = /^"#{Regexp.escape(specified_name)}" \{(.+?)\}$/.match(output) - return match[1].to_s if match - nil + return get_machine_id specified_name end def max_network_adapters diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 4be7a92e7..4cbf5a0a3 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1482,6 +1482,14 @@ en: deleting: Clearing any previously set network interfaces... clear_shared_folders: deleting: Cleaning previously set shared folders... + clone: + importing: Importing box '%{name}' as master vm... + creating: Creating linked clone... + failure: Creation of the linked clone failed. + + create_master: + failure: |- + Failed to create lock-file for master VM creation for box %{box}. customize: failure: |- A customization command failed: From 14b84a4a76dc3bc3e7693b2388b37072370169b6 Mon Sep 17 00:00:00 2001 From: Rob Kinyon Date: Tue, 28 Oct 2014 21:53:41 -0400 Subject: [PATCH 003/484] Added a --plugin-clean-sources parameter that will allow for only those sources that are defined by the user to be used. --- .gitignore | 4 ++++ lib/vagrant/bundler.rb | 5 ----- plugins/commands/plugin/command/mixin_install_opts.rb | 11 ++++++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 47b95a67e..8fe665031 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # OS-specific .DS_Store +# Editor swapfiles +.*.sw? +*~ + # Vagrant stuff acceptance_config.yml boxes/* diff --git a/lib/vagrant/bundler.rb b/lib/vagrant/bundler.rb index 05867da15..e12b89174 100644 --- a/lib/vagrant/bundler.rb +++ b/lib/vagrant/bundler.rb @@ -178,11 +178,6 @@ module Vagrant f = File.open(Tempfile.new("vagrant").path + "2", "w+") f.tap do |gemfile| - if !sources.include?("http://rubygems.org") - gemfile.puts(%Q[source "https://rubygems.org"]) - end - - gemfile.puts(%Q[source "http://gems.hashicorp.com"]) sources.each do |source| next if source == "" gemfile.puts(%Q[source "#{source}"]) diff --git a/plugins/commands/plugin/command/mixin_install_opts.rb b/plugins/commands/plugin/command/mixin_install_opts.rb index 0b1b0973a..ffee55cde 100644 --- a/plugins/commands/plugin/command/mixin_install_opts.rb +++ b/plugins/commands/plugin/command/mixin_install_opts.rb @@ -3,6 +3,11 @@ module VagrantPlugins module Command module MixinInstallOpts def build_install_opts(o, options) + options[:plugin_sources] = [ + "https://rubygems.org", + "http://gems.hashicorp.com", + ] + o.on("--entry-point NAME", String, "The name of the entry point file for loading the plugin.") do |entry_point| options[:entry_point] = entry_point @@ -17,9 +22,13 @@ module VagrantPlugins puts end + o.on("--plugin-clean-sources", String, + "Remove all plugin sources defined so far (including defaults)") do + options[:plugin_sources] = [] + end + o.on("--plugin-source PLUGIN_SOURCE", String, "Add a RubyGems repository source") do |plugin_source| - options[:plugin_sources] ||= [] options[:plugin_sources] << plugin_source end From c3151c0b8829582873dfb355f0ca5a2edcb75208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=B6ter?= Date: Wed, 3 Jun 2015 13:27:03 +0200 Subject: [PATCH 004/484] Ignore failure when trying to delete lock file. Deleting the lock file can fail when another process is currently trying to acquire it (-> race condition). It is safe to ignore this error since the other process will eventually acquire the lock and again try to delete the lock file. --- lib/vagrant/environment.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 571ae75ab..455921485 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -484,7 +484,11 @@ module Vagrant if name != "dotlock" lock("dotlock", retry: true) do f.close - File.delete(lock_path) + begin + File.delete(lock_path) + rescue + @logger.debug("Failed to delete lock file #{lock_path} - some other thread might be trying to acquire it -> ignoring this error") + end end end From 9d63ca4dd237947756ea59b447013e78313f4c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=B6ter?= Date: Wed, 3 Jun 2015 13:31:43 +0200 Subject: [PATCH 005/484] Acquire lock to prevent concurrent creation of master VM for the same box. --- .../virtualbox/action/import_master.rb | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/plugins/providers/virtualbox/action/import_master.rb b/plugins/providers/virtualbox/action/import_master.rb index 090b769c4..cba6216fd 100644 --- a/plugins/providers/virtualbox/action/import_master.rb +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -12,46 +12,50 @@ module VagrantPlugins def call(env) master_id_file = env[:machine].box.directory.join("master_id") - - env[:master_id] = master_id_file.read.chomp if master_id_file.file? - if env[:master_id] && env[:machine].provider.driver.vm_exists?(env[:master_id]) - # Master VM already exists -> nothing to do - continue. - @app.call(env) - end - env[:ui].info I18n.t("vagrant.actions.vm.clone.importing", name: env[:machine].box.name) + env[:machine].env.lock(env[:machine].box.name, retry: true) do + env[:master_id] = master_id_file.read.chomp if master_id_file.file? + if env[:master_id] && env[:machine].provider.driver.vm_exists?(env[:master_id]) + # Master VM already exists -> nothing to do - continue. + @logger.info("Master VM for '#{env[:machine].box.name}' already exists (id=#{env[:master_id]}) - skipping import step.") + return @app.call(env) + end - #TODO - prevent concurrent creation of master vms for the same box. - - # Import the virtual machine - ovf_file = env[:machine].box.directory.join("box.ovf").to_s - env[:master_id] = env[:machine].provider.driver.import(ovf_file) do |progress| + env[:ui].info I18n.t("vagrant.actions.vm.clone.importing", name: env[:machine].box.name) + + # Import the virtual machine + ovf_file = env[:machine].box.directory.join("box.ovf").to_s + env[:master_id] = env[:machine].provider.driver.import(ovf_file) do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + # Clear the line one last time since the progress meter doesn't disappear immediately. env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - - # Clear the line one last time since the progress meter doesn't disappear immediately. - env[:ui].clear_line + + # Flag as erroneous and return if import failed + raise Vagrant::Errors::VMImportFailure if !env[:master_id] - # Flag as erroneous and return if import failed - raise Vagrant::Errors::VMImportFailure if !env[:master_id] - - @logger.info("Imported box #{env[:machine].box.name} as master vm with id #{env[:master_id]}") - - @logger.info("Creating base snapshot for master VM.") - env[:machine].provider.driver.create_snapshot(env[:master_id], "base")do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end + @logger.info("Imported box #{env[:machine].box.name} as master vm with id #{env[:master_id]}") - @logger.debug("Writing id of master VM '#{env[:master_id]}' to #{master_id_file}") - master_id_file.open("w+") do |f| - f.write(env[:master_id]) - end + @logger.info("Creating base snapshot for master VM.") + env[:machine].provider.driver.create_snapshot(env[:master_id], "base") do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + @logger.debug("Writing id of master VM '#{env[:master_id]}' to #{master_id_file}") + master_id_file.open("w+") do |f| + f.write(env[:master_id]) + end + end # If we got interrupted, then the import could have been # interrupted and its not a big deal. Just return out. - return if env[:interrupted] + if env[:interrupted] + @logger.info("Import of master VM was interrupted -> exiting.") + return + end # Import completed successfully. Continue the chain @app.call(env) From c60a020096e576243e703517511d886fd461a925 Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Wed, 27 Aug 2014 12:17:30 -0700 Subject: [PATCH 006/484] adds a ps command to vagrant that drops the user into a remote powershell shell --- plugins/commands/ps/command.rb | 116 ++++++++++++++++++ plugins/commands/ps/errors.rb | 18 +++ plugins/commands/ps/plugin.rb | 31 +++++ .../commands/ps/scripts/enable_psremoting.ps1 | 60 +++++++++ .../ps/scripts/reset_trustedhosts.ps1 | 12 ++ plugins/hosts/windows/cap/ps.rb | 50 ++++++++ plugins/hosts/windows/plugin.rb | 5 + templates/locales/command_ps.yml | 16 +++ 8 files changed, 308 insertions(+) create mode 100644 plugins/commands/ps/command.rb create mode 100644 plugins/commands/ps/errors.rb create mode 100644 plugins/commands/ps/plugin.rb create mode 100644 plugins/commands/ps/scripts/enable_psremoting.ps1 create mode 100644 plugins/commands/ps/scripts/reset_trustedhosts.ps1 create mode 100644 plugins/hosts/windows/cap/ps.rb create mode 100644 templates/locales/command_ps.yml diff --git a/plugins/commands/ps/command.rb b/plugins/commands/ps/command.rb new file mode 100644 index 000000000..33eeec253 --- /dev/null +++ b/plugins/commands/ps/command.rb @@ -0,0 +1,116 @@ +require "optparse" + +require_relative "../../communicators/winrm/helper" + +module VagrantPlugins + module CommandPS + class Command < Vagrant.plugin("2", :command) + def self.synopsis + "connects to machine via powershell remoting" + end + + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant ps [-- extra ps args]" + + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("-c", "--command COMMAND", "Execute a powershell command directly") do |c| + options[:command] = c + end + end + + # Parse out the extra args to send to the ps session, which + # is everything after the "--" + split_index = @argv.index("--") + if split_index + options[:extra_args] = @argv.drop(split_index + 1) + @argv = @argv.take(split_index) + end + + # Parse the options and return if we don't have any target. + argv = parse_options(opts) + return if !argv + + # Check if the host even supports ps remoting + raise Errors::HostUnsupported if !@env.host.capability?(:ps_client) + + # Execute ps session if we can + with_target_vms(argv, single_target: true) do |machine| + if !machine.communicate.ready? + raise Vagrant::Errors::VMNotCreatedError + end + + if machine.config.vm.communicator != :winrm #|| !machine.provider.capability?(:winrm_info) + raise VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady + end + + if !options[:command].nil? + out_code = machine.communicate.execute options[:command] + if out_code == 0 + machine.ui.detail("Command: #{options[:command]} executed succesfully with output code #{out_code}.") + end + break + end + + ps_info = VagrantPlugins::CommunicatorWinRM::Helper.winrm_info(machine) + ps_info[:username] = machine.config.winrm.username + ps_info[:password] = machine.config.winrm.password + # Extra arguments if we have any + ps_info[:extra_args] = options[:extra_args] + + result = ready_ps_remoting_for machine, ps_info + + machine.ui.detail( + "Creating powershell session to #{ps_info[:host]}:#{ps_info[:port]}") + machine.ui.detail("Username: #{ps_info[:username]}") + + begin + @env.host.capability(:ps_client, ps_info) + ensure + if !result["PreviousTrustedHosts"].nil? + reset_ps_remoting_for machine, ps_info + end + end + end + end + + def ready_ps_remoting_for(machine, ps_info) + machine.ui.output(I18n.t("vagrant_ps.detecting")) + script_path = File.expand_path("../scripts/enable_psremoting.ps1", __FILE__) + args = [] + args << "-hostname" << ps_info[:host] + args << "-port" << ps_info[:port].to_s + args << "-username" << ps_info[:username] + args << "-password" << ps_info[:password] + result = Vagrant::Util::PowerShell.execute(script_path, *args) + if result.exit_code != 0 + raise Errors::PowershellError, + script: script_path, + stderr: result.stderr + end + + result_output = JSON.parse(result.stdout) + raise Errors::PSRemotingUndetected if !result_output["Success"] + result_output + end + + def reset_ps_remoting_for(machine, ps_info) + machine.ui.output(I18n.t("vagrant_ps.reseting")) + script_path = File.expand_path("../scripts/reset_trustedhosts.ps1", __FILE__) + args = [] + args << "-hostname" << ps_info[:host] + result = Vagrant::Util::PowerShell.execute(script_path, *args) + if result.exit_code != 0 + raise Errors::PowershellError, + script: script_path, + stderr: result.stderr + end + end + end + end +end diff --git a/plugins/commands/ps/errors.rb b/plugins/commands/ps/errors.rb new file mode 100644 index 000000000..c614fb87a --- /dev/null +++ b/plugins/commands/ps/errors.rb @@ -0,0 +1,18 @@ +module VagrantPlugins + module CommandPS + module Errors + # A convenient superclass for all our errors. + class PSError < Vagrant::Errors::VagrantError + error_namespace("vagrant_ps.errors") + end + + class HostUnsupported < PSError + error_key(:host_unsupported) + end + + class PSRemotingUndetected < PSError + error_key(:ps_remoting_undetected) + end + end + end +end diff --git a/plugins/commands/ps/plugin.rb b/plugins/commands/ps/plugin.rb new file mode 100644 index 000000000..56b84b3c5 --- /dev/null +++ b/plugins/commands/ps/plugin.rb @@ -0,0 +1,31 @@ +require "vagrant" + +module VagrantPlugins + module CommandPS + autoload :Errors, File.expand_path("../errors", __FILE__) + + class Plugin < Vagrant.plugin("2") + name "ps command" + description <<-DESC + The ps command opens a remote powershell session to the + machine if it supports powershell remoting. + DESC + + command("ps") do + require File.expand_path("../command", __FILE__) + init! + Command + end + + protected + + def self.init! + return if defined?(@_init) + I18n.load_path << File.expand_path( + "templates/locales/command_ps.yml", Vagrant.source_root) + I18n.reload! + @_init = true + end + end + end +end diff --git a/plugins/commands/ps/scripts/enable_psremoting.ps1 b/plugins/commands/ps/scripts/enable_psremoting.ps1 new file mode 100644 index 000000000..4d7ded704 --- /dev/null +++ b/plugins/commands/ps/scripts/enable_psremoting.ps1 @@ -0,0 +1,60 @@ +Param( + [string]$hostname, + [string]$port, + [string]$username, + [string]$password +) +# If we are in this script, we know basic winrm is working +# If the user is not using a domain acount and chances are +# they are not, PS Remoting will not work if the guest is not +# listed in the trusted hosts. + +$encrypted_password = ConvertTo-SecureString $password -asplaintext -force +$creds = New-Object System.Management.Automation.PSCredential ( + "$hostname\\$username", $encrypted_password) + +$result = @{ + Success = $false + PreviousTrustedHosts = $null +} +try { + invoke-command -computername $hostname ` + -Credential $creds ` + -Port $port ` + -ScriptBlock {} ` + -ErrorAction Stop + $result.Success = $true +} catch{} + +if(!$result.Success) { + $newHosts = @() + $result.PreviousTrustedHosts=( + Get-Item "wsman:\localhost\client\trustedhosts").Value + $hostArray=$result.PreviousTrustedHosts.Split(",").Trim() + if($hostArray -contains "*") { + $result.PreviousTrustedHosts = $null + } + elseif(!($hostArray -contains $hostname)) { + $strNewHosts = $hostname + if($result.PreviousTrustedHosts.Length -gt 0){ + $strNewHosts = $result.PreviousTrustedHosts + "," + $strNewHosts + } + Set-Item -Path "wsman:\localhost\client\trustedhosts" ` + -Value $strNewHosts -Force + + try { + invoke-command -computername $hostname ` + -Credential $creds ` + -Port $port ` + -ScriptBlock {} ` + -ErrorAction Stop + $result.Success = $true + } catch{ + Set-Item -Path "wsman:\localhost\client\trustedhosts" ` + -Value $result.PreviousTrustedHosts -Force + $result.PreviousTrustedHosts = $null + } + } +} + +Write-Output $(ConvertTo-Json $result) diff --git a/plugins/commands/ps/scripts/reset_trustedhosts.ps1 b/plugins/commands/ps/scripts/reset_trustedhosts.ps1 new file mode 100644 index 000000000..5865a4e77 --- /dev/null +++ b/plugins/commands/ps/scripts/reset_trustedhosts.ps1 @@ -0,0 +1,12 @@ +Param( + [string]$hostname +) + +$trustedHosts = ( + Get-Item "wsman:\localhost\client\trustedhosts").Value.Replace( + $hostname, '') +$trustedHosts = $trustedHosts.Replace(",,","") +if($trustedHosts.EndsWith(",")){ + $trustedHosts = $trustedHosts.Substring(0,$trustedHosts.length-1) +} +Set-Item "wsman:\localhost\client\trustedhosts" -Value $trustedHosts -Force \ No newline at end of file diff --git a/plugins/hosts/windows/cap/ps.rb b/plugins/hosts/windows/cap/ps.rb new file mode 100644 index 000000000..939235915 --- /dev/null +++ b/plugins/hosts/windows/cap/ps.rb @@ -0,0 +1,50 @@ +require "pathname" +require "tmpdir" + +require "vagrant/util/subprocess" + +module VagrantPlugins + module HostWindows + module Cap + class PS + def self.ps_client(env, ps_info) + logger = Log4r::Logger.new("vagrant::hosts::windows") + + command = <<-EOS + $plain_password = "#{ps_info[:password]}" + $username = "#{ps_info[:username]}" + $port = "#{ps_info[:port]}" + $hostname = "#{ps_info[:host]}" + $password = ConvertTo-SecureString $plain_password -asplaintext -force + $creds = New-Object System.Management.Automation.PSCredential ("$hostname\\$username", $password) + function prompt { kill $PID } + Enter-PSSession -ComputerName $hostname -Credential $creds -Port $port + EOS + + logger.debug("Starting remote powershell with command:\n#{command}") + command = command.chars.to_a.join("\x00").chomp + command << "\x00" unless command[-1].eql? "\x00" + if(defined?(command.encode)) + command = command.encode('ASCII-8BIT') + command = Base64.strict_encode64(command) + else + command = Base64.encode64(command).chomp + end + + args = ["-NoProfile"] + args << "-ExecutionPolicy" + args << "Bypass" + args << "-NoExit" + args << "-EncodedCommand" + args << command + if ps_info[:extra_args] + args << ps_info[:extra_args] + end + + # Launch it + Vagrant::Util::Subprocess.execute("powershell", *args) + end + end + end + end +end diff --git a/plugins/hosts/windows/plugin.rb b/plugins/hosts/windows/plugin.rb index fe3b28923..97e8ad134 100644 --- a/plugins/hosts/windows/plugin.rb +++ b/plugins/hosts/windows/plugin.rb @@ -20,6 +20,11 @@ module VagrantPlugins require_relative "cap/rdp" Cap::RDP end + + host_capability("windows", "ps_client") do + require_relative "cap/ps" + Cap::PS + end end end end diff --git a/templates/locales/command_ps.yml b/templates/locales/command_ps.yml new file mode 100644 index 000000000..e30e5246c --- /dev/null +++ b/templates/locales/command_ps.yml @@ -0,0 +1,16 @@ +en: + vagrant_ps: + detecting: |- + Detecting if a remote powershell connection can be made with the guest... + reseting: |- + Reseting WinRM TrustedHosts to their original value. + + errors: + host_unsupported: |- + Your host does not support powershell. A remote powershell connection + can only be made from a windows host. + + ps_remoting_undetected: |- + Unable to establish a remote powershell connection with the guest. + Check if the firewall rules on the guest allow connections to the + windows remote management service. From 1cd10330933fce64608ce2d47bbb72ad0756533f Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 00:11:06 -0700 Subject: [PATCH 007/484] fixes from @sethvargo comments. --- plugins/commands/ps/command.rb | 10 +++++----- plugins/commands/ps/plugin.rb | 7 +++---- plugins/communicators/winrm/shell.rb | 2 +- templates/locales/command_ps.yml | 8 ++++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/plugins/commands/ps/command.rb b/plugins/commands/ps/command.rb index 33eeec253..3bcc41ba8 100644 --- a/plugins/commands/ps/command.rb +++ b/plugins/commands/ps/command.rb @@ -45,16 +45,16 @@ module VagrantPlugins raise Vagrant::Errors::VMNotCreatedError end - if machine.config.vm.communicator != :winrm #|| !machine.provider.capability?(:winrm_info) + if machine.config.vm.communicator != :winrm raise VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady end if !options[:command].nil? - out_code = machine.communicate.execute options[:command] + out_code = machine.communicate.execute(options[:command]) if out_code == 0 machine.ui.detail("Command: #{options[:command]} executed succesfully with output code #{out_code}.") end - break + next end ps_info = VagrantPlugins::CommunicatorWinRM::Helper.winrm_info(machine) @@ -63,7 +63,7 @@ module VagrantPlugins # Extra arguments if we have any ps_info[:extra_args] = options[:extra_args] - result = ready_ps_remoting_for machine, ps_info + result = ready_ps_remoting_for(machine, ps_info) machine.ui.detail( "Creating powershell session to #{ps_info[:host]}:#{ps_info[:port]}") @@ -73,7 +73,7 @@ module VagrantPlugins @env.host.capability(:ps_client, ps_info) ensure if !result["PreviousTrustedHosts"].nil? - reset_ps_remoting_for machine, ps_info + reset_ps_remoting_for(machine, ps_info) end end end diff --git a/plugins/commands/ps/plugin.rb b/plugins/commands/ps/plugin.rb index 56b84b3c5..847b82c47 100644 --- a/plugins/commands/ps/plugin.rb +++ b/plugins/commands/ps/plugin.rb @@ -7,12 +7,12 @@ module VagrantPlugins class Plugin < Vagrant.plugin("2") name "ps command" description <<-DESC - The ps command opens a remote powershell session to the + The ps command opens a remote PowerShell session to the machine if it supports powershell remoting. DESC command("ps") do - require File.expand_path("../command", __FILE__) + require_relative "../command" init! Command end @@ -21,8 +21,7 @@ module VagrantPlugins def self.init! return if defined?(@_init) - I18n.load_path << File.expand_path( - "templates/locales/command_ps.yml", Vagrant.source_root) + I18n.load_path << File.expand_path("templates/locales/command_ps.yml", Vagrant.source_root) I18n.reload! @_init = true end diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index d9d5f28ce..784660b5e 100644 --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -9,7 +9,7 @@ Vagrant::Util::SilenceWarnings.silence! do require "winrm" end -require "winrm-fs/file_manager" +require "winrm-fs" module VagrantPlugins module CommunicatorWinRM diff --git a/templates/locales/command_ps.yml b/templates/locales/command_ps.yml index e30e5246c..51bf666cc 100644 --- a/templates/locales/command_ps.yml +++ b/templates/locales/command_ps.yml @@ -1,16 +1,16 @@ en: vagrant_ps: detecting: |- - Detecting if a remote powershell connection can be made with the guest... + Detecting if a remote PowerShell connection can be made with the guest... reseting: |- Reseting WinRM TrustedHosts to their original value. errors: host_unsupported: |- - Your host does not support powershell. A remote powershell connection + Your host does not support PowerShell. A remote PowerShell connection can only be made from a windows host. ps_remoting_undetected: |- - Unable to establish a remote powershell connection with the guest. + Unable to establish a remote PowerShell connection with the guest. Check if the firewall rules on the guest allow connections to the - windows remote management service. + Windows remote management service. From 47e57a7cd941c1a2f24e287ab175b9728d5e042b Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 04:17:56 -0700 Subject: [PATCH 008/484] fix relative path --- plugins/commands/ps/plugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/commands/ps/plugin.rb b/plugins/commands/ps/plugin.rb index 847b82c47..b108658bc 100644 --- a/plugins/commands/ps/plugin.rb +++ b/plugins/commands/ps/plugin.rb @@ -12,7 +12,7 @@ module VagrantPlugins DESC command("ps") do - require_relative "../command" + require_relative "command" init! Command end From cf6d4ef5a1943ec656695440d452b160aa74f676 Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 04:18:23 -0700 Subject: [PATCH 009/484] clean up command encoding --- plugins/hosts/windows/cap/ps.rb | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/plugins/hosts/windows/cap/ps.rb b/plugins/hosts/windows/cap/ps.rb index 939235915..9960f689e 100644 --- a/plugins/hosts/windows/cap/ps.rb +++ b/plugins/hosts/windows/cap/ps.rb @@ -22,21 +22,13 @@ module VagrantPlugins EOS logger.debug("Starting remote powershell with command:\n#{command}") - command = command.chars.to_a.join("\x00").chomp - command << "\x00" unless command[-1].eql? "\x00" - if(defined?(command.encode)) - command = command.encode('ASCII-8BIT') - command = Base64.strict_encode64(command) - else - command = Base64.encode64(command).chomp - end args = ["-NoProfile"] args << "-ExecutionPolicy" args << "Bypass" args << "-NoExit" args << "-EncodedCommand" - args << command + args << ::WinRM::PowershellScript.new(command).encoded if ps_info[:extra_args] args << ps_info[:extra_args] end From 740877065a8fc0b777cc7aa51b4fb074307affbd Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 05:03:29 -0700 Subject: [PATCH 010/484] marshall back command output when passing a command to ps --- plugins/commands/ps/command.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/commands/ps/command.rb b/plugins/commands/ps/command.rb index 3bcc41ba8..ee021d0ed 100644 --- a/plugins/commands/ps/command.rb +++ b/plugins/commands/ps/command.rb @@ -50,9 +50,11 @@ module VagrantPlugins end if !options[:command].nil? - out_code = machine.communicate.execute(options[:command]) + out_code = machine.communicate.execute(options[:command].dup) do |type,data| + machine.ui.detail(data) if type == :stdout + end if out_code == 0 - machine.ui.detail("Command: #{options[:command]} executed succesfully with output code #{out_code}.") + machine.ui.success("Command: #{options[:command]} executed succesfully with output code #{out_code}.") end next end From e6daf2f17279c7ec1452cec1123ad13dccf52890 Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Fri, 5 Jun 2015 22:24:05 -0700 Subject: [PATCH 011/484] fix pscommand error messaging --- plugins/commands/ps/command.rb | 4 ++-- plugins/commands/ps/errors.rb | 10 +++++++--- templates/locales/command_ps.yml | 13 ++++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/plugins/commands/ps/command.rb b/plugins/commands/ps/command.rb index ee021d0ed..63963491a 100644 --- a/plugins/commands/ps/command.rb +++ b/plugins/commands/ps/command.rb @@ -91,7 +91,7 @@ module VagrantPlugins args << "-password" << ps_info[:password] result = Vagrant::Util::PowerShell.execute(script_path, *args) if result.exit_code != 0 - raise Errors::PowershellError, + raise Errors::PowerShellError, script: script_path, stderr: result.stderr end @@ -108,7 +108,7 @@ module VagrantPlugins args << "-hostname" << ps_info[:host] result = Vagrant::Util::PowerShell.execute(script_path, *args) if result.exit_code != 0 - raise Errors::PowershellError, + raise Errors::PowerShellError, script: script_path, stderr: result.stderr end diff --git a/plugins/commands/ps/errors.rb b/plugins/commands/ps/errors.rb index c614fb87a..4be70551a 100644 --- a/plugins/commands/ps/errors.rb +++ b/plugins/commands/ps/errors.rb @@ -2,17 +2,21 @@ module VagrantPlugins module CommandPS module Errors # A convenient superclass for all our errors. - class PSError < Vagrant::Errors::VagrantError + class PSCommandError < Vagrant::Errors::VagrantError error_namespace("vagrant_ps.errors") end - class HostUnsupported < PSError + class HostUnsupported < PSCommandError error_key(:host_unsupported) end - class PSRemotingUndetected < PSError + class PSRemotingUndetected < PSCommandError error_key(:ps_remoting_undetected) end + + class PowerShellError < PSCommandError + error_key(:powershell_error) + end end end end diff --git a/templates/locales/command_ps.yml b/templates/locales/command_ps.yml index 51bf666cc..179aae842 100644 --- a/templates/locales/command_ps.yml +++ b/templates/locales/command_ps.yml @@ -3,7 +3,7 @@ en: detecting: |- Detecting if a remote PowerShell connection can be made with the guest... reseting: |- - Reseting WinRM TrustedHosts to their original value. + Resetting WinRM TrustedHosts to their original value. errors: host_unsupported: |- @@ -14,3 +14,14 @@ en: Unable to establish a remote PowerShell connection with the guest. Check if the firewall rules on the guest allow connections to the Windows remote management service. + + powershell_error: |- + An error occurred while executing a PowerShell script. This error + is shown below. Please read the error message and see if this is + a configuration error with your system. If it is not, then please + report a bug. + + Script: %{script} + Error: + + %{stderr} From 2717f5605ae4372c93a3eb768242a4355c37f1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=B6ter?= Date: Mon, 8 Jun 2015 18:03:03 +0200 Subject: [PATCH 012/484] Document use_linked_clone property for VirtualBox provider. --- .../v2/virtualbox/configuration.html.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/website/docs/source/v2/virtualbox/configuration.html.md b/website/docs/source/v2/virtualbox/configuration.html.md index 22cb2a6c5..c9ee51540 100644 --- a/website/docs/source/v2/virtualbox/configuration.html.md +++ b/website/docs/source/v2/virtualbox/configuration.html.md @@ -36,6 +36,30 @@ config.vm.provider "virtualbox" do |v| end ``` +## Linked Clones + +By default new machines are created by importing the base box. For large +boxes this produces a large overhead in terms of time (the import operation) +and space (the new machine contains a copy of the base box's image). +Using linked clones can drastically reduce this overhead. + +Linked clones are based on a master VM, which is generated by importing the +base box only once the first time it is required. For the linked clones only +differencing disk images are created where the parent disk image belongs to +the master VM. + +```ruby +config.vm.provider "virtualbox" do |v| + v.use_linked_clone = true +end +``` + +
+ Note: the generated master VMs are currently not removed + automatically by Vagrant. This has to be done manually. However, a master + VM can only be remove when there are no linked clones connected to it. +
+ ## VBoxManage Customizations [VBoxManage](http://www.virtualbox.org/manual/ch08.html) is a utility that can From dbcb513075a6117b867f4d2933f4715cbbd187aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=B6ter?= Date: Thu, 16 Jul 2015 13:23:57 +0200 Subject: [PATCH 013/484] Fix typo. --- website/docs/source/v2/virtualbox/configuration.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/source/v2/virtualbox/configuration.html.md b/website/docs/source/v2/virtualbox/configuration.html.md index c9ee51540..5c908ab68 100644 --- a/website/docs/source/v2/virtualbox/configuration.html.md +++ b/website/docs/source/v2/virtualbox/configuration.html.md @@ -57,7 +57,7 @@ end
Note: the generated master VMs are currently not removed automatically by Vagrant. This has to be done manually. However, a master - VM can only be remove when there are no linked clones connected to it. + VM can only be removed when there are no linked clones connected to it.
## VBoxManage Customizations From 772f276ee3a1cb6ea62b851bb41f45b49a168587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=B6ter?= Date: Thu, 16 Jul 2015 13:27:24 +0200 Subject: [PATCH 014/484] Port support for linked clones to VirtualBox 5.0 driver. --- .../virtualbox/driver/version_5_0.rb | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index d7d3b58df..e2aeb35a1 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -34,6 +34,15 @@ module VagrantPlugins end end + def clonevm(master_id, box_name, snapshot_name) + @logger.debug("Creating linked clone from master vm with id #{master_id} from snapshot '#{snapshot_name}'") + + machine_name = "#{box_name}_#{snapshot_name}_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" + execute("clonevm", master_id, "--snapshot", snapshot_name, "--options", "link", "--register", "--name", machine_name) + + return get_machine_id machine_name + end + def create_dhcp_server(network, options) execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], @@ -62,6 +71,10 @@ module VagrantPlugins } end + def create_snapshot(machine_id, snapshot_name) + execute("snapshot", machine_id, "take", snapshot_name) + end + def delete execute("unregistervm", @uuid, "--delete") end @@ -156,6 +169,13 @@ module VagrantPlugins execute("modifyvm", @uuid, *args) if !args.empty? end + def get_machine_id(machine_name) + output = execute("list", "vms", retryable: true) + match = /^"#{Regexp.escape(machine_name)}" \{(.+?)\}$/.match(output) + return match[1].to_s if match + nil + end + def halt execute("controlvm", @uuid, "poweroff") end @@ -231,10 +251,7 @@ module VagrantPlugins end end - output = execute("list", "vms", retryable: true) - match = /^"#{Regexp.escape(specified_name)}" \{(.+?)\}$/.match(output) - return match[1].to_s if match - nil + return get_machine_id specified_name end def max_network_adapters From 209556c3cdd426cbcca51a186e0b10be52aa7dd7 Mon Sep 17 00:00:00 2001 From: Jon Burgess Date: Fri, 17 Jul 2015 14:26:13 +1000 Subject: [PATCH 015/484] Allow provisioner instance names to be specified for `up` and `reload` commands and option `--provision-with` Ref: https://github.com/mitchellh/vagrant/issues/5139 --- plugins/commands/reload/command.rb | 2 +- plugins/commands/up/command.rb | 2 +- plugins/commands/up/start_mixins.rb | 23 ++++++++++++++++------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/plugins/commands/reload/command.rb b/plugins/commands/reload/command.rb index 33a6f8e5e..1d43d13b7 100644 --- a/plugins/commands/reload/command.rb +++ b/plugins/commands/reload/command.rb @@ -30,7 +30,7 @@ module VagrantPlugins return if !argv # Validate the provisioners - validate_provisioner_flags!(options) + validate_provisioner_flags!(options, argv) @logger.debug("'reload' each target VM...") machines = [] diff --git a/plugins/commands/up/command.rb b/plugins/commands/up/command.rb index 826bea0ae..e340dad96 100644 --- a/plugins/commands/up/command.rb +++ b/plugins/commands/up/command.rb @@ -48,7 +48,7 @@ module VagrantPlugins return if !argv # Validate the provisioners - validate_provisioner_flags!(options) + validate_provisioner_flags!(options, argv) # Go over each VM and bring it up @logger.debug("'Up' each target VM...") diff --git a/plugins/commands/up/start_mixins.rb b/plugins/commands/up/start_mixins.rb index 548d889ca..8be96c68a 100644 --- a/plugins/commands/up/start_mixins.rb +++ b/plugins/commands/up/start_mixins.rb @@ -26,13 +26,22 @@ module VagrantPlugins # This validates the provisioner flags and raises an exception # if there are invalid ones. - def validate_provisioner_flags!(options) - (options[:provision_types] || []).each do |type| - klass = Vagrant.plugin("2").manager.provisioners[type] - if !klass - raise Vagrant::Errors::ProvisionerFlagInvalid, - name: type.to_s - end + def validate_provisioner_flags!(options, argv) + provisioner_instance_names = [] + with_target_vms(argv) do |machine| + provisioner_instance_names << machine.config.vm.provisioners.map(&:name) + end + + provisioner_instance_names.flatten!.uniq! + + if (provisioner_instance_names & options[:provision_types]).empty? + (options[:provision_types] || []).each do |type| + klass = Vagrant.plugin("2").manager.provisioners[type] + if !klass + raise Vagrant::Errors::ProvisionerFlagInvalid, + name: type.to_s + end + end end end end From d2b0df0a7d3f63eaeca81585edb81f9d9be7f83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Br=C3=A6khus?= Date: Fri, 17 Jul 2015 21:44:58 +0200 Subject: [PATCH 016/484] Specify time and don't do -h -H which is not really a valid usage. --- plugins/guests/debian8/cap/halt.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/guests/debian8/cap/halt.rb b/plugins/guests/debian8/cap/halt.rb index 932281347..b2e5a141b 100644 --- a/plugins/guests/debian8/cap/halt.rb +++ b/plugins/guests/debian8/cap/halt.rb @@ -4,7 +4,7 @@ module VagrantPlugins class Halt def self.halt(machine) begin - machine.communicate.sudo("shutdown -h -H") + machine.communicate.sudo("shutdown -h now") rescue IOError # Do nothing, because it probably means the machine shut down # and SSH connection was lost. From f349a58a1e32ba68deedeca57eae1b67a8266ec3 Mon Sep 17 00:00:00 2001 From: Mattias Appelgren Date: Sat, 18 Jul 2015 13:54:58 +0200 Subject: [PATCH 017/484] provisioners/puppet: Fix Puppet environment default manifest Also parse the puppet variables $codedir and $environment when resolving a manifest path from environment.conf --- plugins/provisioners/puppet/provisioner/puppet.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/provisioners/puppet/provisioner/puppet.rb b/plugins/provisioners/puppet/provisioner/puppet.rb index f1987dbd5..d1e124e85 100644 --- a/plugins/provisioners/puppet/provisioner/puppet.rb +++ b/plugins/provisioners/puppet/provisioner/puppet.rb @@ -62,12 +62,16 @@ module VagrantPlugins # Parse out the environment manifest path since puppet apply doesnt do that for us. environment_conf = File.join(environments_guest_path, @config.environment, "environment.conf") if @machine.communicate.test("test -e #{environment_conf}", sudo: true) - conf = @machine.communicate.sudo("cat #{environment_conf}") do | type, data| + @machine.communicate.sudo("cat #{environment_conf}") do | type, data| if type == :stdout data.each_line do |line| if line =~ /^\s*manifest\s+=\s+([^\s]+)/ @manifest_file = $1 - @manifest_file.gsub! '$basemodulepath:', "#{environments_guest_path}/#{@config.environment}/" + @manifest_file.gsub! "$codedir", File.dirname(environments_guest_path) + @manifest_file.gsub! "$environment", @config.environment + if !@manifest_file.start_with? "/" + @manifest_file = File.join(environments_guest_path, @config.environment, @manifest_file) + end @logger.debug("Using manifest from environment.conf: #{@manifest_file}") end end @@ -85,7 +89,7 @@ module VagrantPlugins # In environment mode we still need to specify a manifest file, if its not, use the one from env config if specified. if !@manifest_file - @manifest_file = "#{environments_guest_path}/#{@config.environment}/manifests/site.pp" + @manifest_file = "#{environments_guest_path}/#{@config.environment}/manifests" parse_environment_metadata end # Check that the shared folders are properly shared From 27b948f53ccd3e5743fc223f2e70617ecf663c83 Mon Sep 17 00:00:00 2001 From: Mattias Appelgren Date: Sat, 18 Jul 2015 14:20:57 +0200 Subject: [PATCH 018/484] website/docs: Add more information regarding Puppet environments --- .../docs/source/v2/provisioning/puppet_apply.html.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/website/docs/source/v2/provisioning/puppet_apply.html.md b/website/docs/source/v2/provisioning/puppet_apply.html.md index 89fe90195..b8f0a5f02 100644 --- a/website/docs/source/v2/provisioning/puppet_apply.html.md +++ b/website/docs/source/v2/provisioning/puppet_apply.html.md @@ -140,8 +140,9 @@ that the path is located in the "vm" at "/path/to/manifests". ## Environments -If you are using Puppet 4 or higher, you can also specify the name of the -Puppet environment and the path on the local disk to the environment files: +If you are using Puppet 4 or higher, you can proivision using +[Puppet Environments](https://docs.puppetlabs.com/puppet/latest/reference/environments.html) by specifying the name of the environment and the path on the +local disk to the environment files: ```ruby Vagrant.configure("2") do |config| @@ -152,6 +153,13 @@ Vagrant.configure("2") do |config| end ``` +The default manifest is the environment's `manifests` directory. +If the environment has an `environment.conf` the manifest path is parsed +from there. Relative paths are assumed to be relative to the directory of +the environment. If the manifest setting in `environment.conf` use +the Puppet variables `$codedir` or `$environment` they are resoled to +the parent directory of `environment_path` and `environment` respectively. + ## Modules Vagrant also supports provisioning with [Puppet modules](http://docs.puppetlabs.com/guides/modules.html). From 19b7bbc369b1d6dd27491a3e8da78e3e93466daa Mon Sep 17 00:00:00 2001 From: Pat O'Shea Date: Sat, 18 Jul 2015 08:39:00 -0600 Subject: [PATCH 019/484] Corrected masterless example The basic Vagrantfile file example for a masterless setup doesn't set the masterless value. That property defaults to false. https://github.com/mitchellh/vagrant/blob/master/plugins/provisioners/salt/provisioner.rb --- website/docs/source/v2/provisioning/salt.html.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/docs/source/v2/provisioning/salt.html.md b/website/docs/source/v2/provisioning/salt.html.md index 8169ec28e..1799a09dd 100644 --- a/website/docs/source/v2/provisioning/salt.html.md +++ b/website/docs/source/v2/provisioning/salt.html.md @@ -30,7 +30,8 @@ on a single minion, without a master: ## Use all the defaults: config.vm.provision :salt do |salt| - + + salt.masterless = true salt.minion_config = "salt/minion" salt.run_highstate = true From d34bc38bf39ce9a045dcc4c96c8f2693ef1280a3 Mon Sep 17 00:00:00 2001 From: Pat O'Shea Date: Sat, 18 Jul 2015 20:53:46 -0600 Subject: [PATCH 020/484] Updated salt-minion and call ext on windows guest Salt-minion and salt-call are batch files on a windows guest, not executables. --- plugins/provisioners/salt/provisioner.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/provisioners/salt/provisioner.rb b/plugins/provisioners/salt/provisioner.rb index d74c4fa11..b5f65f1ae 100644 --- a/plugins/provisioners/salt/provisioner.rb +++ b/plugins/provisioners/salt/provisioner.rb @@ -35,8 +35,8 @@ module VagrantPlugins desired_binaries = [] if !@config.no_minion if @machine.config.vm.communicator == :winrm - desired_binaries.push('C:\\salt\\salt-minion.exe') - desired_binaries.push('C:\\salt\\salt-call.exe') + desired_binaries.push('C:\\salt\\salt-minion.bat') + desired_binaries.push('C:\\salt\\salt-call.bat') else desired_binaries.push('salt-minion') desired_binaries.push('salt-call') @@ -361,8 +361,8 @@ module VagrantPlugins else if @machine.config.vm.communicator == :winrm opts = { elevated: true } - @machine.communicate.execute("C:\\salt\\salt-call.exe saltutil.sync_all", opts) - @machine.communicate.execute("C:\\salt\\salt-call.exe state.highstate --retcode-passthrough #{get_loglevel}#{get_colorize}#{get_pillar}", opts) do |type, data| + @machine.communicate.execute("C:\\salt\\salt-call.bat saltutil.sync_all", opts) + @machine.communicate.execute("C:\\salt\\salt-call.bat state.highstate --retcode-passthrough #{get_loglevel}#{get_colorize}#{get_pillar}", opts) do |type, data| if @config.verbose @machine.env.ui.info(data.rstrip) end From 562ed26533a072b4a255ba7d9fe31b1260c367e6 Mon Sep 17 00:00:00 2001 From: Ievgen Prokhorenko Date: Sun, 19 Jul 2015 16:38:52 +0300 Subject: [PATCH 021/484] Fix #3570 'Box data left in ~/.vagrant.d/boxes after removal' --- lib/vagrant/action/builtin/box_remove.rb | 1 + lib/vagrant/box_collection.rb | 28 ++++++++++++++++--- .../vagrant/action/builtin/box_remove_test.rb | 26 +++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/vagrant/action/builtin/box_remove.rb b/lib/vagrant/action/builtin/box_remove.rb index 1d0cea7fd..4c242aa97 100644 --- a/lib/vagrant/action/builtin/box_remove.rb +++ b/lib/vagrant/action/builtin/box_remove.rb @@ -106,6 +106,7 @@ module Vagrant provider: box.provider, version: box.version)) box.destroy! + env[:box_collection].clean_up(box) # Passes on the removed box to the rest of the middleware chain env[:box_removed] = box diff --git a/lib/vagrant/box_collection.rb b/lib/vagrant/box_collection.rb index a7599f7c2..17c2f3927 100644 --- a/lib/vagrant/box_collection.rb +++ b/lib/vagrant/box_collection.rb @@ -13,6 +13,8 @@ module Vagrant # boxes. class BoxCollection TEMP_PREFIX = "vagrant-box-add-temp-" + VAGRANT_SLASH = "-VAGRANTSLASH-" + VAGRANT_COLON = "-VAGRANTCOLON-" # The directory where the boxes in this collection are stored. # @@ -346,6 +348,19 @@ module Vagrant end end + # Removes the whole directory of a given box if there are no + # other versions nor providers of the box exist. + def clean_up(box) + return false if exists?(box.name) + + box_directory = box.name + .gsub('/', VAGRANT_SLASH) + .gsub(':', VAGRANT_COLON) + + path = File.join(directory, box_directory) + FileUtils.rm_r(path) + end + protected # Returns the directory name for the box of the given name. @@ -354,16 +369,16 @@ module Vagrant # @return [String] def dir_name(name) name = name.dup - name.gsub!(":", "-VAGRANTCOLON-") if Util::Platform.windows? - name.gsub!("/", "-VAGRANTSLASH-") + name.gsub!(":", VAGRANT_COLON) if Util::Platform.windows? + name.gsub!("/", VAGRANT_SLASH) name end # Returns the directory name for the box cleaned up def undir_name(name) name = name.dup - name.gsub!("-VAGRANTCOLON-", ":") - name.gsub!("-VAGRANTSLASH-", "/") + name.gsub!(VAGRANT_COLON, ":") + name.gsub!(VAGRANT_SLASH, "/") name end @@ -440,5 +455,10 @@ module Vagrant ensure dir.rmtree if dir.exist? end + + # Checks if a box with a given name exists. + def exists?(box_name) + all.any? { |box| box.first.eql?(box_name) } + end end end diff --git a/test/unit/vagrant/action/builtin/box_remove_test.rb b/test/unit/vagrant/action/builtin/box_remove_test.rb index 9de3e467b..016fec243 100644 --- a/test/unit/vagrant/action/builtin/box_remove_test.rb +++ b/test/unit/vagrant/action/builtin/box_remove_test.rb @@ -30,6 +30,8 @@ describe Vagrant::Action::Builtin::BoxRemove do expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0").and_return(box) + expect(box_collection).to receive(:clean_up).with(box) + .and_return(true) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once @@ -50,6 +52,8 @@ describe Vagrant::Action::Builtin::BoxRemove do expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0").and_return(box) + expect(box_collection).to receive(:clean_up).with(box) + .and_return(false) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once @@ -70,6 +74,8 @@ describe Vagrant::Action::Builtin::BoxRemove do expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0").and_return(box) + expect(box_collection).to receive(:clean_up).with(box) + .and_return(false) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once @@ -78,6 +84,22 @@ describe Vagrant::Action::Builtin::BoxRemove do expect(env[:box_removed]).to equal(box) end + it "deletes the whole directory of the box if it's the last box on the system" do + box_collection.stub( + all: [ + ["foo", "1.0", :virtualbox], + ]) + + env[:box_name] = "foo" + + expect(box_collection).to receive(:find).with( + "foo", :virtualbox, "1.0").and_return(box) + expect(box_collection).to receive(:clean_up).with(box) + .and_return(true) + + subject.call(env) + end + context "checking if a box is in use" do def new_entry(name, provider, version, valid=true) Vagrant::MachineIndex::Entry.new.tap do |entry| @@ -110,6 +132,8 @@ describe Vagrant::Action::Builtin::BoxRemove do expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0").and_return(box) expect(box).to receive(:destroy!).once + expect(box_collection).to receive(:clean_up).with(box) + .and_return(true) subject.call(env) end @@ -123,6 +147,8 @@ describe Vagrant::Action::Builtin::BoxRemove do expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0").and_return(box) + expect(box_collection).to receive(:clean_up).with(box) + .and_return(true) expect(box).to receive(:destroy!).once subject.call(env) From e4cdb473bd1a7ad666ddc9f398c661f62a9cfb2c Mon Sep 17 00:00:00 2001 From: Brian Dwyer Date: Tue, 28 Jul 2015 12:41:51 -0400 Subject: [PATCH 022/484] Bring back `nodes_path` support for the Chef Zero provisioner --- plugins/provisioners/chef/config/chef_zero.rb | 7 +++++++ plugins/provisioners/chef/provisioner/chef_solo.rb | 2 ++ plugins/provisioners/chef/provisioner/chef_zero.rb | 1 + templates/provisioners/chef_zero/zero.erb | 4 ++-- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/provisioners/chef/config/chef_zero.rb b/plugins/provisioners/chef/config/chef_zero.rb index d28de3dc9..9b1daf1a5 100644 --- a/plugins/provisioners/chef/config/chef_zero.rb +++ b/plugins/provisioners/chef/config/chef_zero.rb @@ -17,6 +17,10 @@ module VagrantPlugins # @return [String] attr_accessor :environments_path + # The path where nodes are stored on disk. + # @return [String] + attr_accessor :nodes_path + # The path where roles are stored on disk. # @return [String] attr_accessor :roles_path @@ -31,6 +35,7 @@ module VagrantPlugins @cookbooks_path = UNSET_VALUE @data_bags_path = UNSET_VALUE @environments_path = UNSET_VALUE + @nodes_path = UNSET_VALUE @roles_path = UNSET_VALUE @synced_folder_type = UNSET_VALUE end @@ -47,6 +52,7 @@ module VagrantPlugins end @data_bags_path = [] if @data_bags_path == UNSET_VALUE + @nodes_path = [] if @nodes_path == UNSET_VALUE @roles_path = [] if @roles_path == UNSET_VALUE @environments_path = [] if @environments_path == UNSET_VALUE @environments_path = [@environments_path].flatten @@ -54,6 +60,7 @@ module VagrantPlugins # Make sure the path is an array. @cookbooks_path = prepare_folders_config(@cookbooks_path) @data_bags_path = prepare_folders_config(@data_bags_path) + @nodes_path = prepare_folders_config(@nodes_path) @roles_path = prepare_folders_config(@roles_path) @environments_path = prepare_folders_config(@environments_path) diff --git a/plugins/provisioners/chef/provisioner/chef_solo.rb b/plugins/provisioners/chef/provisioner/chef_solo.rb index f3b52f178..14ee5747c 100644 --- a/plugins/provisioners/chef/provisioner/chef_solo.rb +++ b/plugins/provisioners/chef/provisioner/chef_solo.rb @@ -33,12 +33,14 @@ module VagrantPlugins @role_folders = expanded_folders(@config.roles_path, "roles") @data_bags_folders = expanded_folders(@config.data_bags_path, "data_bags") @environments_folders = expanded_folders(@config.environments_path, "environments") + @node_folders = expanded_folders(@config.nodes_path, "nodes") existing = synced_folders(@machine, cached: true) share_folders(root_config, "csc", @cookbook_folders, existing) share_folders(root_config, "csr", @role_folders, existing) share_folders(root_config, "csdb", @data_bags_folders, existing) share_folders(root_config, "cse", @environments_folders, existing) + share_folders(root_config, "csn", @node_folders, existing) end def provision diff --git a/plugins/provisioners/chef/provisioner/chef_zero.rb b/plugins/provisioners/chef/provisioner/chef_zero.rb index 48078a09f..70fff6854 100644 --- a/plugins/provisioners/chef/provisioner/chef_zero.rb +++ b/plugins/provisioners/chef/provisioner/chef_zero.rb @@ -44,6 +44,7 @@ module VagrantPlugins local_mode: true, enable_reporting: false, cookbooks_path: guest_paths(@cookbook_folders), + nodes_path: guest_paths(@node_folders), roles_path: guest_paths(@role_folders), data_bags_path: guest_paths(@data_bags_folders).first, environments_path: guest_paths(@environments_folders).first, diff --git a/templates/provisioners/chef_zero/zero.erb b/templates/provisioners/chef_zero/zero.erb index 29de30d23..8c4b76407 100644 --- a/templates/provisioners/chef_zero/zero.erb +++ b/templates/provisioners/chef_zero/zero.erb @@ -31,8 +31,8 @@ environment "<%= environment %>" chef_zero.enabled true local_mode true <% end -%> -<% if node_path -%> -node_path <%= node_path.inspect %> +<% if nodes_path -%> +node_path <%= nodes_path.inspect %> <% end -%> <% if formatter %> From 15ec95328ad962ee4326e3cdcaca376af7cea8e0 Mon Sep 17 00:00:00 2001 From: Brian Dwyer Date: Tue, 28 Jul 2015 13:24:39 -0400 Subject: [PATCH 023/484] Update test --- .../plugins/provisioners/chef/config/chef_zero_test.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb b/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb index 2f9cd82aa..9a1f98cd6 100644 --- a/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb +++ b/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb @@ -50,6 +50,14 @@ describe VagrantPlugins::Chef::Config::ChefZero do end end + describe "#nodes_path" do + it "defaults to an empty array" do + subject.finalize! + expect(subject.nodes_path).to be_a(Array) + expect(subject.nodes_path).to be_empty + end + end + describe "#synced_folder_type" do it "defaults to nil" do subject.finalize! From 01f0dccbdc3f26842ed74a018f7cf828e9e19234 Mon Sep 17 00:00:00 2001 From: Brian Dwyer Date: Tue, 28 Jul 2015 13:25:25 -0400 Subject: [PATCH 024/484] Update documentation for Chef Zero `nodes_path` --- website/docs/source/v2/provisioning/chef_zero.html.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/docs/source/v2/provisioning/chef_zero.html.md b/website/docs/source/v2/provisioning/chef_zero.html.md index 4f35f362a..bd2527fc9 100644 --- a/website/docs/source/v2/provisioning/chef_zero.html.md +++ b/website/docs/source/v2/provisioning/chef_zero.html.md @@ -41,6 +41,9 @@ available below this section. * `environments_path` (string) - A path where environment definitions are located. By default, no environments folder is set. +* `nodes_path` (string or array) - A list of paths where node objects (in JSON format) are stored. By default, no + nodes path is set. + * `environment` (string) - The environment you want the Chef run to be a part of. This requires Chef 11.6.0 or later, and that `environments_path` is set. From 3f29be0de2e64498fa7b6d4bfcbe6414e1a5604b Mon Sep 17 00:00:00 2001 From: Ben Hines Date: Tue, 28 Jul 2015 11:16:20 -0700 Subject: [PATCH 025/484] Fix string parse error in the environment path missing error message. --- plugins/provisioners/puppet/config/puppet.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/provisioners/puppet/config/puppet.rb b/plugins/provisioners/puppet/config/puppet.rb index 9910c6e05..0761294ca 100644 --- a/plugins/provisioners/puppet/config/puppet.rb +++ b/plugins/provisioners/puppet/config/puppet.rb @@ -143,7 +143,7 @@ module VagrantPlugins if !expanded_environment_file.file? && !expanded_environment_file.directory? errors << I18n.t("vagrant.provisioners.puppet.environment_missing", environment: environment.to_s, - environment_path: expanded_path.to_s) + environmentpath: expanded_path.to_s) end end end From f13220cd748d67430f4236eefff9dfa69ef68237 Mon Sep 17 00:00:00 2001 From: Casey Lang Date: Fri, 31 Jul 2015 14:10:57 -0400 Subject: [PATCH 026/484] Clarify config.ssh.insert_key docs Modified the description of `config.ssh.insert_key` a bit to improve readability. --- website/docs/source/v2/vagrantfile/ssh_settings.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/source/v2/vagrantfile/ssh_settings.html.md b/website/docs/source/v2/vagrantfile/ssh_settings.html.md index b07835b75..cadeca060 100644 --- a/website/docs/source/v2/vagrantfile/ssh_settings.html.md +++ b/website/docs/source/v2/vagrantfile/ssh_settings.html.md @@ -68,12 +68,12 @@ is enabled. Defaults to false.
`config.ssh.insert_key` - If `true`, Vagrant will automatically insert -an keypair to use for SSH, replacing the default Vagrant's insecure key +a keypair to use for SSH, replacing Vagrant's default insecure key inside the machine if detected. By default, this is true. This only has an effect if you don't already use private keys for authentication or if you are relying on the default insecure key. -If you don't have to take care about security in your project and want to +If you don't have to care about security in your project and want to keep using the default insecure key, set this to `false`.
From ea7b277f411da996fd911fe24e1219fe13800ff0 Mon Sep 17 00:00:00 2001 From: John Syrinek Date: Fri, 31 Jul 2015 16:05:55 -0500 Subject: [PATCH 027/484] Prevent fatal error caused by attempting to upload minion config to privileged directory --- plugins/provisioners/salt/config.rb | 18 ------------------ plugins/provisioners/salt/provisioner.rb | 5 ----- 2 files changed, 23 deletions(-) diff --git a/plugins/provisioners/salt/config.rb b/plugins/provisioners/salt/config.rb index 9c564179e..38aaec8de 100644 --- a/plugins/provisioners/salt/config.rb +++ b/plugins/provisioners/salt/config.rb @@ -20,7 +20,6 @@ module VagrantPlugins attr_accessor :bootstrap_script attr_accessor :verbose attr_accessor :seed_master - attr_accessor :config_dir attr_reader :pillar_data attr_accessor :colorize attr_accessor :log_level @@ -65,7 +64,6 @@ module VagrantPlugins @install_command = UNSET_VALUE @no_minion = UNSET_VALUE @bootstrap_options = UNSET_VALUE - @config_dir = UNSET_VALUE @masterless = UNSET_VALUE @minion_id = UNSET_VALUE @version = UNSET_VALUE @@ -98,7 +96,6 @@ module VagrantPlugins @install_command = nil if @install_command == UNSET_VALUE @no_minion = nil if @no_minion == UNSET_VALUE @bootstrap_options = nil if @bootstrap_options == UNSET_VALUE - @config_dir = nil if @config_dir == UNSET_VALUE @masterless = false if @masterless == UNSET_VALUE @minion_id = nil if @minion_id == UNSET_VALUE @version = nil if @version == UNSET_VALUE @@ -111,17 +108,6 @@ module VagrantPlugins @pillar_data = Vagrant::Util::DeepMerge.deep_merge(@pillar_data, data) end - def default_config_dir(machine) - guest_type = machine.config.vm.guest || :linux - - # FIXME: there should be a way to do that a bit smarter - if guest_type == :windows - return "C:\\salt\\conf" - else - return "/etc/salt" - end - end - def validate(machine) errors = _detected_errors if @minion_config @@ -161,10 +147,6 @@ module VagrantPlugins errors << I18n.t("vagrant.provisioners.salt.must_accept_keys") end - if @config_dir.nil? - @config_dir = default_config_dir(machine) - end - return {"salt provisioner" => errors} end end diff --git a/plugins/provisioners/salt/provisioner.rb b/plugins/provisioners/salt/provisioner.rb index d74c4fa11..5b217da4b 100644 --- a/plugins/provisioners/salt/provisioner.rb +++ b/plugins/provisioners/salt/provisioner.rb @@ -342,11 +342,6 @@ module VagrantPlugins end def call_highstate - if @config.minion_config - @machine.env.ui.info "Copying salt minion config to #{@config.config_dir}" - @machine.communicate.upload(expanded_path(@config.minion_config).to_s, @config.config_dir + "/minion") - end - if @config.masterless call_masterless elsif @config.run_highstate From 587c88e65ae294c4f2939937c2079c545efd17cb Mon Sep 17 00:00:00 2001 From: Mike Averto Date: Sun, 9 Aug 2015 12:44:49 -0400 Subject: [PATCH 028/484] Fix Win 10 Enterprise Vagrant Error This fixes error for Win 10 Enterprise: An error occurred while executing a PowerShell script. This error is shown below. Please read the error message and see if this is a configuration error with your system. If it is not, then please report a bug. Script: get_vm_status.ps1 Error: C:\HashiCorp\Vagrant\embedded\gems\gems\vagrant-1.7.4\plugins\providers\hyperv\scripts\get_vm_status.ps1 : Unable to find type [Microsoft.HyperV.PowerShell.VirtualizationOperationFailedException]. At line:1 char:1 + &('C:\HashiCorp\Vagrant\embedded\gems\gems\vagrant-1.7.4\plugins\prov ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (Microsoft.Hyper...FailedException:TypeName) [get_vm_status.ps1], Ru ntimeException + FullyQualifiedErrorId : TypeNotFound,get_vm_status.ps1 --- plugins/providers/hyperv/scripts/get_vm_status.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/providers/hyperv/scripts/get_vm_status.ps1 b/plugins/providers/hyperv/scripts/get_vm_status.ps1 index 739560190..5c1e1aa73 100644 --- a/plugins/providers/hyperv/scripts/get_vm_status.ps1 +++ b/plugins/providers/hyperv/scripts/get_vm_status.ps1 @@ -12,7 +12,7 @@ try { $VM = Get-VM -Id $VmId -ErrorAction "Stop" $State = $VM.state $Status = $VM.status -} catch [Microsoft.HyperV.PowerShell.VirtualizationOperationFailedException] { +} catch [Microsoft.HyperV.PowerShell.VirtualizationException] { $State = "not_created" $Status = $State } From 2a2f0a4751a7ea40f7db9941904291746d9729ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20P=C3=B6ter?= Date: Wed, 12 Aug 2015 14:25:54 +0200 Subject: [PATCH 029/484] Use hash of machine name for lock file to avoid problems with invalid characters for file names. --- plugins/providers/virtualbox/action/import_master.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/providers/virtualbox/action/import_master.rb b/plugins/providers/virtualbox/action/import_master.rb index cba6216fd..61b0064eb 100644 --- a/plugins/providers/virtualbox/action/import_master.rb +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -13,7 +13,7 @@ module VagrantPlugins def call(env) master_id_file = env[:machine].box.directory.join("master_id") - env[:machine].env.lock(env[:machine].box.name, retry: true) do + env[:machine].env.lock(Digest::MD5.hexdigest(env[:machine].box.name), retry: true) do env[:master_id] = master_id_file.read.chomp if master_id_file.file? if env[:master_id] && env[:machine].provider.driver.vm_exists?(env[:master_id]) # Master VM already exists -> nothing to do - continue. From 6f59c8bb548c70053c23c80ca7fb62429fa99496 Mon Sep 17 00:00:00 2001 From: Victor Costan Date: Sat, 15 Aug 2015 06:19:51 -0400 Subject: [PATCH 030/484] The docs typo'd the nic_type parameter The parameter name in the VirtualBox provider source is nic_type, not nictype. The typo is probably caused by the fact that the parameter maps to nictype args in the VirtualBox CLI. --- website/docs/source/v2/virtualbox/networking.html.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/source/v2/virtualbox/networking.html.md b/website/docs/source/v2/virtualbox/networking.html.md index 55e7d4c4c..5811373de 100644 --- a/website/docs/source/v2/virtualbox/networking.html.md +++ b/website/docs/source/v2/virtualbox/networking.html.md @@ -36,8 +36,8 @@ end ## VirtualBox NIC Type -You can specify a specific nictype for the created network interface -by using the `nictype` parameter. This isn't prefixed by `virtualbox__` +You can specify a specific NIC type for the created network interface +by using the `nic_type` parameter. This isn't prefixed by `virtualbox__` for legacy reasons, but is VirtualBox-specific. This is an advanced option and should only be used if you know what @@ -48,6 +48,6 @@ Example: ```ruby Vagrant.configure("2") do |config| config.vm.network "private_network", ip: "192.168.50.4", - nictype: "virtio" + nic_type: "virtio" end ``` From 4425d91d863b3b36347ebc53550b13d8e6e5bc37 Mon Sep 17 00:00:00 2001 From: Victor Costan Date: Sat, 15 Aug 2015 06:53:01 -0400 Subject: [PATCH 031/484] Don't warn about an .1 IP for DHCP networks When the network's type is :dhcp, the :ip option is used to derive the DHCP server configuration, and it doesn't actually indicate the IP that will be received by the VM(s). --- plugins/kernel_v2/config/vm.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 2de1b343f..5f2486179 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -688,7 +688,7 @@ module VagrantPlugins end end - if options[:ip] && options[:ip].end_with?(".1") + if options[:ip] && options[:ip].end_with?(".1") && options[:type].to_sym != :dhcp machine.ui.warn(I18n.t( "vagrant.config.vm.network_ip_ends_in_one")) end From 0f8a88f4975a68c13057d7bcb6b55d04081a0766 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Mon, 17 Aug 2015 11:22:24 -0400 Subject: [PATCH 032/484] website/www: Fix logos/copyright alignment Before: ![](http://cl.ly/image/2g2h0v1Z371r/Screen%20Shot%202015-08-17%20at%2011.22.48%20AM.png) After: ![](http://cl.ly/image/0s1e2a272o2q/Screen%20Shot%202015-08-17%20at%2011.21.31%20AM.png) --- website/www/source/layouts/layout.erb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/website/www/source/layouts/layout.erb b/website/www/source/layouts/layout.erb index 4e6736743..a510d31bf 100644 --- a/website/www/source/layouts/layout.erb +++ b/website/www/source/layouts/layout.erb @@ -78,9 +78,11 @@
- +
+ +
From c58c9a1ffccd576c6c08fb2edc721258808b9d6d Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Mon, 17 Aug 2015 11:29:46 -0400 Subject: [PATCH 033/484] website/www: Add Atlas box search to nav --- website/www/source/layouts/layout.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/website/www/source/layouts/layout.erb b/website/www/source/layouts/layout.erb index 4e6736743..cbf2b8c88 100644 --- a/website/www/source/layouts/layout.erb +++ b/website/www/source/layouts/layout.erb @@ -41,6 +41,7 @@
  • VMware Integration
  • Downloads
  • +
  • Boxes
  • Documentation
  • Blog
  • About
  • From b897fd73651e263fc2b0fd364b7d6aba2f8f905d Mon Sep 17 00:00:00 2001 From: Lonnie VanZandt Date: Tue, 18 Aug 2015 10:56:13 -0600 Subject: [PATCH 034/484] Scrub Guest Paths for Windows Rsync leaving Dirty Paths for Winrm Mkdir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows offers no out-of-the-box rsync utility. By far, the most commonly used external utilities for Windows rsync are built with the GNU Cygwin libraries. The cost for this convenience is that rsync on Windows has to be provided paths that begin “/cygdrive/c” rather than “c:/“ like other Windows-API utilities. Compounding the situation, rsync doesn’t create paths/to/sub/targets and so the vagrant plugin code, when performing an rsync, is responsible for creating intermediate directories in guest paths if there are any. Furthermore, the mkdir utility in Windows is not another Cygwin utility like rsync but the routine mkdir of Windows command.com. Therefore, while rsync needs the /cygwin paths, mkdir uses the Windows paths. Later, the chef_solo.rp provisioner running within the guest will expect to find Windows-style paths in its solo.rb configuration file. Due to all this, vagrant has to keep track of both the original, possibly dirty Windows guest path and the cygwin-scrubbed guest path. --- plugins/guests/windows/cap/rsync.rb | 9 +++++++++ plugins/guests/windows/plugin.rb | 5 +++++ plugins/synced_folders/rsync/helper.rb | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/plugins/guests/windows/cap/rsync.rb b/plugins/guests/windows/cap/rsync.rb index e391b92db..aa9f1bcdf 100644 --- a/plugins/guests/windows/cap/rsync.rb +++ b/plugins/guests/windows/cap/rsync.rb @@ -2,8 +2,17 @@ module VagrantPlugins module GuestWindows module Cap class RSync + def self.rsync_scrub_guestpath( machine, opts ) + # Windows guests most often use cygwin-dependent rsync utilities + # that expect "/cygdrive/c" instead of "c:" as the path prefix + # some vagrant code may pass guest paths with drive-lettered paths here + opts[:guestpath].gsub( /^([a-zA-Z]):/, '/cygdrive/\1' ) + end + def self.rsync_pre(machine, opts) machine.communicate.tap do |comm| + # rsync does not construct any gaps in the path to the target directory + # make sure that all subdirectories are created comm.execute("mkdir '#{opts[:guestpath]}'") end end diff --git a/plugins/guests/windows/plugin.rb b/plugins/guests/windows/plugin.rb index 47ca67e3f..d22dc2743 100644 --- a/plugins/guests/windows/plugin.rb +++ b/plugins/guests/windows/plugin.rb @@ -64,6 +64,11 @@ module VagrantPlugins Cap::MountSharedFolder end + guest_capability(:windows, :rsync_scrub_guestpath) do + require_relative "cap/rsync" + Cap::RSync + end + guest_capability(:windows, :rsync_pre) do require_relative "cap/rsync" Cap::RSync diff --git a/plugins/synced_folders/rsync/helper.rb b/plugins/synced_folders/rsync/helper.rb index 594410436..e155d61f7 100644 --- a/plugins/synced_folders/rsync/helper.rb +++ b/plugins/synced_folders/rsync/helper.rb @@ -38,6 +38,11 @@ module VagrantPlugins hostpath = File.expand_path(hostpath, machine.env.root_path) hostpath = Vagrant::Util::Platform.fs_real_path(hostpath).to_s + # if the guest has a guest path scrubber capability, use it + if machine.guest.capability?(:rsync_scrub_guestpath) + guestpath = machine.guest.capability(:rsync_scrub_guestpath, opts) + end + if Vagrant::Util::Platform.windows? # rsync for Windows expects cygwin style paths, always. hostpath = Vagrant::Util::Platform.cygwin_path(hostpath) From d11ff3237066a3c65de32ae4e058122a6c958819 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Wed, 19 Aug 2015 11:21:43 -0700 Subject: [PATCH 035/484] Change docs for checkpoint environment variable to reflect the not-vagrant-specific version CHECKPOINT_DISABLE --- website/docs/source/v2/other/environmental-variables.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/source/v2/other/environmental-variables.html.md b/website/docs/source/v2/other/environmental-variables.html.md index 41ecdd252..7ed73f4fd 100644 --- a/website/docs/source/v2/other/environmental-variables.html.md +++ b/website/docs/source/v2/other/environmental-variables.html.md @@ -17,12 +17,12 @@ when launching Vagrant from the official installer, you can specify the `VAGRANT_DEBUG_LAUNCHER` environment variable to output debugging information about the launch process. -## VAGRANT\_CHECKPOINT\_DISABLE +## CHECKPOINT\_DISABLE Vagrant does occasional network calls to check whether the version of Vagrant that is running locally is up to date. We understand that software making remote calls over the internet for any reason can be undesirable. To surpress these -calls, set the environment variable `VAGRANT_CHECKPOINT_DISABLE` to any +calls, set the environment variable `CHECKPOINT_DISABLE` to any non-empty value. ## VAGRANT\_CWD From e7ca6acbfe6bea66ca1b7f8ebbfd6d96ff6557f5 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Thu, 20 Aug 2015 14:34:23 -0700 Subject: [PATCH 036/484] Revert heading change and add note about CHECKPOINT_DISABLE --- .../docs/source/v2/other/environmental-variables.html.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/website/docs/source/v2/other/environmental-variables.html.md b/website/docs/source/v2/other/environmental-variables.html.md index 7ed73f4fd..866b4369a 100644 --- a/website/docs/source/v2/other/environmental-variables.html.md +++ b/website/docs/source/v2/other/environmental-variables.html.md @@ -17,14 +17,17 @@ when launching Vagrant from the official installer, you can specify the `VAGRANT_DEBUG_LAUNCHER` environment variable to output debugging information about the launch process. -## CHECKPOINT\_DISABLE +## VAGRANT\_CHECKPOINT\_DISABLE Vagrant does occasional network calls to check whether the version of Vagrant that is running locally is up to date. We understand that software making remote calls over the internet for any reason can be undesirable. To surpress these -calls, set the environment variable `CHECKPOINT_DISABLE` to any +calls, set the environment variable `VAGRANT_CHECKPOINT_DISABLE` to any non-empty value. +If you use other HashiCorp tools like Packer and would prefer to configure this +setting only once, you can set `CHECKPOINT_DISABLE` instead. + ## VAGRANT\_CWD `VAGRANT_CWD` can be set to change the working directory of Vagrant. By From f71b27ff27b3061c3206ce48762ee4a79f56005d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Fri, 21 Aug 2015 12:49:36 +0300 Subject: [PATCH 037/484] fix network detection on pld-linux pld linux uses redhat as base, but lacks :flavour this will add it --- plugins/guests/pld/cap/flavor.rb | 11 +++++++++++ plugins/guests/pld/plugin.rb | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 plugins/guests/pld/cap/flavor.rb diff --git a/plugins/guests/pld/cap/flavor.rb b/plugins/guests/pld/cap/flavor.rb new file mode 100644 index 000000000..5ade9d18c --- /dev/null +++ b/plugins/guests/pld/cap/flavor.rb @@ -0,0 +1,11 @@ +module VagrantPlugins + module GuestPld + module Cap + class Flavor + def self.flavor(machine) + return :pld + end + end + end + end +end diff --git a/plugins/guests/pld/plugin.rb b/plugins/guests/pld/plugin.rb index ef7939fad..697599c81 100644 --- a/plugins/guests/pld/plugin.rb +++ b/plugins/guests/pld/plugin.rb @@ -20,6 +20,11 @@ module VagrantPlugins require_relative "cap/network_scripts_dir" Cap::NetworkScriptsDir end + + guest_capability("pld", "flavor") do + require_relative "cap/flavor" + Cap::Flavor + end end end end From af0f267b5089098ded8c2c3e6b8495cbeb8cbcdd Mon Sep 17 00:00:00 2001 From: Arlo Louis O'Keeffe Date: Wed, 26 Aug 2015 00:15:03 +0200 Subject: [PATCH 038/484] Check if Schedule.Service com object is available --- .../communicators/winrm/scripts/elevated_shell.ps1.erb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb index 17767e436..77529aa76 100644 --- a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb +++ b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb @@ -1,5 +1,13 @@ param([String]$username, [String]$password, [String]$encoded_command) +$schedule = $null +Try { + $schedule = New-Object -ComObject "Schedule.Service" +} Catch [System.Management.Automation.PSArgumentException] { + powershell.exe -EncodedCommand $encoded_command + exit $LASTEXITCODE +} + $task_name = "WinRM_Elevated_Shell" $out_file = "$env:SystemRoot\Temp\WinRM_Elevated_Shell.log" @@ -50,7 +58,6 @@ $arguments = "/c powershell.exe -EncodedCommand $encoded_command > $out_file $task_xml = $task_xml.Replace("{arguments}", $arguments) $task_xml = $task_xml.Replace("{username}", $username) -$schedule = New-Object -ComObject "Schedule.Service" $schedule.Connect() $task = $schedule.NewTask($null) $task.XmlText = $task_xml From be90f6b1da85b6dd9e52617f344004cf8253718e Mon Sep 17 00:00:00 2001 From: Dusty Mabe Date: Thu, 27 Aug 2015 16:44:17 -0400 Subject: [PATCH 039/484] Fix Fedora /etc/hosts bug Update so that localhost entries don't get deleted when the hostname gets added to the 127.0.0.1 line. Closes #6202 --- plugins/guests/fedora/cap/change_host_name.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/guests/fedora/cap/change_host_name.rb b/plugins/guests/fedora/cap/change_host_name.rb index 2a93b9de6..f0c95eec6 100644 --- a/plugins/guests/fedora/cap/change_host_name.rb +++ b/plugins/guests/fedora/cap/change_host_name.rb @@ -48,7 +48,7 @@ module VagrantPlugins def update_etc_hosts ip_address = '([0-9]{1,3}\.){3}[0-9]{1,3}' search = "^(#{ip_address})\\s+#{Regexp.escape(current_hostname)}(\\s.*)?$" - replace = "\\1 #{fqdn} #{short_hostname}" + replace = "\\1 #{fqdn} #{short_hostname} \\3" expression = ['s', search, replace, 'g'].join('@') sudo("sed -ri '#{expression}' /etc/hosts") @@ -72,4 +72,4 @@ module VagrantPlugins end end end -end \ No newline at end of file +end From 18d229ca829536c115e351489f88891376232620 Mon Sep 17 00:00:00 2001 From: Jeremy Roberts Date: Sun, 30 Aug 2015 13:09:25 -0400 Subject: [PATCH 040/484] Added execution_time_limit for WinRM. Adds a configurable value for WinRm and the elevated permission shell ExecutionTimeLimit. Please see mitchellh/vagrant#5506 Ex: config.winrm.execution_time_limit = "P1D" --- plugins/communicators/winrm/communicator.rb | 3 ++- plugins/communicators/winrm/config.rb | 4 ++++ plugins/communicators/winrm/scripts/elevated_shell.ps1.erb | 5 +++-- plugins/communicators/winrm/shell.rb | 2 ++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb index fdb3fe615..38fda78f9 100644 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -210,7 +210,8 @@ module VagrantPlugins "powershell -executionpolicy bypass -file \"#{guest_script_path}\" " + "-username \"#{shell.username}\" -password \"#{shell.password}\" " + - "-encoded_command \"#{wrapped_encoded_command}\"" + "-encoded_command \"#{wrapped_encoded_command}\" " + + "-execution_time_limit \"#{shell.execution_time_limit}\"" end # Handles the raw WinRM shell result and converts it to a diff --git a/plugins/communicators/winrm/config.rb b/plugins/communicators/winrm/config.rb index 79901d503..3387ca142 100644 --- a/plugins/communicators/winrm/config.rb +++ b/plugins/communicators/winrm/config.rb @@ -11,6 +11,7 @@ module VagrantPlugins attr_accessor :timeout attr_accessor :transport attr_accessor :ssl_peer_verification + attr_accessor :execution_time_limit def initialize @username = UNSET_VALUE @@ -23,6 +24,7 @@ module VagrantPlugins @timeout = UNSET_VALUE @transport = UNSET_VALUE @ssl_peer_verification = UNSET_VALUE + @execution_time_limit = UNSET_VALUE end def finalize! @@ -37,6 +39,7 @@ module VagrantPlugins @retry_delay = 2 if @retry_delay == UNSET_VALUE @timeout = 1800 if @timeout == UNSET_VALUE @ssl_peer_verification = true if @ssl_peer_verification == UNSET_VALUE + @execution_time_limit = "PT2H" if @execution_time_limit == UNSET_VALUE end def validate(machine) @@ -49,6 +52,7 @@ module VagrantPlugins errors << "winrm.max_tries cannot be nil." if @max_tries.nil? errors << "winrm.retry_delay cannot be nil." if @max_tries.nil? errors << "winrm.timeout cannot be nil." if @timeout.nil? + errors << "winrm.execution_time_limit cannot be nil." if @execution_time_limit.nil? unless @ssl_peer_verification == true || @ssl_peer_verification == false errors << "winrm.ssl_peer_verification must be a boolean." end diff --git a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb index 17767e436..66bc94043 100644 --- a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb +++ b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb @@ -1,4 +1,4 @@ -param([String]$username, [String]$password, [String]$encoded_command) +param([String]$username, [String]$password, [String]$encoded_command, [String]$execution_time_limit) $task_name = "WinRM_Elevated_Shell" $out_file = "$env:SystemRoot\Temp\WinRM_Elevated_Shell.log" @@ -33,7 +33,7 @@ $task_xml = @' false false false - PT2H + {execution_time_limit} 4 @@ -49,6 +49,7 @@ $arguments = "/c powershell.exe -EncodedCommand $encoded_command > $out_file $task_xml = $task_xml.Replace("{arguments}", $arguments) $task_xml = $task_xml.Replace("{username}", $username) +$task_xml = $task_xml.Replace("{execution_time_limit}", $execution_time_limit) $schedule = New-Object -ComObject "Schedule.Service" $schedule.Connect() diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index 9b0f4e302..967565046 100644 --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -37,6 +37,7 @@ module VagrantPlugins attr_reader :port attr_reader :username attr_reader :password + attr_reader :execution_time_limit attr_reader :config def initialize(host, port, config) @@ -47,6 +48,7 @@ module VagrantPlugins @port = port @username = config.username @password = config.password + @execution_time_limit = config.execution_time_limit @config = config end From 44154c92a90e32b5b5189ece0d12f086198bdd77 Mon Sep 17 00:00:00 2001 From: Jeremy Roberts Date: Sun, 30 Aug 2015 21:08:39 -0400 Subject: [PATCH 041/484] Fixing WinRM communicator unit test. --- test/unit/plugins/communicators/winrm/communicator_test.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/plugins/communicators/winrm/communicator_test.rb b/test/unit/plugins/communicators/winrm/communicator_test.rb index ceea0985d..42588a511 100644 --- a/test/unit/plugins/communicators/winrm/communicator_test.rb +++ b/test/unit/plugins/communicators/winrm/communicator_test.rb @@ -20,6 +20,7 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do before do allow(shell).to receive(:username).and_return('vagrant') allow(shell).to receive(:password).and_return('password') + allow(shell).to receive(:execution_time_limit).and_return('PT2H') end describe ".ready?" do @@ -56,7 +57,7 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do expect(shell).to receive(:upload).with(kind_of(String), "c:/tmp/vagrant-elevated-shell.ps1") expect(shell).to receive(:powershell) do |cmd| expect(cmd).to eq("powershell -executionpolicy bypass -file \"c:/tmp/vagrant-elevated-shell.ps1\" " + - "-username \"vagrant\" -password \"password\" -encoded_command \"ZABpAHIAOwAgAGUAeABpAHQAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAA==\"") + "-username \"vagrant\" -password \"password\" -encoded_command \"ZABpAHIAOwAgAGUAeABpAHQAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAA==\" -execution_time_limit \"PT2H\"") end.and_return({ exitcode: 0 }) expect(subject.execute("dir", { elevated: true })).to eq(0) end From c844a9c4fd587c78b8ada9c2ac521712aa99f0f2 Mon Sep 17 00:00:00 2001 From: Jeremy Roberts Date: Mon, 31 Aug 2015 11:27:08 -0400 Subject: [PATCH 042/484] Adding WinRM execution_time_limit to log. --- plugins/communicators/winrm/communicator.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb index 38fda78f9..6a11d985c 100644 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -38,6 +38,7 @@ module VagrantPlugins # Got it! Let the user know what we're connecting to. @machine.ui.detail("WinRM address: #{shell.host}:#{shell.port}") @machine.ui.detail("WinRM username: #{shell.username}") + @machine.ui.detail("WinRM execution_time_limit: #{shell.execution_time_limit}") @machine.ui.detail("WinRM transport: #{shell.config.transport}") last_message = nil From c7186236f1ae30b6f4824b5aa2c56574c2fd7e5c Mon Sep 17 00:00:00 2001 From: Jeff Goldschrafe Date: Mon, 31 Aug 2015 16:50:58 -0400 Subject: [PATCH 043/484] Better Ubuntu systemd detection Check the running process at PID 1 to determine which init system is currently in use. --- plugins/guests/ubuntu/cap/change_host_name.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/guests/ubuntu/cap/change_host_name.rb b/plugins/guests/ubuntu/cap/change_host_name.rb index 706d4850a..df68a010a 100644 --- a/plugins/guests/ubuntu/cap/change_host_name.rb +++ b/plugins/guests/ubuntu/cap/change_host_name.rb @@ -7,7 +7,7 @@ module VagrantPlugins end def update_etc_hostname - return super unless vivid? + return super unless systemd? sudo("hostnamectl set-hostname '#{short_hostname}'") end @@ -15,7 +15,7 @@ module VagrantPlugins if hardy? # hostname.sh returns 1, so use `true` to get a 0 exitcode sudo("/etc/init.d/hostname.sh start; true") - elsif vivid? + elsif systemd? # Service runs via hostnamectl else sudo("service hostname start") @@ -26,19 +26,25 @@ module VagrantPlugins os_version("hardy") end - def vivid? - os_version("vivid") - end - def renew_dhcp sudo("ifdown -a; ifup -a; ifup -a --allow=hotplug") end private + def init_package + machine.communicate.execute('cat /proc/1/comm') do |type, data| + return data.chomp if type == :stdout + end + end + def os_version(name) machine.communicate.test("[ `lsb_release -c -s` = #{name} ]") end + + def systemd? + init_package == 'systemd' + end end end end From 1e84cc4d6aa5eff2f27fc2e13663aa9734336a4f Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Wed, 2 Sep 2015 16:36:23 -0500 Subject: [PATCH 044/484] communicators/winrm: respect boot_timeout when fetching winrm_info We gained a ton of improvemnts to WinRM error handling in https://github.com/mitchellh/vagrant/pull/4943, but we also got one bug. The new code raises an exception when `winrm_info` does not return right away. This was preventing us from catching the retry/timout logic that's meant to wait until boot_timeout for the WinRM communicator to be ready. This restores the proper behavior by rescuing the WinRMNotReady exception and continuing to retry until the surrounding timeout fires. --- plugins/communicators/winrm/communicator.rb | 7 +++- .../communicators/winrm/communicator_test.rb | 38 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb index fdb3fe615..b14b28efa 100644 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -30,7 +30,12 @@ module VagrantPlugins # Wait for winrm_info to be ready winrm_info = nil while true - winrm_info = Helper.winrm_info(@machine) + winrm_info = nil + begin + winrm_info = Helper.winrm_info(@machine) + rescue Errors::WinRMNotReady + @logger.debug("WinRM not ready yet; retrying until boot_timeout is reached.") + end break if winrm_info sleep 0.5 end diff --git a/test/unit/plugins/communicators/winrm/communicator_test.rb b/test/unit/plugins/communicators/winrm/communicator_test.rb index ceea0985d..e98f646b3 100644 --- a/test/unit/plugins/communicators/winrm/communicator_test.rb +++ b/test/unit/plugins/communicators/winrm/communicator_test.rb @@ -5,10 +5,11 @@ require Vagrant.source_root.join("plugins/communicators/winrm/communicator") describe VagrantPlugins::CommunicatorWinRM::Communicator do include_context "unit" - let(:winrm) { double("winrm", timeout: 1) } + let(:winrm) { double("winrm", timeout: 1, host: nil, port: 5986, guest_port: 5986) } let(:config) { double("config", winrm: winrm) } - let(:machine) { double("machine", config: config) } - + let(:provider) { double("provider") } + let(:ui) { double("ui") } + let(:machine) { double("machine", config: config, provider: provider, ui: ui) } let(:shell) { double("shell") } subject do @@ -22,6 +23,37 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do allow(shell).to receive(:password).and_return('password') end + describe ".wait_for_ready" do + context "with no winrm_info capability and no static config (default scenario)" do + before do + # No default providers support this capability + allow(provider).to receive(:capability?).with(:winrm_info).and_return(false) + + # Get us through the detail prints + allow(ui).to receive(:detail) + allow(shell).to receive(:host) + allow(shell).to receive(:port) + allow(shell).to receive(:username) + allow(shell).to receive(:config) { double("config", transport: nil)} + end + + context "when ssh_info requires a multiple tries before it is ready" do + before do + allow(machine).to receive(:ssh_info).and_return(nil, { + host: '10.1.2.3', + port: '22', + }) + # Makes ready? return true + allow(shell).to receive(:powershell).with("hostname").and_return({ exitcode: 0 }) + end + + it "retries ssh_info until ready" do + expect(subject.wait_for_ready(2)).to eq(true) + end + end + end + end + describe ".ready?" do it "returns true if hostname command executes without error" do expect(shell).to receive(:powershell).with("hostname").and_return({ exitcode: 0 }) From 4b32744424ee3c0307ee9a24ad9bba330e9ad5ed Mon Sep 17 00:00:00 2001 From: Jeff Kwan Date: Thu, 3 Sep 2015 16:54:41 -0400 Subject: [PATCH 045/484] Use a .NET API call instead of a Win8+ cmdlet The root cause is that Windows 7 doesn't have Get-NetIPAddress ( see: https://stackoverflow.com/questions/19529442/gather-ip-address-information ) but the change was to try and solve the bug that the VPN IP addresses aren't visible detailed here: https://support.microsoft.com/en-us/kb/2549091 Resolved using the 2nd solution from http://serverfault.com/questions/145259/powershell-win32-networkadapterconfiguration-not-seeing-ppp-adapter --- .../synced_folders/smb/scripts/host_info.ps1 | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/plugins/synced_folders/smb/scripts/host_info.ps1 b/plugins/synced_folders/smb/scripts/host_info.ps1 index 0e089932e..c4f13aada 100644 --- a/plugins/synced_folders/smb/scripts/host_info.ps1 +++ b/plugins/synced_folders/smb/scripts/host_info.ps1 @@ -1,11 +1,21 @@ -$ErrorAction = "Stop" - -$net = Get-NetIPAddress | Where-Object { - ($_.IPAddress -ne "127.0.0.1") -and ($_.IPAddress -ne "::1") -} | Sort-Object $_.AddressFamily - -$result = @{ - ip_addresses = $net.IPAddress -} - -Write-Output $(ConvertTo-Json $result) +$ErrorAction = "Stop" + +# Find all of the NICsq +$nics = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() + +# Save the IP addresses somewhere +$nic_ip_addresses = @() + +foreach ($nic in $nics) { + $nic_ip_addresses += $nic.GetIPProperties().UnicastAddresses | Where-Object { + ($_.Address.IPAddressToString -ne "127.0.0.1") -and ($_.Address.IPAddressToString -ne "::1") + } | Select -ExpandProperty Address +} + +$nic_ip_addresses = $nic_ip_addresses | Sort-Object $_.AddressFamily + +$result = @{ + ip_addresses = $nic_ip_addresses.IPAddressToString +} + +Write-Output $(ConvertTo-Json $result) From 1911586832132f93669d2db58a2add78b70e432b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= Date: Thu, 10 Sep 2015 10:05:14 +0200 Subject: [PATCH 046/484] Better NFS status check command for SUSE --- plugins/hosts/suse/cap/nfs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/hosts/suse/cap/nfs.rb b/plugins/hosts/suse/cap/nfs.rb index 42bde6bb2..789c6cb47 100644 --- a/plugins/hosts/suse/cap/nfs.rb +++ b/plugins/hosts/suse/cap/nfs.rb @@ -7,7 +7,7 @@ module VagrantPlugins end def self.nfs_check_command(env) - "pidof nfsd > /dev/null" + "/sbin/service nfsserver status" end def self.nfs_start_command(env) From 33b4d6a63d9275ce9b3156883cac32551a79038d Mon Sep 17 00:00:00 2001 From: Philip Wigg Date: Fri, 11 Sep 2015 20:06:21 +0100 Subject: [PATCH 047/484] Fix verify_binary for Puppet for Windows guests. --- plugins/provisioners/puppet/provisioner/puppet.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/provisioners/puppet/provisioner/puppet.rb b/plugins/provisioners/puppet/provisioner/puppet.rb index f1987dbd5..6cd8716aa 100644 --- a/plugins/provisioners/puppet/provisioner/puppet.rb +++ b/plugins/provisioners/puppet/provisioner/puppet.rb @@ -150,7 +150,7 @@ module VagrantPlugins # This is very platform dependent. test_cmd = "sh -c 'command -v #{binary}'" if windows? - test_cmd = "which #{binary}" + test_cmd = "where #{binary}" if @config.binary_path test_cmd = "where \"#{@config.binary_path}:#{binary}\"" end From acde6e1b16674f3d63f6d2e94e43ce57c95641fa Mon Sep 17 00:00:00 2001 From: Rickard von Essen Date: Sun, 20 Sep 2015 09:30:49 +0200 Subject: [PATCH 048/484] Use dnf on Fedora guests instead of yum if available. Fixes #6286 now properly installs Docker on Fedora guests. Fixes #6287 use dnf if available. --- plugins/guests/redhat/cap/nfs_client.rb | 6 +++- plugins/guests/redhat/cap/rsync.rb | 6 +++- plugins/guests/redhat/plugin.rb | 4 +++ .../cfengine/cap/redhat/cfengine_install.rb | 12 ++++++- .../chef/cap/redhat/chef_install.rb | 12 ++++++- .../docker/cap/fedora/docker_install.rb | 31 +++++++++++++++++++ plugins/provisioners/docker/plugin.rb | 5 +++ 7 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 plugins/provisioners/docker/cap/fedora/docker_install.rb diff --git a/plugins/guests/redhat/cap/nfs_client.rb b/plugins/guests/redhat/cap/nfs_client.rb index ef88e9900..78e2d5bca 100644 --- a/plugins/guests/redhat/cap/nfs_client.rb +++ b/plugins/guests/redhat/cap/nfs_client.rb @@ -3,7 +3,11 @@ module VagrantPlugins module Cap class NFSClient def self.nfs_client_install(machine) - machine.communicate.sudo("yum -y install nfs-utils nfs-utils-lib") + if VagrantPlugins::GuestRedHat::Plugin.dnf?(machine) + machine.communicate.sudo("dnf -y install nfs-utils nfs-utils-lib") + else + machine.communicate.sudo("yum -y install nfs-utils nfs-utils-lib") + end restart_nfs(machine) end diff --git a/plugins/guests/redhat/cap/rsync.rb b/plugins/guests/redhat/cap/rsync.rb index 9a1cfeeac..8eace63f0 100644 --- a/plugins/guests/redhat/cap/rsync.rb +++ b/plugins/guests/redhat/cap/rsync.rb @@ -4,7 +4,11 @@ module VagrantPlugins class RSync def self.rsync_install(machine) machine.communicate.tap do |comm| - comm.sudo("yum -y install rsync") + if VagrantPlugins::GuestRedHat::Plugin.dnf?(machine) + comm.sudo("dnf -y install rsync") + else + comm.sudo("yum -y install rsync") + end end end end diff --git a/plugins/guests/redhat/plugin.rb b/plugins/guests/redhat/plugin.rb index 96e641bcb..0a97febee 100644 --- a/plugins/guests/redhat/plugin.rb +++ b/plugins/guests/redhat/plugin.rb @@ -45,6 +45,10 @@ module VagrantPlugins require_relative "cap/rsync" Cap::RSync end + + def self.dnf?(machine) + machine.communicate.test("/usr/bin/which -s dnf") + end end end end diff --git a/plugins/provisioners/cfengine/cap/redhat/cfengine_install.rb b/plugins/provisioners/cfengine/cap/redhat/cfengine_install.rb index 828b336d6..f22979388 100644 --- a/plugins/provisioners/cfengine/cap/redhat/cfengine_install.rb +++ b/plugins/provisioners/cfengine/cap/redhat/cfengine_install.rb @@ -14,9 +14,19 @@ module VagrantPlugins logger.info("Installing CFEngine Community Yum Repository GPG KEY from #{config.repo_gpg_key_url}") comm.sudo("GPGFILE=$(mktemp) && wget -O $GPGFILE #{config.repo_gpg_key_url} && rpm --import $GPGFILE; rm -f $GPGFILE") - comm.sudo("yum -y install #{config.package_name}") + if dnf?(machine) + comm.sudo("dnf -y install #{config.package_name}") + else + comm.sudo("yum -y install #{config.package_name}") + end end end + + protected + + def self.dnf?(machine) + machine.communicate.test("/usr/bin/which -s dnf") + end end end end diff --git a/plugins/provisioners/chef/cap/redhat/chef_install.rb b/plugins/provisioners/chef/cap/redhat/chef_install.rb index c6aff7bad..7d376e7d0 100644 --- a/plugins/provisioners/chef/cap/redhat/chef_install.rb +++ b/plugins/provisioners/chef/cap/redhat/chef_install.rb @@ -6,11 +6,21 @@ module VagrantPlugins module Redhat module ChefInstall def self.chef_install(machine, version, prerelease, download_path) - machine.communicate.sudo("yum install -y -q curl") + if dnf?(machine) + machine.communicate.sudo("dnf install -y -q curl") + else + machine.communicate.sudo("yum install -y -q curl") + end command = Omnibus.build_command(version, prerelease, download_path) machine.communicate.sudo(command) end + + protected + + def self.dnf?(machine) + machine.communicate.test("/usr/bin/which -s dnf") + end end end end diff --git a/plugins/provisioners/docker/cap/fedora/docker_install.rb b/plugins/provisioners/docker/cap/fedora/docker_install.rb new file mode 100644 index 000000000..16377f410 --- /dev/null +++ b/plugins/provisioners/docker/cap/fedora/docker_install.rb @@ -0,0 +1,31 @@ +module VagrantPlugins + module DockerProvisioner + module Cap + module Fedora + module DockerInstall + def self.docker_install(machine, version) + if version != :latest + machine.ui.warn(I18n.t("vagrant.docker_install_with_version_not_supported")) + end + + machine.communicate.tap do |comm| + if dnf?(machine) + comm.sudo("dnf -y install docker") + else + comm.sudo("yum -y install docker") + end + comm.sudo("systemctl start docker.service") + comm.sudo("systemctl enable docker.service") + end + end + + protected + + def self.dnf?(machine) + machine.communicate.test("/usr/bin/which -s dnf") + end + end + end + end + end +end diff --git a/plugins/provisioners/docker/plugin.rb b/plugins/provisioners/docker/plugin.rb index b606729ac..b39422cd0 100644 --- a/plugins/provisioners/docker/plugin.rb +++ b/plugins/provisioners/docker/plugin.rb @@ -24,6 +24,11 @@ module VagrantPlugins Cap::Debian::DockerStartService end + guest_capability("fedora", "docker_install") do + require_relative "cap/fedora/docker_install" + Cap::Fedora::DockerInstall + end + guest_capability("redhat", "docker_install") do require_relative "cap/redhat/docker_install" Cap::Redhat::DockerInstall From f3215f0ae62376bb9168d8868b9d5a1a7d6ad781 Mon Sep 17 00:00:00 2001 From: J Lynn Date: Wed, 23 Sep 2015 13:29:11 +1000 Subject: [PATCH 049/484] copied documentation from docs to website for use_dhcp_assigned_default_route in order to complete documentation --- .../docs/source/v2/networking/public_network.html.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/website/docs/source/v2/networking/public_network.html.md b/website/docs/source/v2/networking/public_network.html.md index e52cbaf58..913f6164e 100644 --- a/website/docs/source/v2/networking/public_network.html.md +++ b/website/docs/source/v2/networking/public_network.html.md @@ -51,6 +51,17 @@ When DHCP is used, the IP can be determined by using `vagrant ssh` to SSH into the machine and using the appropriate command line tool to find the IP, such as `ifconfig`. +### Using the DHCP Assigned Default Route + +Some cases require the DHCP assigned default route to be untouched. In these cases one +my specify the :use_dhcp_assigned_default_route option. As an example: + +```ruby +Vagrant.configure("2") do |config| + config.vm.network "public_network", use_dhcp_assigned_default_route: true +end +``` + ## Static IP Depending on your setup, you may wish to manually set the IP of your From f22bfcb8def85d5f4ba4634f6b7d1a81dc082aa2 Mon Sep 17 00:00:00 2001 From: J Lynn Date: Wed, 23 Sep 2015 13:58:01 +1000 Subject: [PATCH 050/484] resolve formatting issues --- website/docs/source/v2/networking/public_network.html.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/docs/source/v2/networking/public_network.html.md b/website/docs/source/v2/networking/public_network.html.md index 913f6164e..91b2443b7 100644 --- a/website/docs/source/v2/networking/public_network.html.md +++ b/website/docs/source/v2/networking/public_network.html.md @@ -54,11 +54,12 @@ the IP, such as `ifconfig`. ### Using the DHCP Assigned Default Route Some cases require the DHCP assigned default route to be untouched. In these cases one -my specify the :use_dhcp_assigned_default_route option. As an example: +may specify the `use_dhcp_assigned_default_route` option. As an example: ```ruby Vagrant.configure("2") do |config| - config.vm.network "public_network", use_dhcp_assigned_default_route: true + config.vm.network "public_network", + use_dhcp_assigned_default_route: true end ``` From ed1c219a07180bb525d2eaa915625656bdefc888 Mon Sep 17 00:00:00 2001 From: Trey Briggs Date: Tue, 22 Sep 2015 23:26:02 -0500 Subject: [PATCH 051/484] version is a Symbol, convert to String before concat. --- plugins/provisioners/chef/cap/windows/chef_installed.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/provisioners/chef/cap/windows/chef_installed.rb b/plugins/provisioners/chef/cap/windows/chef_installed.rb index c95ea8498..6ab3bd4b7 100644 --- a/plugins/provisioners/chef/cap/windows/chef_installed.rb +++ b/plugins/provisioners/chef/cap/windows/chef_installed.rb @@ -7,7 +7,7 @@ module VagrantPlugins # @return [true, false] def self.chef_installed(machine, version) if version != :latest - command = 'if ((&knife --version) -Match "Chef: "' + version + '"){ exit 0 } else { exit 1 }' + command = 'if ((&knife --version) -Match "Chef: "' + version.to_s + '"){ exit 0 } else { exit 1 }' else command = 'if ((&knife --version) -Match "Chef: *"){ exit 0 } else { exit 1 }' end From a5b6e23e201f57dcd2ef66373046ca4be921b349 Mon Sep 17 00:00:00 2001 From: Trey Briggs Date: Tue, 22 Sep 2015 23:28:14 -0500 Subject: [PATCH 052/484] Removed extra quote in powershell command. --- plugins/provisioners/chef/cap/windows/chef_installed.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/provisioners/chef/cap/windows/chef_installed.rb b/plugins/provisioners/chef/cap/windows/chef_installed.rb index 6ab3bd4b7..b08794833 100644 --- a/plugins/provisioners/chef/cap/windows/chef_installed.rb +++ b/plugins/provisioners/chef/cap/windows/chef_installed.rb @@ -7,7 +7,7 @@ module VagrantPlugins # @return [true, false] def self.chef_installed(machine, version) if version != :latest - command = 'if ((&knife --version) -Match "Chef: "' + version.to_s + '"){ exit 0 } else { exit 1 }' + command = 'if ((&knife --version) -Match "Chef: ' + version.to_s + '"){ exit 0 } else { exit 1 }' else command = 'if ((&knife --version) -Match "Chef: *"){ exit 0 } else { exit 1 }' end From e0dad41b0c04de7a35b4fbf44b23eea1b41d1e4e Mon Sep 17 00:00:00 2001 From: Maarten De Wispelaere Date: Thu, 24 Sep 2015 12:18:59 +0200 Subject: [PATCH 053/484] FIX: no exception for debian 8 needed, shutdown -h -H doesn't work ; use normal shutdown -h now --- plugins/guests/debian8/cap/halt.rb | 16 ---------------- plugins/guests/debian8/guest.rb | 9 --------- plugins/guests/debian8/plugin.rb | 21 --------------------- 3 files changed, 46 deletions(-) delete mode 100644 plugins/guests/debian8/cap/halt.rb delete mode 100644 plugins/guests/debian8/guest.rb delete mode 100644 plugins/guests/debian8/plugin.rb diff --git a/plugins/guests/debian8/cap/halt.rb b/plugins/guests/debian8/cap/halt.rb deleted file mode 100644 index 932281347..000000000 --- a/plugins/guests/debian8/cap/halt.rb +++ /dev/null @@ -1,16 +0,0 @@ -module VagrantPlugins - module GuestDebian8 - module Cap - class Halt - def self.halt(machine) - begin - machine.communicate.sudo("shutdown -h -H") - rescue IOError - # Do nothing, because it probably means the machine shut down - # and SSH connection was lost. - end - end - end - end - end -end diff --git a/plugins/guests/debian8/guest.rb b/plugins/guests/debian8/guest.rb deleted file mode 100644 index ec95e9e85..000000000 --- a/plugins/guests/debian8/guest.rb +++ /dev/null @@ -1,9 +0,0 @@ -module VagrantPlugins - module GuestDebian8 - class Guest < Vagrant.plugin("2", :guest) - def detect?(machine) - machine.communicate.test("cat /etc/issue | grep 'Debian' | grep '8'") - end - end - end -end diff --git a/plugins/guests/debian8/plugin.rb b/plugins/guests/debian8/plugin.rb deleted file mode 100644 index 1f566f5fa..000000000 --- a/plugins/guests/debian8/plugin.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "vagrant" - -module VagrantPlugins - module GuestDebian8 - class Plugin < Vagrant.plugin("2") - name "Debian Jessie guest" - description "Debian Jessie guest support." - - guest("debian8", "debian") do - require File.expand_path("../guest", __FILE__) - Guest - end - - guest_capability("debian8", "halt") do - require_relative "cap/halt" - Cap::Halt - end - - end - end -end From 599feb5d0e179a2c0647109f238627a73016d014 Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Thu, 24 Sep 2015 16:41:40 -0700 Subject: [PATCH 054/484] Add Makefile to `website/docs` --- website/docs/Makefile | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 website/docs/Makefile diff --git a/website/docs/Makefile b/website/docs/Makefile new file mode 100644 index 000000000..63bb4cab1 --- /dev/null +++ b/website/docs/Makefile @@ -0,0 +1,10 @@ +all: build + +init: + bundle + +dev: init + bundle exec middleman server + +build: init + bundle exec middleman build \ No newline at end of file From b4bd0c925eeb4a4674d67de7f5f47436c9e04569 Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Thu, 24 Sep 2015 16:42:27 -0700 Subject: [PATCH 055/484] Point website/docs README to Makefile and /v2 url --- website/docs/README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/website/docs/README.md b/website/docs/README.md index ca4c038fe..e9ac25640 100644 --- a/website/docs/README.md +++ b/website/docs/README.md @@ -14,13 +14,7 @@ requests like any normal GitHub project, and we'll merge it in. ## Running the Site Locally -Running the site locally is simple. Clone this repo and run the following -commands: +Running the site locally is simple. Clone this repo and run `make dev`. -``` -$ bundle -$ bundle exec middleman server -``` - -Then open up `localhost:4567`. Note that some URLs you may need to append +Then open up `localhost:4567/v2`. Note that some URLs you may need to append ".html" to make them work (in the navigation and such). From 73314cfcbef8ca659cf8aadcce75fcecb0c5f1c2 Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Thu, 24 Sep 2015 16:44:08 -0700 Subject: [PATCH 056/484] Add 'Edit this page' link to website/docs footer --- website/docs/source/layouts/layout.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/source/layouts/layout.erb b/website/docs/source/layouts/layout.erb index 4fedc3627..38a735de3 100644 --- a/website/docs/source/layouts/layout.erb +++ b/website/docs/source/layouts/layout.erb @@ -324,6 +324,8 @@
  • Documentation
  • About
  • Support
  • + <% github_link = 'https://github.com/mitchellh/vagrant/blob/master/' + current_page.source_file.match(/website.*/)[0] %> +
  • Edit this page
  • Download
  • From ab590f7740628cb7cf22550d22faa16452e8a2b8 Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Thu, 24 Sep 2015 16:44:54 -0700 Subject: [PATCH 057/484] Add Makefile to `website/www` --- website/www/Makefile | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 website/www/Makefile diff --git a/website/www/Makefile b/website/www/Makefile new file mode 100644 index 000000000..63bb4cab1 --- /dev/null +++ b/website/www/Makefile @@ -0,0 +1,10 @@ +all: build + +init: + bundle + +dev: init + bundle exec middleman server + +build: init + bundle exec middleman build \ No newline at end of file From 33d602ba84a805be2e88b0ee71ef984026cb45e6 Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Thu, 24 Sep 2015 16:45:43 -0700 Subject: [PATCH 058/484] Point `website/www` README to Makefile --- website/www/README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/website/www/README.md b/website/www/README.md index 814da3877..3645fa52c 100644 --- a/website/www/README.md +++ b/website/www/README.md @@ -14,13 +14,7 @@ requests like any normal GitHub project, and we'll merge it in. ## Running the Site Locally -Running the site locally is simple. Clone this repo and run the following -commands: - -``` -$ bundle -$ bundle exec middleman server -``` +Running the site locally is simple. Clone this repo and run `make dev`. Then open up `localhost:4567`. Note that some URLs you may need to append ".html" to make them work (in the navigation and such). From 094d8b89efc14a9c47c9dcd7833692e791f04025 Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Thu, 24 Sep 2015 16:47:14 -0700 Subject: [PATCH 059/484] Add 'Edit this page' link to website/www footer --- website/www/source/layouts/layout.erb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/www/source/layouts/layout.erb b/website/www/source/layouts/layout.erb index 4e6736743..2daba176d 100644 --- a/website/www/source/layouts/layout.erb +++ b/website/www/source/layouts/layout.erb @@ -59,6 +59,10 @@
  • Documentation
  • About
  • Support
  • + <% if current_page.url != "/" %> + <% github_link = 'https://github.com/mitchellh/vagrant/blob/master/' + current_page.source_file.match(/website.*/)[0] %> +
  • Edit this page
  • + <% end %>
  • Download
  • From 6b033f2d65b3f347f813969fb032ef5ac3f62d4b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 25 Sep 2015 15:14:12 +0200 Subject: [PATCH 060/484] doc: mention "directory" in the file provisioner reference --- .../docs/source/v2/provisioning/file.html.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/website/docs/source/v2/provisioning/file.html.md b/website/docs/source/v2/provisioning/file.html.md index ca65c8bb9..0fab619ce 100644 --- a/website/docs/source/v2/provisioning/file.html.md +++ b/website/docs/source/v2/provisioning/file.html.md @@ -7,8 +7,8 @@ sidebar_current: "provisioning-file" **Provisioner name: `"file"`** -The file provisioner allows you to upload a file from the host machine to -the guest machine. +The file provisioner allows you to upload a file or directory from the host +machine to the guest machine. File provisioning is a simple way to, for example, replicate your local ~/.gitconfig to the vagrant user's home directory on the guest machine so @@ -21,19 +21,20 @@ new VM. config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig" end -Note that, unlike with synced folders, files 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 reflected -in the copy you uploaded to the guest machine. +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 +reflected in the copy you uploaded to the guest machine. ## Options The file provisioner takes only two options, both of which are required: -* `source` (string) - Is the local path of the file to be uploaded. +* `source` (string) - Is the local path of the file or directory to be + uploaded. * `destination` (string) - Is the remote path on the guest machine where - the file will be uploaded to. The file is uploaded as the SSH user over - SCP, so this location must be writable to that user. The SSH user can be + 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". From fd593a85b7034229e2428e2caa1a38355f0057e8 Mon Sep 17 00:00:00 2001 From: Brian Dwyer Date: Sun, 27 Sep 2015 23:23:28 -0400 Subject: [PATCH 061/484] Add `nodes_path` support for the Chef-Solo provisioner --- plugins/provisioners/chef/config/chef_solo.rb | 7 +++++++ plugins/provisioners/chef/provisioner/chef_solo.rb | 2 ++ templates/provisioners/chef_solo/solo.erb | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/provisioners/chef/config/chef_solo.rb b/plugins/provisioners/chef/config/chef_solo.rb index ee4a2ac2e..a9630a7b8 100644 --- a/plugins/provisioners/chef/config/chef_solo.rb +++ b/plugins/provisioners/chef/config/chef_solo.rb @@ -17,6 +17,10 @@ module VagrantPlugins # @return [String] attr_accessor :environments_path + # The path where nodes are stored on disk. + # @return [String] + attr_accessor :nodes_path + # A URL download a remote recipe from. Note: you should use chef-apply # instead. # @@ -39,6 +43,7 @@ module VagrantPlugins @cookbooks_path = UNSET_VALUE @data_bags_path = UNSET_VALUE @environments_path = UNSET_VALUE + @nodes_path = UNSET_VALUE @recipe_url = UNSET_VALUE @roles_path = UNSET_VALUE @synced_folder_type = UNSET_VALUE @@ -86,6 +91,7 @@ module VagrantPlugins end @data_bags_path = [] if @data_bags_path == UNSET_VALUE + @nodes_path = [] if @nodes_path == UNSET_VALUE @roles_path = [] if @roles_path == UNSET_VALUE @environments_path = [] if @environments_path == UNSET_VALUE @environments_path = [@environments_path].flatten @@ -93,6 +99,7 @@ module VagrantPlugins # Make sure the path is an array. @cookbooks_path = prepare_folders_config(@cookbooks_path) @data_bags_path = prepare_folders_config(@data_bags_path) + @nodes_path = prepare_folders_config(@nodes_path) @roles_path = prepare_folders_config(@roles_path) @environments_path = prepare_folders_config(@environments_path) end diff --git a/plugins/provisioners/chef/provisioner/chef_solo.rb b/plugins/provisioners/chef/provisioner/chef_solo.rb index 14ee5747c..1fc29da6e 100644 --- a/plugins/provisioners/chef/provisioner/chef_solo.rb +++ b/plugins/provisioners/chef/provisioner/chef_solo.rb @@ -19,6 +19,7 @@ module VagrantPlugins attr_reader :environments_folders attr_reader :cookbook_folders + attr_reader :node_folders attr_reader :role_folders attr_reader :data_bags_folders @@ -160,6 +161,7 @@ module VagrantPlugins { cookbooks_path: guest_paths(@cookbook_folders), recipe_url: @config.recipe_url, + nodes_path: guest_paths(@node_folders), roles_path: guest_paths(@role_folders), data_bags_path: guest_paths(@data_bags_folders).first, environments_path: guest_paths(@environments_folders).first diff --git a/templates/provisioners/chef_solo/solo.erb b/templates/provisioners/chef_solo/solo.erb index 25d3346b7..a3b01ed3b 100644 --- a/templates/provisioners/chef_solo/solo.erb +++ b/templates/provisioners/chef_solo/solo.erb @@ -31,8 +31,8 @@ environment "<%= environment %>" <% if local_mode -%> local_mode true <% end -%> -<% if node_path -%> -node_path <%= node_path.inspect %> +<% if nodes_path -%> +node_path <%= nodes_path.inspect %> <% end -%> http_proxy <%= http_proxy.inspect %> From c23610d70379f03ac27ba281356def7108aca868 Mon Sep 17 00:00:00 2001 From: Brian Dwyer Date: Sun, 27 Sep 2015 23:24:01 -0400 Subject: [PATCH 062/484] Update test --- .../plugins/provisioners/chef/config/chef_solo_test.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/unit/plugins/provisioners/chef/config/chef_solo_test.rb b/test/unit/plugins/provisioners/chef/config/chef_solo_test.rb index d6a2a03af..2c699cbd2 100644 --- a/test/unit/plugins/provisioners/chef/config/chef_solo_test.rb +++ b/test/unit/plugins/provisioners/chef/config/chef_solo_test.rb @@ -57,6 +57,14 @@ describe VagrantPlugins::Chef::Config::ChefSolo do end end + describe "#nodes_path" do + it "defaults to an empty array" do + subject.finalize! + expect(subject.nodes_path).to be_a(Array) + expect(subject.nodes_path).to be_empty + end + end + describe "#synced_folder_type" do it "defaults to nil" do subject.finalize! From 8fb2e0b4a54f38a93c133928c630101aa55a7e3c Mon Sep 17 00:00:00 2001 From: Brian Dwyer Date: Sun, 27 Sep 2015 23:24:40 -0400 Subject: [PATCH 063/484] Update documentation for Chef Solo `nodes_path` --- website/docs/source/v2/provisioning/chef_solo.html.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/docs/source/v2/provisioning/chef_solo.html.md b/website/docs/source/v2/provisioning/chef_solo.html.md index a7841b90e..a43232bed 100644 --- a/website/docs/source/v2/provisioning/chef_solo.html.md +++ b/website/docs/source/v2/provisioning/chef_solo.html.md @@ -42,6 +42,9 @@ available below this section. * `environments_path` (string) - A path where environment definitions are located. By default, no environments folder is set. +* `nodes_path` (string or array) - A list of paths where node objects (in JSON format) are stored. By default, no + nodes path is set. + * `environment` (string) - The environment you want the Chef run to be a part of. This requires Chef 11.6.0 or later, and that `environments_path` is set. From cfd4270cdb36833e5a4d322641c68b7a53cacc58 Mon Sep 17 00:00:00 2001 From: Maarten De Wispelaere Date: Mon, 28 Sep 2015 09:08:20 +0200 Subject: [PATCH 064/484] FIX bug introduced in #6315 --- plugins/guests/debian/guest.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/guests/debian/guest.rb b/plugins/guests/debian/guest.rb index be08b3605..a7f3d65de 100644 --- a/plugins/guests/debian/guest.rb +++ b/plugins/guests/debian/guest.rb @@ -2,7 +2,7 @@ module VagrantPlugins module GuestDebian class Guest < Vagrant.plugin("2", :guest) def detect?(machine) - machine.communicate.test("cat /etc/issue | grep 'Debian' | grep -v '8'") + machine.communicate.test("cat /etc/issue | grep 'Debian'") end end end From 702e8d6324a9485f2d1e6c46c9cee5aa8316405d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= Date: Thu, 10 Sep 2015 10:05:41 +0200 Subject: [PATCH 065/484] Add a sudoers script for SUSE --- contrib/sudoers/linux-suse | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 contrib/sudoers/linux-suse diff --git a/contrib/sudoers/linux-suse b/contrib/sudoers/linux-suse new file mode 100644 index 000000000..30658af3f --- /dev/null +++ b/contrib/sudoers/linux-suse @@ -0,0 +1,7 @@ +Cmnd_Alias VAGRANT_EXPORTS_ADD = /usr/bin/tee -a /etc/exports +Cmnd_Alias VAGRANT_NFSD_CHECK = /sbin/service nfsserver status +Cmnd_Alias VAGRANT_NFSD_START = /sbin/service nfsserver start +Cmnd_Alias VAGRANT_NFSD_APPLY = /usr/sbin/exportfs -ar +Cmnd_Alias VAGRANT_EXPORTS_REMOVE = /usr/bin/sed -r -e * d -ibak /*/exports +Cmnd_Alias VAGRANT_EXPORTS_REMOVE_2 = /usr/bin/cp /*/exports /etc/exports +%vagrant ALL=(root) NOPASSWD: VAGRANT_EXPORTS_ADD, VAGRANT_NFSD_CHECK, VAGRANT_NFSD_START, VAGRANT_NFSD_APPLY, VAGRANT_EXPORTS_REMOVE, VAGRANT_EXPORTS_REMOVE_2 From 04fb0d99f6fd3e878938ec31174843fa3ea8d812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20Gr=C3=B6nlund?= Date: Mon, 28 Sep 2015 09:17:40 +0200 Subject: [PATCH 066/484] Fix sudoers files after change to cleanup command PR #5773 modified the cleanup command so that the example sudoers scripts no longer match. --- contrib/sudoers/linux-fedora | 5 +++-- contrib/sudoers/linux-ubuntu | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contrib/sudoers/linux-fedora b/contrib/sudoers/linux-fedora index f2d64b66d..5bec89189 100644 --- a/contrib/sudoers/linux-fedora +++ b/contrib/sudoers/linux-fedora @@ -2,5 +2,6 @@ Cmnd_Alias VAGRANT_EXPORTS_ADD = /usr/bin/tee -a /etc/exports Cmnd_Alias VAGRANT_NFSD_CHECK = /usr/bin/systemctl status nfs-server.service Cmnd_Alias VAGRANT_NFSD_START = /usr/bin/systemctl start nfs-server.service Cmnd_Alias VAGRANT_NFSD_APPLY = /usr/sbin/exportfs -ar -Cmnd_Alias VAGRANT_EXPORTS_REMOVE = /bin/sed -r -e * d -ibak /etc/exports -%vagrant ALL=(root) NOPASSWD: VAGRANT_EXPORTS_ADD, VAGRANT_NFSD_CHECK, VAGRANT_NFSD_START, VAGRANT_NFSD_APPLY, VAGRANT_EXPORTS_REMOVE +Cmnd_Alias VAGRANT_EXPORTS_REMOVE = /bin/sed -r -e * d -ibak /*/exports +Cmnd_Alias VAGRANT_EXPORTS_REMOVE_2 = /bin/cp /*/exports /etc/exports +%vagrant ALL=(root) NOPASSWD: VAGRANT_EXPORTS_ADD, VAGRANT_NFSD_CHECK, VAGRANT_NFSD_START, VAGRANT_NFSD_APPLY, VAGRANT_EXPORTS_REMOVE, VAGRANT_EXPORTS_REMOVE_2 diff --git a/contrib/sudoers/linux-ubuntu b/contrib/sudoers/linux-ubuntu index c4e786cf3..4e2cd8bde 100644 --- a/contrib/sudoers/linux-ubuntu +++ b/contrib/sudoers/linux-ubuntu @@ -4,5 +4,6 @@ Cmnd_Alias VAGRANT_EXPORTS_ADD = /usr/bin/tee -a /etc/exports Cmnd_Alias VAGRANT_NFSD_CHECK = /etc/init.d/nfs-kernel-server status Cmnd_Alias VAGRANT_NFSD_START = /etc/init.d/nfs-kernel-server start Cmnd_Alias VAGRANT_NFSD_APPLY = /usr/sbin/exportfs -ar -Cmnd_Alias VAGRANT_EXPORTS_REMOVE = /bin/sed -r -e * d -ibak /etc/exports -%sudo ALL=(root) NOPASSWD: VAGRANT_EXPORTS_ADD, VAGRANT_NFSD_CHECK, VAGRANT_NFSD_START, VAGRANT_NFSD_APPLY, VAGRANT_EXPORTS_REMOVE +Cmnd_Alias VAGRANT_EXPORTS_REMOVE = /bin/sed -r -e * d -ibak /*/exports +Cmnd_Alias VAGRANT_EXPORTS_REMOVE_2 = /bin/cp /*/exports /etc/exports +%sudo ALL=(root) NOPASSWD: VAGRANT_EXPORTS_ADD, VAGRANT_NFSD_CHECK, VAGRANT_NFSD_START, VAGRANT_NFSD_APPLY, VAGRANT_EXPORTS_REMOVE, VAGRANT_EXPORTS_REMOVE_2 From ec0b0fb7f9e12bcff1c246668268084ebd308bb6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Sep 2015 17:23:25 -0700 Subject: [PATCH 067/484] providers/virtualbox: IPv6 host only networks --- lib/vagrant/util/network_ip.rb | 11 +++ .../providers/virtualbox/action/network.rb | 72 ++++++++++++------- .../virtualbox/driver/version_4_3.rb | 24 +++++-- .../virtualbox/driver/version_5_0.rb | 4 ++ templates/guests/debian/network_static6.erb | 10 +++ test/unit/vagrant/util/network_ip_test.rb | 12 ++++ 6 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 templates/guests/debian/network_static6.erb diff --git a/lib/vagrant/util/network_ip.rb b/lib/vagrant/util/network_ip.rb index fca98f8b4..5f22f69d4 100644 --- a/lib/vagrant/util/network_ip.rb +++ b/lib/vagrant/util/network_ip.rb @@ -1,10 +1,21 @@ +require "ipaddr" + module Vagrant module Util module NetworkIP # Returns the network address of the given IP and subnet. # + # If the IP address is an IPv6 address, subnet should be a prefix + # length such as "64". + # # @return [String] def network_address(ip, subnet) + # If this is an IPv6 address, then just mask it + if subnet.to_s =~ /^\d+$/ + ip = IPAddr.new(ip) + return ip.mask(subnet.to_i).to_s + end + ip = ip_parts(ip) netmask = ip_parts(subnet) diff --git a/plugins/providers/virtualbox/action/network.rb b/plugins/providers/virtualbox/action/network.rb index 3697cdfef..881fcdf51 100644 --- a/plugins/providers/virtualbox/action/network.rb +++ b/plugins/providers/virtualbox/action/network.rb @@ -1,3 +1,4 @@ +require "ipaddr" require "set" require "log4r" @@ -248,7 +249,6 @@ module VagrantPlugins auto_config: true, mac: nil, nic_type: nil, - netmask: "255.255.255.0", type: :static }.merge(options) @@ -258,31 +258,45 @@ module VagrantPlugins # Default IP is in the 20-bit private network block for DHCP based networks options[:ip] = "172.28.128.1" if options[:type] == :dhcp && !options[:ip] - # Calculate our network address for the given IP/netmask - netaddr = network_address(options[:ip], options[:netmask]) + ip = IPAddr.new(options[:ip]) + if !ip.ipv6? + options[:netmask] ||= "255.255.255.0" - # Verify that a host-only network subnet would not collide - # with a bridged networking interface. - # - # If the subnets overlap in any way then the host only network - # will not work because the routing tables will force the - # traffic onto the real interface rather than the VirtualBox - # interface. - @env[:machine].provider.driver.read_bridged_interfaces.each do |interface| - that_netaddr = network_address(interface[:ip], interface[:netmask]) - raise Vagrant::Errors::NetworkCollision if \ - netaddr == that_netaddr && interface[:status] != "Down" + # Calculate our network address for the given IP/netmask + netaddr = network_address(options[:ip], options[:netmask]) + + # Verify that a host-only network subnet would not collide + # with a bridged networking interface. + # + # If the subnets overlap in any way then the host only network + # will not work because the routing tables will force the + # traffic onto the real interface rather than the VirtualBox + # interface. + @env[:machine].provider.driver.read_bridged_interfaces.each do |interface| + that_netaddr = network_address(interface[:ip], interface[:netmask]) + raise Vagrant::Errors::NetworkCollision if \ + netaddr == that_netaddr && interface[:status] != "Down" + end + + # Split the IP address into its components + ip_parts = netaddr.split(".").map { |i| i.to_i } + + # Calculate the adapter IP, which we assume is the IP ".1" at + # the end usually. + adapter_ip = ip_parts.dup + adapter_ip[3] += 1 + options[:adapter_ip] ||= adapter_ip.join(".") + else + # Default subnet prefix length + options[:netmask] ||= 64 + + # IPv6 we just mask the address and use that as the adapter + options[:adapter_ip] ||= ip.mask(options[:netmask].to_i).to_s + + # Append a 6 to the end of the type + options[:type] = "#{options[:type]}6".to_sym end - # Split the IP address into its components - ip_parts = netaddr.split(".").map { |i| i.to_i } - - # Calculate the adapter IP, which we assume is the IP ".1" at - # the end usually. - adapter_ip = ip_parts.dup - adapter_ip[3] += 1 - options[:adapter_ip] ||= adapter_ip.join(".") - dhcp_options = {} if options[:type] == :dhcp # Calculate the DHCP server IP, which is the network address @@ -456,8 +470,16 @@ module VagrantPlugins @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| return interface if config[:name] && config[:name] == interface[:name] - return interface if this_netaddr == \ - network_address(interface[:ip], interface[:netmask]) + + if interface[:ip] != "" + return interface if this_netaddr == \ + network_address(interface[:ip], interface[:netmask]) + end + + if interface[:ipv6] != "" + return interface if this_netaddr == \ + network_address(interface[:ipv6], interface[:ipv6_prefix]) + end end nil diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index b421e7911..ed4b7aa9e 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -1,3 +1,4 @@ +require 'ipaddr' require 'log4r' require "vagrant/util/platform" @@ -46,12 +47,21 @@ module VagrantPlugins def create_host_only_network(options) # Create the interface execute("hostonlyif", "create") =~ /^Interface '(.+?)' was successfully created$/ - name = $1.to_s + name = $1.to_s - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask]) + end # Return the details return { @@ -366,6 +376,10 @@ module VagrantPlugins info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s.strip + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s.strip elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index 102d838bc..c8d659e11 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -366,6 +366,10 @@ module VagrantPlugins info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end diff --git a/templates/guests/debian/network_static6.erb b/templates/guests/debian/network_static6.erb new file mode 100644 index 000000000..7b9e8a694 --- /dev/null +++ b/templates/guests/debian/network_static6.erb @@ -0,0 +1,10 @@ +#VAGRANT-BEGIN +# The contents below are automatically generated by Vagrant. Do not modify. +auto eth<%= options[:interface] %> +iface eth<%= options[:interface] %> inet6 static + address <%= options[:ip] %> + netmask <%= options[:netmask] %> +<% if options[:gateway] %> + gateway <%= options[:gateway] %> +<% end %> +#VAGRANT-END diff --git a/test/unit/vagrant/util/network_ip_test.rb b/test/unit/vagrant/util/network_ip_test.rb index a4ff8944c..9d0d09b12 100644 --- a/test/unit/vagrant/util/network_ip_test.rb +++ b/test/unit/vagrant/util/network_ip_test.rb @@ -13,5 +13,17 @@ describe Vagrant::Util::NetworkIP do it "calculates it properly" do expect(klass.network_address("192.168.2.234", "255.255.255.0")).to eq("192.168.2.0") end + + it "calculates it properly with integer submask" do + expect(klass.network_address("192.168.2.234", "24")).to eq("192.168.2.0") + end + + it "calculates it properly for IPv6" do + expect(klass.network_address("fde4:8dba:82e1::c4", "64")).to eq("fde4:8dba:82e1::") + end + + it "calculates it properly for IPv6" do + expect(klass.network_address("fde4:8dba:82e1::c4", 64)).to eq("fde4:8dba:82e1::") + end end end From 0d50f454ea17ee05aeabe7dd33ff3bd85d75c000 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Sep 2015 17:29:23 -0700 Subject: [PATCH 068/484] providers/virtualbox: VB5 support --- .../virtualbox/driver/version_4_3.rb | 2 +- .../virtualbox/driver/version_5_0.rb | 23 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index ed4b7aa9e..b3d29c6c8 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -60,7 +60,7 @@ module VagrantPlugins elsif ip.ipv6? execute("hostonlyif", "ipconfig", name, "--ipv6", options[:adapter_ip], - "--netmasklengthv6", options[:netmask]) + "--netmasklengthv6", options[:netmask].to_s) end # Return the details diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index c8d659e11..a9aad8442 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -46,12 +46,21 @@ module VagrantPlugins def create_host_only_network(options) # Create the interface execute("hostonlyif", "create") =~ /^Interface '(.+?)' was successfully created$/ - name = $1.to_s + name = $1.to_s - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask].to_s) + end # Return the details return { @@ -367,9 +376,9 @@ module VagrantPlugins elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s elsif line =~ /^IPV6Address:\s+(.+?)$/ - info[:ipv6] = $1.to_s + info[:ipv6] = $1.to_s.strip elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ - info[:ipv6_prefix] = $1.to_s + info[:ipv6_prefix] = $1.to_s.strip elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end From 061a91d09b5c7ad3401c4c48a48d071512cfd0e7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 30 Sep 2015 18:19:37 -0700 Subject: [PATCH 069/484] providers/virtualbox: workaround IPv6 routing bug in VB VirtualBox has a bug where the IPv6 route is lost on every other configuration of a host-only network. This is also triggered when a VM is booted. To fix this, we test the route-ability of all IPv6 networks, and reconfigure if necessary. This is very fast but we still only do this if we have any IPv6 networks. --- plugins/providers/virtualbox/action.rb | 2 + .../virtualbox/action/network_fix_ipv6.rb | 64 +++++++++++++++++++ plugins/providers/virtualbox/driver/base.rb | 8 +++ plugins/providers/virtualbox/driver/meta.rb | 1 + .../virtualbox/driver/version_4_3.rb | 5 ++ .../virtualbox/driver/version_5_0.rb | 5 ++ 6 files changed, 85 insertions(+) create mode 100644 plugins/providers/virtualbox/action/network_fix_ipv6.rb diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 83eb3aec9..0d8fc8e38 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -30,6 +30,7 @@ module VagrantPlugins autoload :MessageNotRunning, File.expand_path("../action/message_not_running", __FILE__) autoload :MessageWillNotDestroy, File.expand_path("../action/message_will_not_destroy", __FILE__) autoload :Network, File.expand_path("../action/network", __FILE__) + autoload :NetworkFixIPv6, File.expand_path("../action/network_fix_ipv6", __FILE__) autoload :Package, File.expand_path("../action/package", __FILE__) autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__) autoload :PrepareNFSSettings, File.expand_path("../action/prepare_nfs_settings", __FILE__) @@ -63,6 +64,7 @@ module VagrantPlugins b.use PrepareNFSSettings b.use ClearNetworkInterfaces b.use Network + b.use NetworkFixIPv6 b.use ForwardPorts b.use SetHostname b.use SaneDefaults diff --git a/plugins/providers/virtualbox/action/network_fix_ipv6.rb b/plugins/providers/virtualbox/action/network_fix_ipv6.rb new file mode 100644 index 000000000..eb4ca48ef --- /dev/null +++ b/plugins/providers/virtualbox/action/network_fix_ipv6.rb @@ -0,0 +1,64 @@ +require "ipaddr" +require "socket" + +require "log4r" + +require "vagrant/util/scoped_hash_override" + +module VagrantPlugins + module ProviderVirtualBox + module Action + # This middleware works around a bug in VirtualBox where booting + # a VM with an IPv6 host-only network will someties lose the + # route to that machine. + class NetworkFixIPv6 + include Vagrant::Util::ScopedHashOverride + + def initialize(app, env) + @logger = Log4r::Logger.new("vagrant::plugins::virtualbox::network") + @app = app + end + + def call(env) + @env = env + + # Determine if we have an IPv6 network + has_v6 = false + env[:machine].config.vm.networks.each do |type, options| + next if type != :private_network + options = scoped_hash_override(options, :virtualbox) + next if options[:ip] == "" + if IPAddr.new(options[:ip]).ipv6? + has_v6 = true + break + end + end + + # Call up + @app.call(env) + + # If we have no IPv6, forget it + return if !has_v6 + + # We do, so fix them if we must + env[:machine].provider.driver.read_host_only_interfaces.each do |interface| + # Ignore interfaces without an IPv6 address + next if interface[:ipv6] == "" + + # Make the test IP. This is just the highest value IP + ip = IPAddr.new(interface[:ipv6]) + ip |= IPAddr.new(":#{":FFFF" * (interface[:ipv6_prefix].to_i / 16)}") + + @logger.info("testing IPv6: #{ip}") + begin + UDPSocket.new(Socket::AF_INET6).connect(ip.to_s, 80) + rescue Errno::EHOSTUNREACH + @logger.info("IPv6 host unreachable. Fixing: #{ip}") + env[:machine].provider.driver.reconfig_host_only(interface) + end + end + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb index 112e97d5b..768bd9cef 100644 --- a/plugins/providers/virtualbox/driver/base.rb +++ b/plugins/providers/virtualbox/driver/base.rb @@ -263,6 +263,14 @@ module VagrantPlugins def read_vms end + # Reconfigure the hostonly network given by interface (the result + # of read_host_only_networks). This is a sad function that only + # exists to work around VirtualBox bugs. + # + # @return nil + def reconfig_host_only(interface) + end + # Removes the DHCP server identified by the provided network name. # # @param [String] network_name The the full network name associated diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index 10bef3f8f..242372452 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -109,6 +109,7 @@ module VagrantPlugins :read_state, :read_used_ports, :read_vms, + :reconfig_host_only, :remove_dhcp_server, :resume, :set_mac_address, diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index b3d29c6c8..3b08d43cd 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -72,6 +72,11 @@ module VagrantPlugins } end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def delete execute("unregistervm", @uuid, "--delete") end diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index a9aad8442..c58bae3c0 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -488,6 +488,11 @@ module VagrantPlugins results end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name) end From 7e18a92bd94ce2fe20b97b27cf6504e6a795b7cd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 1 Oct 2015 08:45:44 -0400 Subject: [PATCH 070/484] test: fix tests --- .../support/shared/virtualbox_driver_version_4_x_examples.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb index 88f199a82..a56a45311 100644 --- a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb +++ b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb @@ -159,6 +159,7 @@ shared_examples "a version 4.x virtualbox driver" do |options| name: 'vboxnet0', ip: '172.28.128.1', netmask: '255.255.255.0', + ipv6_prefix: '0', status: 'Up', }]) end @@ -196,8 +197,8 @@ shared_examples "a version 4.x virtualbox driver" do |options| it "returns a list with one entry for each interface" do expect(subject.read_host_only_interfaces).to eq([ - {name: 'vboxnet0', ip: '172.28.128.1', netmask: '255.255.255.0', status: 'Up'}, - {name: 'vboxnet1', ip: '10.0.0.1', netmask: '255.255.255.0', status: 'Up'}, + {name: 'vboxnet0', ip: '172.28.128.1', netmask: '255.255.255.0', ipv6_prefix: "0", status: 'Up'}, + {name: 'vboxnet1', ip: '10.0.0.1', netmask: '255.255.255.0', ipv6_prefix: "0", status: 'Up'}, ]) end end From 199a58fdd902cdb7e9f7d9707d5b1f36358682aa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 1 Oct 2015 08:47:25 -0400 Subject: [PATCH 071/484] test: test IPv6 --- .../virtualbox_driver_version_4_x_examples.rb | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb index a56a45311..85dee3cb1 100644 --- a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb +++ b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb @@ -202,6 +202,35 @@ shared_examples "a version 4.x virtualbox driver" do |options| ]) end end + + context "with an IPv6 host-only interface" do + let(:output) { + <<-OUTPUT.gsub(/^ */, '') + Name: vboxnet1 + GUID: 786f6276-656e-4174-8000-0a0027000001 + DHCP: Disabled + IPAddress: 192.168.57.1 + NetworkMask: 255.255.255.0 + IPV6Address: fde4:8dba:82e1:: + IPV6NetworkMaskPrefixLength: 64 + HardwareAddress: 0a:00:27:00:00:01 + MediumType: Ethernet + Status: Up + VBoxNetworkName: HostInterfaceNetworking-vboxnet1 + OUTPUT + } + + it "returns a list with one entry describing that interface" do + expect(subject.read_host_only_interfaces).to eq([{ + name: 'vboxnet1', + ip: '192.168.57.1', + netmask: '255.255.255.0', + ipv6: 'fde4:8dba:82e1::', + ipv6_prefix: '64', + status: 'Up', + }]) + end + end end describe "remove_dhcp_server" do From 05fbb4ced2e717e0255da2bf04f30a408aac5cf8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 1 Oct 2015 08:50:47 -0400 Subject: [PATCH 072/484] providers/virtualbox: more support --- .../virtualbox/driver/version_4_0.rb | 26 ++++++++++++++--- .../virtualbox/driver/version_4_1.rb | 26 ++++++++++++++--- .../virtualbox/driver/version_4_2.rb | 28 +++++++++++++++---- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/plugins/providers/virtualbox/driver/version_4_0.rb b/plugins/providers/virtualbox/driver/version_4_0.rb index 53361bbbb..66e9a37be 100644 --- a/plugins/providers/virtualbox/driver/version_4_0.rb +++ b/plugins/providers/virtualbox/driver/version_4_0.rb @@ -48,10 +48,19 @@ module VagrantPlugins interface = execute("hostonlyif", "create") name = interface[/^Interface '(.+?)' was successfully created$/, 1] - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask].to_s) + end # Return the details return { @@ -320,6 +329,10 @@ module VagrantPlugins info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s.strip + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s.strip elsif status = line[/^Status:\s+(.+?)$/, 1] info[:status] = status end @@ -429,6 +442,11 @@ module VagrantPlugins results end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name) end diff --git a/plugins/providers/virtualbox/driver/version_4_1.rb b/plugins/providers/virtualbox/driver/version_4_1.rb index ae1830019..8830297ba 100644 --- a/plugins/providers/virtualbox/driver/version_4_1.rb +++ b/plugins/providers/virtualbox/driver/version_4_1.rb @@ -48,10 +48,19 @@ module VagrantPlugins interface = execute("hostonlyif", "create") name = interface[/^Interface '(.+?)' was successfully created$/, 1] - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask].to_s) + end # Return the details return { @@ -325,6 +334,10 @@ module VagrantPlugins info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s.strip + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s.strip elsif status = line[/^Status:\s+(.+?)$/, 1] info[:status] = status end @@ -434,6 +447,11 @@ module VagrantPlugins results end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name) end diff --git a/plugins/providers/virtualbox/driver/version_4_2.rb b/plugins/providers/virtualbox/driver/version_4_2.rb index dc7da430a..0f1f79698 100644 --- a/plugins/providers/virtualbox/driver/version_4_2.rb +++ b/plugins/providers/virtualbox/driver/version_4_2.rb @@ -46,12 +46,21 @@ module VagrantPlugins def create_host_only_network(options) # Create the interface execute("hostonlyif", "create") =~ /^Interface '(.+?)' was successfully created$/ - name = $1.to_s + name = $1.to_s - # Configure it - execute("hostonlyif", "ipconfig", name, - "--ip", options[:adapter_ip], - "--netmask", options[:netmask]) + # Get the IP so we can determine v4 vs v6 + ip = IPAddr.new(options[:adapter_ip]) + + # Configure + if ip.ipv4? + execute("hostonlyif", "ipconfig", name, + "--ip", options[:adapter_ip], + "--netmask", options[:netmask]) + elsif ip.ipv6? + execute("hostonlyif", "ipconfig", name, + "--ipv6", options[:adapter_ip], + "--netmasklengthv6", options[:netmask].to_s) + end # Return the details return { @@ -356,6 +365,10 @@ module VagrantPlugins info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s + elsif line =~ /^IPV6Address:\s+(.+?)$/ + info[:ipv6] = $1.to_s.strip + elsif line =~ /^IPV6NetworkMaskPrefixLength:\s+(.+?)$/ + info[:ipv6_prefix] = $1.to_s.strip elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end @@ -465,6 +478,11 @@ module VagrantPlugins results end + def reconfig_host_only(interface) + execute("hostonlyif", "ipconfig", interface[:name], + "--ipv6", interface[:ipv6]) + end + def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name) end From 2b732a96c7b2d675cdc33f80c6e6cc8b82786e71 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 1 Oct 2015 08:55:03 -0400 Subject: [PATCH 073/484] website/docs: IPv6 --- .../v2/networking/private_network.html.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/website/docs/source/v2/networking/private_network.html.md b/website/docs/source/v2/networking/private_network.html.md index 345547f05..e11c7b3c7 100644 --- a/website/docs/source/v2/networking/private_network.html.md +++ b/website/docs/source/v2/networking/private_network.html.md @@ -74,6 +74,35 @@ reachable.

    +## IPv6 + +You can specify a static IP via IPv6. DHCP for IPv6 is not supported. +To use IPv6, just specify an IPv6 address as the IP: + +```ruby +Vagrant.configure("2") do |config| + config.vm.network "private_network", ip: "fde4:8dba:82e1::c4" +end +``` + +This will assign that IP to the machine. The entire `/64` subnet will +be reserved. Please make sure to use the reserved local addresses approved +for IPv6. + +You can also modify the prefix length by changing the `netmask` option +(defaults to 64): + +```ruby +Vagrant.configure("2") do |config| + config.vm.network "private_network", + ip: "fde4:8dba:82e1::c4", + netmask: "96" +end +``` + +IPv6 supports for private networks was added in Vagrant 1.7.5 and may +not work with every provider. + ## Disable Auto-Configuration If you want to manually configure the network interface yourself, you From 2299715b4135d199e2335cca43db69f33f1e2532 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 1 Oct 2015 17:01:27 -0400 Subject: [PATCH 074/484] providers/virtualbox: code review comments --- plugins/providers/virtualbox/action/network.rb | 6 ++++-- plugins/providers/virtualbox/driver/version_5_0.rb | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/providers/virtualbox/action/network.rb b/plugins/providers/virtualbox/action/network.rb index 881fcdf51..cf5ca3b47 100644 --- a/plugins/providers/virtualbox/action/network.rb +++ b/plugins/providers/virtualbox/action/network.rb @@ -259,7 +259,7 @@ module VagrantPlugins options[:ip] = "172.28.128.1" if options[:type] == :dhcp && !options[:ip] ip = IPAddr.new(options[:ip]) - if !ip.ipv6? + if ip.ipv4? options[:netmask] ||= "255.255.255.0" # Calculate our network address for the given IP/netmask @@ -286,7 +286,7 @@ module VagrantPlugins adapter_ip = ip_parts.dup adapter_ip[3] += 1 options[:adapter_ip] ||= adapter_ip.join(".") - else + elsif ip.ipv6? # Default subnet prefix length options[:netmask] ||= 64 @@ -295,6 +295,8 @@ module VagrantPlugins # Append a 6 to the end of the type options[:type] = "#{options[:type]}6".to_sym + else + raise "BUG: Unknown IP type: #{ip.inspect}" end dhcp_options = {} diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index c58bae3c0..3c84ad3c7 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -60,6 +60,8 @@ module VagrantPlugins execute("hostonlyif", "ipconfig", name, "--ipv6", options[:adapter_ip], "--netmasklengthv6", options[:netmask].to_s) + else + raise "BUG: Unknown IP type: #{ip.inspect}" end # Return the details From a152045f81e3433c76640cae449a359de2c311bb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 1 Oct 2015 17:06:56 -0400 Subject: [PATCH 075/484] update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3567eb1c..7166a1b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ FEATURES: + - **IPv6 Private Networks**: Private networking now supports IPv6. This + only works with VirtualBox and VMware at this point. [GH-6342] + IMPROVEMENTS: BUG FIXES: From 0556b3b040a75c3d698a5e0917722e7d83b5b001 Mon Sep 17 00:00:00 2001 From: ctammes Date: Tue, 6 Oct 2015 14:02:45 +0200 Subject: [PATCH 076/484] Update push.rb https://github.com/mitchellh/vagrant/issues/5570 When uploading from Windows to Linux, the Windows filepath was added to the Linux path. --- plugins/pushes/ftp/push.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/pushes/ftp/push.rb b/plugins/pushes/ftp/push.rb index 7a69c4f02..755304ee6 100644 --- a/plugins/pushes/ftp/push.rb +++ b/plugins/pushes/ftp/push.rb @@ -19,7 +19,7 @@ module VagrantPlugins # wait and close the (S)FTP connection as well files = Hash[*all_files.flat_map do |file| relative_path = relative_path_for(file, config.dir) - destination = File.expand_path(File.join(config.destination, relative_path)) + destination = File.join(config.destination, relative_path) file = File.expand_path(file, env.root_path) [file, destination] end] From 2c936b2e3722a84ca703e335e8cb2f19c09d670f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 Oct 2015 14:11:41 -0400 Subject: [PATCH 077/484] providers/virtualbox: tidying up the linked clone feature --- lib/vagrant/environment.rb | 4 +- plugins/providers/virtualbox/action.rb | 7 +- .../virtualbox/action/create_clone.rb | 9 +- plugins/providers/virtualbox/action/import.rb | 5 +- .../virtualbox/action/import_master.rb | 88 +++++++++++-------- plugins/providers/virtualbox/config.rb | 8 +- templates/locales/en.yml | 9 +- 7 files changed, 75 insertions(+), 55 deletions(-) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index d4b8db1fd..c3dabd676 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -529,7 +529,9 @@ module Vagrant begin File.delete(lock_path) rescue - @logger.debug("Failed to delete lock file #{lock_path} - some other thread might be trying to acquire it -> ignoring this error") + @logger.error( + "Failed to delete lock file #{lock_path} - some other thread " + + "might be trying to acquire it. ignoring this error") end end end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 635907827..088221ed1 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -327,13 +327,14 @@ module VagrantPlugins if !env[:result] b2.use CheckAccessible b2.use Customize, "pre-import" - - if env[:machine].provider_config.use_linked_clone + + if env[:machine].provider_config.linked_clone b2.use ImportMaster b2.use CreateClone else b2.use Import - end + end + b2.use MatchMACAddress end end diff --git a/plugins/providers/virtualbox/action/create_clone.rb b/plugins/providers/virtualbox/action/create_clone.rb index e26b5721f..9cc8f7ed5 100644 --- a/plugins/providers/virtualbox/action/create_clone.rb +++ b/plugins/providers/virtualbox/action/create_clone.rb @@ -1,5 +1,4 @@ require "log4r" -#require "lockfile" module VagrantPlugins module ProviderVirtualBox @@ -12,14 +11,16 @@ module VagrantPlugins def call(env) @logger.info("Creating linked clone from master '#{env[:master_id]}'") - + env[:ui].info I18n.t("vagrant.actions.vm.clone.creating", name: env[:machine].box.name) - env[:machine].id = env[:machine].provider.driver.clonevm(env[:master_id], env[:machine].box.name, "base") do |progress| + env[:machine].id = env[:machine].provider.driver.clonevm( + env[:master_id], env[:machine].box.name, "base") do |progress| env[:ui].clear_line env[:ui].report_progress(progress, 100, false) end - # Clear the line one last time since the progress meter doesn't disappear immediately. + # Clear the line one last time since the progress meter doesn't + # disappear immediately. env[:ui].clear_line # Flag as erroneous and return if clone failed diff --git a/plugins/providers/virtualbox/action/import.rb b/plugins/providers/virtualbox/action/import.rb index 6648faefc..238f88e00 100644 --- a/plugins/providers/virtualbox/action/import.rb +++ b/plugins/providers/virtualbox/action/import.rb @@ -12,11 +12,14 @@ module VagrantPlugins # Import the virtual machine ovf_file = env[:machine].box.directory.join("box.ovf").to_s - env[:machine].id = env[:machine].provider.driver.import(ovf_file) do |progress| + id = env[:machine].provider.driver.import(ovf_file) do |progress| env[:ui].clear_line env[:ui].report_progress(progress, 100, false) end + # Set the machine ID + env[:machine].id = id if !env[:skip_machine] + # Clear the line one last time since the progress meter doesn't disappear # immediately. env[:ui].clear_line diff --git a/plugins/providers/virtualbox/action/import_master.rb b/plugins/providers/virtualbox/action/import_master.rb index 61b0064eb..9b088b2a1 100644 --- a/plugins/providers/virtualbox/action/import_master.rb +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -1,5 +1,6 @@ require "log4r" -#require "lockfile" + +require "digest/md5" module VagrantPlugins module ProviderVirtualBox @@ -11,45 +12,14 @@ module VagrantPlugins end def call(env) - master_id_file = env[:machine].box.directory.join("master_id") - - env[:machine].env.lock(Digest::MD5.hexdigest(env[:machine].box.name), retry: true) do - env[:master_id] = master_id_file.read.chomp if master_id_file.file? - if env[:master_id] && env[:machine].provider.driver.vm_exists?(env[:master_id]) - # Master VM already exists -> nothing to do - continue. - @logger.info("Master VM for '#{env[:machine].box.name}' already exists (id=#{env[:master_id]}) - skipping import step.") - return @app.call(env) - end - - env[:ui].info I18n.t("vagrant.actions.vm.clone.importing", name: env[:machine].box.name) - - # Import the virtual machine - ovf_file = env[:machine].box.directory.join("box.ovf").to_s - env[:master_id] = env[:machine].provider.driver.import(ovf_file) do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - - # Clear the line one last time since the progress meter doesn't disappear immediately. - env[:ui].clear_line - - # Flag as erroneous and return if import failed - raise Vagrant::Errors::VMImportFailure if !env[:master_id] + # Do the import while locked so that nobody else imports + # a master at the same time. This is a no-op if we already + # have a master that exists. + lock_key = Digest::MD5.hexdigest(env[:machine].box.name) + env[:machine].env.lock(lock_key, retry: true) do + import_master(env) + end - @logger.info("Imported box #{env[:machine].box.name} as master vm with id #{env[:master_id]}") - - @logger.info("Creating base snapshot for master VM.") - env[:machine].provider.driver.create_snapshot(env[:master_id], "base") do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - - @logger.debug("Writing id of master VM '#{env[:master_id]}' to #{master_id_file}") - master_id_file.open("w+") do |f| - f.write(env[:master_id]) - end - end - # If we got interrupted, then the import could have been # interrupted and its not a big deal. Just return out. if env[:interrupted] @@ -60,6 +30,46 @@ module VagrantPlugins # Import completed successfully. Continue the chain @app.call(env) end + + protected + + def import_master(env) + master_id_file = env[:machine].box.directory.join("master_id") + + # Read the master ID if we have it in the file. + env[:master_id] = master_id_file.read.chomp if master_id_file.file? + + # If we have the ID and the VM exists already, then we + # have nothing to do. Success! + if env[:master_id] && env[:machine].provider.driver.vm_exists?(env[:master_id]) + @logger.info( + "Master VM for '#{env[:machine].box.name}' already exists " + + " (id=#{env[:master_id]}) - skipping import step.") + return + end + + env[:ui].info(I18n.t("vagrant.actions.vm.clone.setup_master")) + env[:ui].detail("\n"+I18n.t("vagrant.actions.vm.clone.setup_master_detail")) + + # Import the virtual machine + import_env = env[:action_runner].run(Import, skip_machine: true) + env[:master_id] = import_env[:machine_id] + + @logger.info( + "Imported box #{env[:machine].box.name} as master vm " + + "with id #{env[:master_id]}") + + @logger.info("Creating base snapshot for master VM.") + env[:machine].provider.driver.create_snapshot(env[:master_id], "base") do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + @logger.debug("Writing id of master VM '#{env[:master_id]}' to #{master_id_file}") + master_id_file.open("w+") do |f| + f.write(env[:master_id]) + end + end end end end diff --git a/plugins/providers/virtualbox/config.rb b/plugins/providers/virtualbox/config.rb index 55b8356aa..12b882b01 100644 --- a/plugins/providers/virtualbox/config.rb +++ b/plugins/providers/virtualbox/config.rb @@ -36,8 +36,8 @@ module VagrantPlugins # VM generated from the specified box. # # @return [Boolean] - attr_accessor :use_linked_clone - + attr_accessor :linked_clone + # This should be set to the name of the machine in the VirtualBox # GUI. # @@ -65,7 +65,7 @@ module VagrantPlugins @name = UNSET_VALUE @network_adapters = {} @gui = UNSET_VALUE - @use_linked_clone = UNSET_VALUE + @linked_clone = UNSET_VALUE # We require that network adapter 1 is a NAT device. network_adapter(1, :nat) @@ -144,7 +144,7 @@ module VagrantPlugins @gui = false if @gui == UNSET_VALUE # Do not create linked clone by default - @use_linked_clone = false if @use_linked_clone == UNSET_VALUE + @linked_clone = false if @linked_clone == UNSET_VALUE # The default name is just nothing, and we default it @name = nil if @name == UNSET_VALUE diff --git a/templates/locales/en.yml b/templates/locales/en.yml index d0f311fd0..171cd60ba 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1619,10 +1619,13 @@ en: clear_shared_folders: deleting: Cleaning previously set shared folders... clone: - importing: Importing box '%{name}' as master vm... - creating: Creating linked clone... + setup_master: Preparing master VM for linked clones... + setup_master_detail: |- + This is a one time operation. Once the master VM is prepared, + it will be used as a base for linked clones, making the creation + of new VMs take milliseconds on a modern system. + creating: Creating linked clone... failure: Creation of the linked clone failed. - create_master: failure: |- Failed to create lock-file for master VM creation for box %{box}. From 3b3de6e2e510227e47a0b1bf46cba10c553a2152 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 Oct 2015 14:24:43 -0400 Subject: [PATCH 078/484] support Bundler 1.10.6 --- vagrant.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vagrant.gemspec b/vagrant.gemspec index 3e0aea4e3..b0f0638e3 100644 --- a/vagrant.gemspec +++ b/vagrant.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = ">= 1.3.6" s.rubyforge_project = "vagrant" - s.add_dependency "bundler", ">= 1.5.2", "<= 1.10.5" + s.add_dependency "bundler", ">= 1.5.2", "<= 1.10.6" s.add_dependency "childprocess", "~> 0.5.0" s.add_dependency "erubis", "~> 2.7.0" s.add_dependency "i18n", ">= 0.6.0", "<= 0.8.0" From 0586412f9dc0bcfa2b4a6b996a386be29e6a30d4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 Oct 2015 16:22:48 -0400 Subject: [PATCH 079/484] providers/virtualbox: fix some crashing bugs --- plugins/providers/virtualbox/action/import.rb | 5 +++-- plugins/providers/virtualbox/action/import_master.rb | 4 ++-- plugins/providers/virtualbox/driver/version_5_0.rb | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/providers/virtualbox/action/import.rb b/plugins/providers/virtualbox/action/import.rb index 238f88e00..92e004f01 100644 --- a/plugins/providers/virtualbox/action/import.rb +++ b/plugins/providers/virtualbox/action/import.rb @@ -18,6 +18,7 @@ module VagrantPlugins end # Set the machine ID + env[:machine_id] = id env[:machine].id = id if !env[:skip_machine] # Clear the line one last time since the progress meter doesn't disappear @@ -29,14 +30,14 @@ module VagrantPlugins return if env[:interrupted] # Flag as erroneous and return if import failed - raise Vagrant::Errors::VMImportFailure if !env[:machine].id + raise Vagrant::Errors::VMImportFailure if !id # Import completed successfully. Continue the chain @app.call(env) end def recover(env) - if env[:machine].state.id != :not_created + if env[:machine] && env[:machine].state.id != :not_created return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError) # If we're not supposed to destroy on error then just return diff --git a/plugins/providers/virtualbox/action/import_master.rb b/plugins/providers/virtualbox/action/import_master.rb index 9b088b2a1..5b9ae7790 100644 --- a/plugins/providers/virtualbox/action/import_master.rb +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -49,10 +49,10 @@ module VagrantPlugins end env[:ui].info(I18n.t("vagrant.actions.vm.clone.setup_master")) - env[:ui].detail("\n"+I18n.t("vagrant.actions.vm.clone.setup_master_detail")) + env[:ui].detail(I18n.t("vagrant.actions.vm.clone.setup_master_detail")) # Import the virtual machine - import_env = env[:action_runner].run(Import, skip_machine: true) + import_env = env[:action_runner].run(Import, env.dup.merge(skip_machine: true)) env[:master_id] = import_env[:machine_id] @logger.info( diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index 4bb839c1c..df833bb3f 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -36,13 +36,13 @@ module VagrantPlugins def clonevm(master_id, box_name, snapshot_name) @logger.debug("Creating linked clone from master vm with id #{master_id} from snapshot '#{snapshot_name}'") - + machine_name = "#{box_name}_#{snapshot_name}_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" execute("clonevm", master_id, "--snapshot", snapshot_name, "--options", "link", "--register", "--name", machine_name) - + return get_machine_id machine_name end - + def create_dhcp_server(network, options) execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], @@ -85,7 +85,7 @@ module VagrantPlugins def create_snapshot(machine_id, snapshot_name) execute("snapshot", machine_id, "take", snapshot_name) end - + def delete execute("unregistervm", @uuid, "--delete") end @@ -186,7 +186,7 @@ module VagrantPlugins return match[1].to_s if match nil end - + def halt execute("controlvm", @uuid, "poweroff") end From 169cacd710805b23e880302dd41156765b00c61d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 Oct 2015 16:24:02 -0400 Subject: [PATCH 080/484] update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7166a1b3f..8840cc522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ FEATURES: - **IPv6 Private Networks**: Private networking now supports IPv6. This only works with VirtualBox and VMware at this point. [GH-6342] + - **Linked Clones**: VirtualBox and VMware providers now support + linked clones for very fast (millisecond) imports on up. [GH-4484] IMPROVEMENTS: From 07e38f1bb3e890fa373fda3e106f08e69c7194a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Bonveh=C3=AD?= Date: Wed, 7 Oct 2015 01:17:07 -0300 Subject: [PATCH 081/484] Fix Slackware Host detection and nfsd checks Slackware's version file is /etc/slackware-version not /etc/slackware-release. pidof is not on PATH by default (not running as root) so call it using full path --- plugins/hosts/slackware/cap/nfs.rb | 2 +- plugins/hosts/slackware/host.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/hosts/slackware/cap/nfs.rb b/plugins/hosts/slackware/cap/nfs.rb index e7f21a094..84413236d 100644 --- a/plugins/hosts/slackware/cap/nfs.rb +++ b/plugins/hosts/slackware/cap/nfs.rb @@ -3,7 +3,7 @@ module VagrantPlugins module Cap class NFS def self.nfs_check_command(env) - "pidof nfsd >/dev/null" + "/sbin/pidof nfsd >/dev/null" end def self.nfs_start_command(env) diff --git a/plugins/hosts/slackware/host.rb b/plugins/hosts/slackware/host.rb index 2afaff8f3..ec3503ac0 100644 --- a/plugins/hosts/slackware/host.rb +++ b/plugins/hosts/slackware/host.rb @@ -4,7 +4,7 @@ module VagrantPlugins module HostSlackware class Host < Vagrant.plugin("2", :host) def detect?(env) - return File.exists?("/etc/slackware-release") || + return File.exists?("/etc/slackware-version") || !Dir.glob("/usr/lib/setup/Plamo-*").empty? end end From a99ebcb3ceeead60397d5632b19b61917eb6cc22 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Oct 2015 15:52:37 -0400 Subject: [PATCH 082/484] commands/snapshot --- plugins/commands/snapshot/command/delete.rb | 35 +++++++++ plugins/commands/snapshot/command/list.rb | 30 ++++++++ plugins/commands/snapshot/command/restore.rb | 35 +++++++++ plugins/commands/snapshot/command/root.rb | 79 ++++++++++++++++++++ plugins/commands/snapshot/command/save.rb | 40 ++++++++++ plugins/commands/snapshot/plugin.rb | 15 ++++ 6 files changed, 234 insertions(+) create mode 100644 plugins/commands/snapshot/command/delete.rb create mode 100644 plugins/commands/snapshot/command/list.rb create mode 100644 plugins/commands/snapshot/command/restore.rb create mode 100644 plugins/commands/snapshot/command/root.rb create mode 100644 plugins/commands/snapshot/command/save.rb create mode 100644 plugins/commands/snapshot/plugin.rb diff --git a/plugins/commands/snapshot/command/delete.rb b/plugins/commands/snapshot/command/delete.rb new file mode 100644 index 000000000..0d5118847 --- /dev/null +++ b/plugins/commands/snapshot/command/delete.rb @@ -0,0 +1,35 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class Delete < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot delete [options] [vm-name] " + o.separator "" + o.separator "Delete a snapshot taken previously with snapshot save." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + name = argv.pop + with_target_vms(argv) do |vm| + vm.action(:snapshot_delete, snapshot_name: name) + end + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/list.rb b/plugins/commands/snapshot/command/list.rb new file mode 100644 index 000000000..af91e76f5 --- /dev/null +++ b/plugins/commands/snapshot/command/list.rb @@ -0,0 +1,30 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class List < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot list [options] [vm-name]" + o.separator "" + o.separator "List all snapshots taken for a machine." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + with_target_vms(argv) do |vm| + vm.action(:snapshot_list) + end + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/restore.rb b/plugins/commands/snapshot/command/restore.rb new file mode 100644 index 000000000..930a811f4 --- /dev/null +++ b/plugins/commands/snapshot/command/restore.rb @@ -0,0 +1,35 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class Restore < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot restore [options] [vm-name] " + o.separator "" + o.separator "Restore a snapshot taken previously with snapshot save." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + name = argv.pop + with_target_vms(argv) do |vm| + vm.action(:snapshot_restore, snapshot_name: name) + end + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/root.rb b/plugins/commands/snapshot/command/root.rb new file mode 100644 index 000000000..03bcebb80 --- /dev/null +++ b/plugins/commands/snapshot/command/root.rb @@ -0,0 +1,79 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class Root < Vagrant.plugin("2", :command) + def self.synopsis + "manages snapshots: saving, restoring, etc." + end + + def initialize(argv, env) + super + + @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) + + @subcommands = Vagrant::Registry.new + @subcommands.register(:save) do + require File.expand_path("../save", __FILE__) + Save + end + + @subcommands.register(:restore) do + require File.expand_path("../restore", __FILE__) + Restore + end + + @subcommands.register(:delete) do + require File.expand_path("../delete", __FILE__) + Delete + end + + @subcommands.register(:list) do + require File.expand_path("../list", __FILE__) + List + end + end + + def execute + if @main_args.include?("-h") || @main_args.include?("--help") + # Print the help for all the commands. + return help + end + + # If we reached this far then we must have a subcommand. If not, + # then we also just print the help and exit. + command_class = @subcommands.get(@sub_command.to_sym) if @sub_command + return help if !command_class || !@sub_command + @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") + + # Initialize and execute the command class + command_class.new(@sub_args, @env).execute + end + + # Prints the help out for this command + def help + opts = OptionParser.new do |opts| + opts.banner = "Usage: vagrant snapshot []" + opts.separator "" + opts.separator "Available subcommands:" + + # Add the available subcommands as separators in order to print them + # out as well. + keys = [] + @subcommands.each { |key, value| keys << key.to_s } + + keys.sort.each do |key| + opts.separator " #{key}" + end + + opts.separator "" + opts.separator "For help on any individual subcommand run `vagrant snapshot -h`" + end + + @env.ui.info(opts.help, prefix: false) + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/save.rb b/plugins/commands/snapshot/command/save.rb new file mode 100644 index 000000000..496891eac --- /dev/null +++ b/plugins/commands/snapshot/command/save.rb @@ -0,0 +1,40 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class Save < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot save [options] [vm-name] " + o.separator "" + o.separator "Take a snapshot of the current state of the machine. The snapshot" + o.separator "can be restored via `vagrant snapshot restore` at any point in the" + o.separator "future to get back to this exact machine state." + o.separator "" + o.separator "Snapshots are useful for experimenting in a machine and being able" + o.separator "to rollback quickly." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + name = argv.pop + with_target_vms(argv) do |vm| + vm.action(:snapshot_save, snapshot_name: name) + end + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/snapshot/plugin.rb b/plugins/commands/snapshot/plugin.rb new file mode 100644 index 000000000..e83be5bca --- /dev/null +++ b/plugins/commands/snapshot/plugin.rb @@ -0,0 +1,15 @@ +require "vagrant" + +module VagrantPlugins + module CommandSnapshot + class Plugin < Vagrant.plugin("2") + name "snapshot command" + description "The `snapshot` command gives you a way to manage snapshots." + + command("snapshot") do + require File.expand_path("../command/root", __FILE__) + Command::Root + end + end + end +end From d0e8ecfc73e9604d60c3d15aa1aae4bf34f79cdb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Oct 2015 16:16:24 -0400 Subject: [PATCH 083/484] providers/virtualbox: snapshot save/delete --- plugins/providers/virtualbox/action.rb | 29 +++++++++++++++++++ .../virtualbox/action/snapshot_delete.rb | 25 ++++++++++++++++ .../virtualbox/action/snapshot_save.rb | 25 ++++++++++++++++ plugins/providers/virtualbox/driver/meta.rb | 2 ++ .../virtualbox/driver/version_5_0.rb | 8 +++++ templates/locales/en.yml | 11 +++++++ 6 files changed, 100 insertions(+) create mode 100644 plugins/providers/virtualbox/action/snapshot_delete.rb create mode 100644 plugins/providers/virtualbox/action/snapshot_save.rb diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 088221ed1..96cd2373d 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -42,6 +42,8 @@ module VagrantPlugins autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__) autoload :SetName, File.expand_path("../action/set_name", __FILE__) autoload :SetupPackageFiles, File.expand_path("../action/setup_package_files", __FILE__) + autoload :SnapshotDelete, File.expand_path("../action/snapshot_delete", __FILE__) + autoload :SnapshotSave, File.expand_path("../action/snapshot_save", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) # Include the built-in modules so that we can use them as top-level @@ -222,6 +224,33 @@ module VagrantPlugins end end + def self.action_snapshot_delete + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use SnapshotDelete + else + b2.use MessageNotCreated + end + end + end + end + + # This is the action that is primarily responsible for saving a snapshot + def self.action_snapshot_save + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use SnapshotSave + else + b2.use MessageNotCreated + end + end + end + end + # This is the action that will exec into an SSH shell. def self.action_ssh Vagrant::Action::Builder.new.tap do |b| diff --git a/plugins/providers/virtualbox/action/snapshot_delete.rb b/plugins/providers/virtualbox/action/snapshot_delete.rb new file mode 100644 index 000000000..2efc8eae2 --- /dev/null +++ b/plugins/providers/virtualbox/action/snapshot_delete.rb @@ -0,0 +1,25 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class SnapshotDelete + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info(I18n.t( + "vagrant.actions.vm.snapshot.deleting", + name: env[:snapshot_name])) + env[:machine].provider.driver.delete_snapshot( + env[:machine].id, env[:snapshot_name]) + + env[:ui].success(I18n.t( + "vagrant.actions.vm.snapshot.deleted", + name: env[:snapshot_name])) + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/snapshot_save.rb b/plugins/providers/virtualbox/action/snapshot_save.rb new file mode 100644 index 000000000..98b720763 --- /dev/null +++ b/plugins/providers/virtualbox/action/snapshot_save.rb @@ -0,0 +1,25 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class SnapshotSave + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info(I18n.t( + "vagrant.actions.vm.snapshot.saving", + name: env[:snapshot_name])) + env[:machine].provider.driver.create_snapshot( + env[:machine].id, env[:snapshot_name]) + + env[:ui].success(I18n.t( + "vagrant.actions.vm.snapshot.saved", + name: env[:snapshot_name])) + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index df52f0849..a4246ba78 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -89,6 +89,7 @@ module VagrantPlugins :create_host_only_network, :create_snapshot, :delete, + :delete_snapshot, :delete_unused_host_only_networks, :discard_saved_state, :enable_adapters, @@ -113,6 +114,7 @@ module VagrantPlugins :read_vms, :reconfig_host_only, :remove_dhcp_server, + :restore_snapshot, :resume, :set_mac_address, :set_name, diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index df833bb3f..b7e186538 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -86,6 +86,14 @@ module VagrantPlugins execute("snapshot", machine_id, "take", snapshot_name) end + def delete_snapshot(machine_id, snapshot_name) + execute("snapshot", machine_id, "delete", snapshot_name) + end + + def restore_snapshot(machine_id, snapshot_name) + execute("snapshot", machine_id, "restore", snapshot_name) + end + def delete execute("unregistervm", @uuid, "--delete") end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 171cd60ba..7db1260ae 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1765,6 +1765,17 @@ en: set_name: setting_name: |- Setting the name of the VM: %{name} + snapshot: + deleting: |- + Deleting the snapshot '%{name}'... + deleted: |- + Snapshot deleted! + saving: |- + Snapshotting the machine as '%{name}'... + saved: |- + Snapshot saved! You can restore the snapshot at any time by + using `vagrant snapshot restore`. You can delete it using + `vagrant snapshot delete`. suspend: suspending: Saving VM state and suspending execution... From c635352b898b42c4ff7ede683dc527a3dfa5d395 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Oct 2015 16:41:58 -0400 Subject: [PATCH 084/484] providers/virtualbox: list snapshots, progress for delete --- plugins/providers/virtualbox/action.rb | 15 +++++++ .../virtualbox/action/snapshot_delete.rb | 9 +++- .../virtualbox/action/snapshot_list.rb | 22 ++++++++++ .../virtualbox/action/snapshot_restore.rb | 21 ++++++++++ plugins/providers/virtualbox/driver/meta.rb | 1 + .../virtualbox/driver/version_5_0.rb | 41 ++++++++++++++++++- 6 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 plugins/providers/virtualbox/action/snapshot_list.rb create mode 100644 plugins/providers/virtualbox/action/snapshot_restore.rb diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 96cd2373d..d355b8a69 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -43,6 +43,8 @@ module VagrantPlugins autoload :SetName, File.expand_path("../action/set_name", __FILE__) autoload :SetupPackageFiles, File.expand_path("../action/setup_package_files", __FILE__) autoload :SnapshotDelete, File.expand_path("../action/snapshot_delete", __FILE__) + autoload :SnapshotList, File.expand_path("../action/snapshot_list", __FILE__) + autoload :SnapshotRestore, File.expand_path("../action/snapshot_restore", __FILE__) autoload :SnapshotSave, File.expand_path("../action/snapshot_save", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) @@ -237,6 +239,19 @@ module VagrantPlugins end end + def self.action_snapshot_list + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use SnapshotList + else + b2.use MessageNotCreated + end + end + end + end + # This is the action that is primarily responsible for saving a snapshot def self.action_snapshot_save Vagrant::Action::Builder.new.tap do |b| diff --git a/plugins/providers/virtualbox/action/snapshot_delete.rb b/plugins/providers/virtualbox/action/snapshot_delete.rb index 2efc8eae2..1d8cecc73 100644 --- a/plugins/providers/virtualbox/action/snapshot_delete.rb +++ b/plugins/providers/virtualbox/action/snapshot_delete.rb @@ -11,7 +11,14 @@ module VagrantPlugins "vagrant.actions.vm.snapshot.deleting", name: env[:snapshot_name])) env[:machine].provider.driver.delete_snapshot( - env[:machine].id, env[:snapshot_name]) + env[:machine].id, env[:snapshot_name]) do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + # Clear the line one last time since the progress meter doesn't disappear + # immediately. + env[:ui].clear_line env[:ui].success(I18n.t( "vagrant.actions.vm.snapshot.deleted", diff --git a/plugins/providers/virtualbox/action/snapshot_list.rb b/plugins/providers/virtualbox/action/snapshot_list.rb new file mode 100644 index 000000000..e909c5e87 --- /dev/null +++ b/plugins/providers/virtualbox/action/snapshot_list.rb @@ -0,0 +1,22 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class SnapshotList + def initialize(app, env) + @app = app + end + + def call(env) + snapshots = env[:machine].provider.driver.list_snapshots( + env[:machine].id) + + snapshots.each do |snapshot| + env[:machine].ui.output(snapshot, prefix: false) + end + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/snapshot_restore.rb b/plugins/providers/virtualbox/action/snapshot_restore.rb new file mode 100644 index 000000000..c05480261 --- /dev/null +++ b/plugins/providers/virtualbox/action/snapshot_restore.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class SnapshotRestore + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info(I18n.t( + "vagrant.actions.vm.snapshot.restoring", + name: env[:snapshot_name])) + env[:machine].provider.driver.restore_snapshot( + env[:machine].id, env[:snapshot_name]) + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index a4246ba78..136ab196a 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -98,6 +98,7 @@ module VagrantPlugins :forward_ports, :halt, :import, + :list_snapshots, :read_forwarded_ports, :read_bridged_interfaces, :read_dhcp_servers, diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index b7e186538..2a0f3ea3d 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -87,7 +87,46 @@ module VagrantPlugins end def delete_snapshot(machine_id, snapshot_name) - execute("snapshot", machine_id, "delete", snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + # Snapshot and report the % progress + execute("snapshot", machine_id, "delete", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + def list_snapshots(machine_id) + result = [] + output = execute( + "snapshot", machine_id, "list", "--machinereadable", + retryable: true) + output.split("\n").each do |line| + if line =~ /^SnapshotName.*?="(.+?)"$/i + result << $1.to_s + end + end + + result.sort end def restore_snapshot(machine_id, snapshot_name) From 8c0e38b3978acdc18ac2dc3223d219c242db33b1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Oct 2015 16:48:29 -0400 Subject: [PATCH 085/484] providers/virtualbox: snapshot restore --- plugins/providers/virtualbox/action.rb | 19 ++++++++++++++ .../virtualbox/action/snapshot_restore.rb | 9 ++++++- .../virtualbox/driver/version_5_0.rb | 26 ++++++++++++++++++- templates/locales/en.yml | 2 ++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index d355b8a69..d8b4d7ea1 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -252,6 +252,25 @@ module VagrantPlugins end end + # This is the action that is primarily responsible for saving a snapshot + def self.action_snapshot_restore + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if !env[:result] + b2.use MessageNotCreated + next + end + + b2.use CheckAccessible + b2.use EnvSet, force_halt: true + b2.use action_halt + b2.use SnapshotRestore + b2.use action_start + end + end + end + # This is the action that is primarily responsible for saving a snapshot def self.action_snapshot_save Vagrant::Action::Builder.new.tap do |b| diff --git a/plugins/providers/virtualbox/action/snapshot_restore.rb b/plugins/providers/virtualbox/action/snapshot_restore.rb index c05480261..f655471c1 100644 --- a/plugins/providers/virtualbox/action/snapshot_restore.rb +++ b/plugins/providers/virtualbox/action/snapshot_restore.rb @@ -11,7 +11,14 @@ module VagrantPlugins "vagrant.actions.vm.snapshot.restoring", name: env[:snapshot_name])) env[:machine].provider.driver.restore_snapshot( - env[:machine].id, env[:snapshot_name]) + env[:machine].id, env[:snapshot_name]) do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + # Clear the line one last time since the progress meter doesn't disappear + # immediately. + env[:ui].clear_line @app.call(env) end diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index 2a0f3ea3d..9f1c14359 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -130,7 +130,31 @@ module VagrantPlugins end def restore_snapshot(machine_id, snapshot_name) - execute("snapshot", machine_id, "restore", snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + execute("snapshot", machine_id, "restore", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end end def delete diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 7db1260ae..f6d9e8cbc 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1770,6 +1770,8 @@ en: Deleting the snapshot '%{name}'... deleted: |- Snapshot deleted! + restoring: |- + Restoring the snapshot '%{name}'... saving: |- Snapshotting the machine as '%{name}'... saved: |- From 00894b5a2791afce6ac5132c08d2a7d463519c8c Mon Sep 17 00:00:00 2001 From: caleblloyd Date: Wed, 7 Oct 2015 20:25:09 -0400 Subject: [PATCH 086/484] hyper-v boot device by generation fixes #6372 --- .../providers/hyperv/scripts/import_vm.ps1 | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/plugins/providers/hyperv/scripts/import_vm.ps1 b/plugins/providers/hyperv/scripts/import_vm.ps1 index 3e8664eb2..a994c1cfb 100644 --- a/plugins/providers/hyperv/scripts/import_vm.ps1 +++ b/plugins/providers/hyperv/scripts/import_vm.ps1 @@ -81,14 +81,24 @@ if (!$switchname) { $switchname = (Select-Xml -xml $vmconfig -XPath "//AltSwitchName").node."#text" } -# Determine boot device -Switch ((Select-Xml -xml $vmconfig -XPath "//boot").node.device0."#text") { - "Floppy" { $bootdevice = "floppy" } - "HardDrive" { $bootdevice = "IDE" } - "Optical" { $bootdevice = "CD" } - "Network" { $bootdevice = "LegacyNetworkAdapter" } - "Default" { $bootdevice = "IDE" } -} #switch +if ($generation -eq 1) { + # Determine boot device + Switch ((Select-Xml -xml $vmconfig -XPath "//boot").node.device0."#text") { + "Floppy" { $bootdevice = "Floppy" } + "HardDrive" { $bootdevice = "IDE" } + "Optical" { $bootdevice = "CD" } + "Network" { $bootdevice = "LegacyNetworkAdapter" } + "Default" { $bootdevice = "IDE" } + } #switch +} else { + # Determine boot device + Switch ((Select-Xml -xml $vmconfig -XPath "//boot").node.device0."#text") { + "HardDrive" { $bootdevice = "VHD" } + "Optical" { $bootdevice = "CD" } + "Network" { $bootdevice = "NetworkAdapter" } + "Default" { $bootdevice = "VHD" } + } #switch +} # Determine secure boot options $secure_boot_enabled = (Select-Xml -xml $vmconfig -XPath "//secure_boot_enabled").Node."#text" From c36b682e40e46f0e1e48f7d1cc2a24c2eed616a1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Oct 2015 22:22:55 -0400 Subject: [PATCH 087/484] providers/virtualbox: fix error if no snapshots --- plugins/providers/virtualbox/action/snapshot_list.rb | 5 +++++ plugins/providers/virtualbox/driver/base.rb | 3 ++- plugins/providers/virtualbox/driver/version_5_0.rb | 6 +++++- templates/locales/en.yml | 7 +++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/plugins/providers/virtualbox/action/snapshot_list.rb b/plugins/providers/virtualbox/action/snapshot_list.rb index e909c5e87..27c1e3d12 100644 --- a/plugins/providers/virtualbox/action/snapshot_list.rb +++ b/plugins/providers/virtualbox/action/snapshot_list.rb @@ -14,6 +14,11 @@ module VagrantPlugins env[:machine].ui.output(snapshot, prefix: false) end + if snapshots.empty? + env[:machine].ui.output(I18n.t("vagrant.actions.vm.snapshot.list_none")) + env[:machine].ui.detail(I18n.t("vagrant.actions.vm.snapshot.list_none_detail")) + end + @app.call(env) end end diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb index 768bd9cef..8dc0ba994 100644 --- a/plugins/providers/virtualbox/driver/base.rb +++ b/plugins/providers/virtualbox/driver/base.rb @@ -386,7 +386,8 @@ module VagrantPlugins if errored raise Vagrant::Errors::VBoxManageError, command: command.inspect, - stderr: r.stderr + stderr: r.stderr, + stdout: r.stdout end end diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index 9f1c14359..9762eda42 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -116,10 +116,11 @@ module VagrantPlugins end def list_snapshots(machine_id) - result = [] output = execute( "snapshot", machine_id, "list", "--machinereadable", retryable: true) + + result = [] output.split("\n").each do |line| if line =~ /^SnapshotName.*?="(.+?)"$/i result << $1.to_s @@ -127,6 +128,9 @@ module VagrantPlugins end result.sort + rescue Vagrant::Errors::VBoxManageError => e + return [] if e.extra_data[:stdout].include?("does not have") + raise end def restore_snapshot(machine_id, snapshot_name) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index f6d9e8cbc..5d4e4d912 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1770,6 +1770,13 @@ en: Deleting the snapshot '%{name}'... deleted: |- Snapshot deleted! + list_none: |- + No snapshots have been taken yet! + list_none_detail: |- + You can take a snapshot using `vagrant snapshot save`. Note that + not all providers support this yet. Once a snapshot is taken, you + can list them using this command, and use commands such as + `vagrant snapshot restore` to go back to a certain snapshot. restoring: |- Restoring the snapshot '%{name}'... saving: |- From ed4df21c85813eeec76c4c1d3e4bcd097bbe912f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Oct 2015 22:52:27 -0400 Subject: [PATCH 088/484] commands/snapshot: push and pop --- lib/vagrant/action.rb | 1 + lib/vagrant/action/builtin/is_env_set.rb | 23 +++++++ plugins/commands/snapshot/command/pop.rb | 28 ++++++++ plugins/commands/snapshot/command/push.rb | 33 ++++++++++ .../commands/snapshot/command/push_shared.rb | 65 +++++++++++++++++++ plugins/commands/snapshot/command/root.rb | 10 +++ plugins/providers/virtualbox/action.rb | 7 ++ 7 files changed, 167 insertions(+) create mode 100644 lib/vagrant/action/builtin/is_env_set.rb create mode 100644 plugins/commands/snapshot/command/pop.rb create mode 100644 plugins/commands/snapshot/command/push.rb create mode 100644 plugins/commands/snapshot/command/push_shared.rb diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index fcf8d916d..db4875dbb 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -20,6 +20,7 @@ module Vagrant autoload :HandleBox, "vagrant/action/builtin/handle_box" autoload :HandleBoxUrl, "vagrant/action/builtin/handle_box_url" autoload :HandleForwardedPortCollisions, "vagrant/action/builtin/handle_forwarded_port_collisions" + autoload :IsEnvSet, "vagrant/action/builtin/is_env_set" autoload :IsState, "vagrant/action/builtin/is_state" autoload :Lock, "vagrant/action/builtin/lock" autoload :Message, "vagrant/action/builtin/message" diff --git a/lib/vagrant/action/builtin/is_env_set.rb b/lib/vagrant/action/builtin/is_env_set.rb new file mode 100644 index 000000000..d08ed9632 --- /dev/null +++ b/lib/vagrant/action/builtin/is_env_set.rb @@ -0,0 +1,23 @@ +module Vagrant + module Action + module Builtin + # This middleware is meant to be used with Call and can check if + # a variable in env is set. + class IsEnvSet + def initialize(app, env, key, **opts) + @app = app + @logger = Log4r::Logger.new("vagrant::action::builtin::is_env_set") + @key = key + @invert = !!opts[:invert] + end + + def call(env) + @logger.debug("Checking if env is set: '#{@key}'") + env[:result] = !!env[@key] + @logger.debug(" - Result: #{env[:result].inspect}") + @app.call(env) + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/pop.rb b/plugins/commands/snapshot/command/pop.rb new file mode 100644 index 000000000..2c3499f43 --- /dev/null +++ b/plugins/commands/snapshot/command/pop.rb @@ -0,0 +1,28 @@ +require 'json' +require 'optparse' + +require_relative "push_shared" + +module VagrantPlugins + module CommandSnapshot + module Command + class Pop < Vagrant.plugin("2", :command) + include PushShared + + def execute + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot pop [options] [vm-name]" + o.separator "" + o.separator "Restore state that was pushed with `vagrant snapshot push`." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + return shared_exec(argv, method(:pop)) + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/push.rb b/plugins/commands/snapshot/command/push.rb new file mode 100644 index 000000000..c1168bcf2 --- /dev/null +++ b/plugins/commands/snapshot/command/push.rb @@ -0,0 +1,33 @@ +require 'json' +require 'optparse' + +require_relative "push_shared" + +module VagrantPlugins + module CommandSnapshot + module Command + class Push < Vagrant.plugin("2", :command) + include PushShared + + def execute + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot push [options] [vm-name]" + o.separator "" + o.separator "Take a snapshot of the current state of the machine and 'push'" + o.separator "it onto the stack of states. You can use `vagrant snapshot pop`" + o.separator "to restore back to this state at any time." + o.separator "" + o.separator "If you use `vagrant snapshot save` or restore at any point after" + o.separator "a push, pop will still bring you back to this pushed state." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + return shared_exec(argv, method(:push)) + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/push_shared.rb b/plugins/commands/snapshot/command/push_shared.rb new file mode 100644 index 000000000..880f8bcf5 --- /dev/null +++ b/plugins/commands/snapshot/command/push_shared.rb @@ -0,0 +1,65 @@ +require 'json' + +module VagrantPlugins + module CommandSnapshot + module Command + module PushShared + def shared_exec(argv, m) + with_target_vms(argv) do |vm| + if !vm.id + vm.ui.info("Not created. Cannot push snapshot state.") + next + end + + vm.env.lock("machine-snapshot-stack") do + m.call(vm) + end + end + + 0 + end + + def push(machine) + snapshot_name = "push_#{Time.now.to_i}_#{rand(10000)}" + + # Save the snapshot. This will raise an exception if it fails. + machine.action(:snapshot_save, snapshot_name: snapshot_name) + + # Success! Write the resulting stack out + modify_snapshot_stack(machine) do |stack| + stack << snapshot_name + end + end + + def pop(machine) + modify_snapshot_stack(machine) do |stack| + name = stack.pop + + # Restore the snapshot and tell the provider to delete it as well. + machine.action( + :snapshot_restore, + snapshot_name: name, + snapshot_delete: true) + end + end + + protected + + def modify_snapshot_stack(machine) + # Get the stack + snapshot_stack = [] + snapshot_file = machine.data_dir.join("snapshot_stack") + snapshot_stack = JSON.parse(snapshot_file.read) if snapshot_file.file? + + # Yield it so it can be modified + yield snapshot_stack + + # Write it out + snapshot_file.open("w+") do |f| + f.write(JSON.dump(snapshot_stack)) + end + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/root.rb b/plugins/commands/snapshot/command/root.rb index 03bcebb80..2ed460fa4 100644 --- a/plugins/commands/snapshot/command/root.rb +++ b/plugins/commands/snapshot/command/root.rb @@ -33,6 +33,16 @@ module VagrantPlugins require File.expand_path("../list", __FILE__) List end + + @subcommands.register(:push) do + require File.expand_path("../push", __FILE__) + Push + end + + @subcommands.register(:pop) do + require File.expand_path("../pop", __FILE__) + Pop + end end def execute diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index d8b4d7ea1..12773736d 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -266,6 +266,13 @@ module VagrantPlugins b2.use EnvSet, force_halt: true b2.use action_halt b2.use SnapshotRestore + + b2.use Call, IsEnvSet, :snapshot_delete do |env2, b3| + if env2[:result] + b3.use action_snapshot_delete + end + end + b2.use action_start end end From cc8cdafdc32f4b6e627421e4c06fda1e009f5886 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Oct 2015 22:54:27 -0400 Subject: [PATCH 089/484] test: test for IsEnvSet --- .../vagrant/action/builtin/is_env_set_test.rb | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/unit/vagrant/action/builtin/is_env_set_test.rb diff --git a/test/unit/vagrant/action/builtin/is_env_set_test.rb b/test/unit/vagrant/action/builtin/is_env_set_test.rb new file mode 100644 index 000000000..56eafa956 --- /dev/null +++ b/test/unit/vagrant/action/builtin/is_env_set_test.rb @@ -0,0 +1,31 @@ +require "pathname" +require "tmpdir" + +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::IsEnvSet do + let(:app) { lambda { |env| } } + let(:env) { { } } + + describe "#call" do + it "sets result to true if it is set" do + env[:bar] = true + + subject = described_class.new(app, env, :bar) + + expect(app).to receive(:call).with(env) + + subject.call(env) + expect(env[:result]).to be_true + end + + it "sets result to false if it isn't set" do + subject = described_class.new(app, env, :bar) + + expect(app).to receive(:call).with(env) + + subject.call(env) + expect(env[:result]).to be_false + end + end +end From 94b675581379a062d6555adfa6ac82b87ec9084b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Oct 2015 23:03:13 -0400 Subject: [PATCH 090/484] website/docs: update docs for snapshotting --- website/docs/source/layouts/layout.erb | 1 + website/docs/source/v2/cli/snapshot.html.md | 79 +++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 website/docs/source/v2/cli/snapshot.html.md diff --git a/website/docs/source/layouts/layout.erb b/website/docs/source/layouts/layout.erb index 4fedc3627..259577d79 100644 --- a/website/docs/source/layouts/layout.erb +++ b/website/docs/source/layouts/layout.erb @@ -108,6 +108,7 @@ >reload >resume >share + >snapshot >ssh >ssh-config >status diff --git a/website/docs/source/v2/cli/snapshot.html.md b/website/docs/source/v2/cli/snapshot.html.md new file mode 100644 index 000000000..42a2dfc68 --- /dev/null +++ b/website/docs/source/v2/cli/snapshot.html.md @@ -0,0 +1,79 @@ +--- +page_title: "vagrant snapshot - Command-Line Interface" +sidebar_current: "cli-snapshot" +--- + +# Snapshot + +**Command: `vagrant snapshot`** + +This is the command used to manage snapshots with the guest machine. +Snapshots record a point-in-time state of a guest machine. You can then +quickly restore to this environment. This lets you experiment and try things +and quickly restore back to a previous state. + +Snapshotting is not supported by every provider. If it isn't supported, +Vagrant will give you an error message. + +The main functionality of this command is exposed via even more subcommands: + +* `push` +* `pop` +* `save` +* `restore` +* `list` +* `delete` + +# Snapshot Push + +**Command: `vagrant snapshot push`** + +This takes a snapshot and pushes it onto the snapshot stack. + +This is a shorthand for `vagrant snapshot save` where you don't need +to specify a name. When you call the inverse `vagrant snapshot pop`, it will +restore the pushed state. + +~> **Warning:** If you are using `push` and `pop`, avoid using `save` + and `restore` which are unsafe to mix. + +# Snapshot Pop + +**Command: `vagrant snapshot pop`** + +This command is the inverse of `vagrant snapshot push`: it will restore +the pushed state. + +# Snapshot Save + +**Command: `vagrant snapshot save NAME`** + +This command saves a new named snapshot. If this command is used, the +`push` and `pop` subcommands cannot be safely used. + +# Snapshot Restore + +**Command: `vagrant snapshot restore NAME`** + +This command restores the named snapshot. + +# Snapshot List + +**Command: `vagrant snapshot list`** + +This command will list all the snapshots taken. + +# Snapshot Delete + +**Command: `vagrant snapshot delete NAME`** + +This command will delete the named snapshot. + +Some providers require all "child" snapshots to be deleted first. Vagrant +itself doesn't track what these children are. If this is the case (such +as with VirtualBox), then you must be sure to delete the snapshots in the +reverse order they were taken. + +This command is typically _much faster_ if the machine is halted prior to +snapshotting. If this isn't an option, or isn't ideal, then the deletion +can also be done online with most providers. From 7480b65e9dff0cbb1ada8f8fe4d7086882b23e28 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 08:46:36 -0400 Subject: [PATCH 091/484] providers/virtualbox: use caps for snapshot list --- plugins/commands/snapshot/command/list.rb | 23 +++++++++++++--- plugins/providers/virtualbox/action.rb | 14 ---------- .../virtualbox/action/snapshot_list.rb | 27 ------------------- plugins/providers/virtualbox/cap.rb | 7 +++++ plugins/providers/virtualbox/plugin.rb | 5 ++++ templates/locales/en.yml | 7 +++++ 6 files changed, 39 insertions(+), 44 deletions(-) delete mode 100644 plugins/providers/virtualbox/action/snapshot_list.rb diff --git a/plugins/commands/snapshot/command/list.rb b/plugins/commands/snapshot/command/list.rb index af91e76f5..d0c433b94 100644 --- a/plugins/commands/snapshot/command/list.rb +++ b/plugins/commands/snapshot/command/list.rb @@ -5,8 +5,6 @@ module VagrantPlugins module Command class List < Vagrant.plugin("2", :command) def execute - options = {} - opts = OptionParser.new do |o| o.banner = "Usage: vagrant snapshot list [options] [vm-name]" o.separator "" @@ -18,7 +16,26 @@ module VagrantPlugins return if !argv with_target_vms(argv) do |vm| - vm.action(:snapshot_list) + if !vm.id + vm.ui.info(I18n.t("vagrant.commands.common.vm_not_created")) + next + end + + if !vm.provider.capability?(:snapshot_list) + vm.ui.info(I18n.t("vagrant.commands.snapshot.not_supported")) + next + end + + snapshots = vm.provider.capability(:snapshot_list) + if snapshots.empty? + vm.ui.output(I18n.t("vagrant.actions.vm.snapshot.list_none")) + vm.ui.detail(I18n.t("vagrant.actions.vm.snapshot.list_none_detail")) + next + end + + snapshots.each do |snapshot| + vm.ui.output(snapshot, prefix: false) + end end # Success, exit status 0 diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 12773736d..9c4d68c72 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -43,7 +43,6 @@ module VagrantPlugins autoload :SetName, File.expand_path("../action/set_name", __FILE__) autoload :SetupPackageFiles, File.expand_path("../action/setup_package_files", __FILE__) autoload :SnapshotDelete, File.expand_path("../action/snapshot_delete", __FILE__) - autoload :SnapshotList, File.expand_path("../action/snapshot_list", __FILE__) autoload :SnapshotRestore, File.expand_path("../action/snapshot_restore", __FILE__) autoload :SnapshotSave, File.expand_path("../action/snapshot_save", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) @@ -239,19 +238,6 @@ module VagrantPlugins end end - def self.action_snapshot_list - Vagrant::Action::Builder.new.tap do |b| - b.use CheckVirtualbox - b.use Call, Created do |env, b2| - if env[:result] - b2.use SnapshotList - else - b2.use MessageNotCreated - end - end - end - end - # This is the action that is primarily responsible for saving a snapshot def self.action_snapshot_restore Vagrant::Action::Builder.new.tap do |b| diff --git a/plugins/providers/virtualbox/action/snapshot_list.rb b/plugins/providers/virtualbox/action/snapshot_list.rb deleted file mode 100644 index 27c1e3d12..000000000 --- a/plugins/providers/virtualbox/action/snapshot_list.rb +++ /dev/null @@ -1,27 +0,0 @@ -module VagrantPlugins - module ProviderVirtualBox - module Action - class SnapshotList - def initialize(app, env) - @app = app - end - - def call(env) - snapshots = env[:machine].provider.driver.list_snapshots( - env[:machine].id) - - snapshots.each do |snapshot| - env[:machine].ui.output(snapshot, prefix: false) - end - - if snapshots.empty? - env[:machine].ui.output(I18n.t("vagrant.actions.vm.snapshot.list_none")) - env[:machine].ui.detail(I18n.t("vagrant.actions.vm.snapshot.list_none_detail")) - end - - @app.call(env) - end - end - end - end -end diff --git a/plugins/providers/virtualbox/cap.rb b/plugins/providers/virtualbox/cap.rb index e459c97bf..77f8ee1ad 100644 --- a/plugins/providers/virtualbox/cap.rb +++ b/plugins/providers/virtualbox/cap.rb @@ -22,6 +22,13 @@ module VagrantPlugins def self.nic_mac_addresses(machine) machine.provider.driver.read_mac_addresses end + + # Returns a list of the snapshots that are taken on this machine. + # + # @return [Array] Snapshot Name + def self.snapshot_list(machine) + machine.provider.driver.list_snapshots(machine.id) + end end end end diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb index 18e33d4bb..84f86ba51 100644 --- a/plugins/providers/virtualbox/plugin.rb +++ b/plugins/providers/virtualbox/plugin.rb @@ -33,6 +33,11 @@ module VagrantPlugins require_relative "cap" Cap end + + provider_capability(:virtualbox, :snapshot_list) do + require_relative "cap" + Cap + end end autoload :Action, File.expand_path("../action", __FILE__) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 5d4e4d912..c57f78688 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1501,6 +1501,13 @@ en: Post install message from the '%{name}' plugin: %{message} + snapshot: |- + not_supported: |- + This provider doesn't support snapshots. + + This may be intentional or this may be a bug. If this provider + should support snapshots, then please report this as a bug to the + maintainer of the provider. status: aborted: |- The VM is in an aborted state. This means that it was abruptly From 0abc17eaed7755f9ebc2c3cabd3de8a73c843064 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 08:55:13 -0400 Subject: [PATCH 092/484] commands/snapshot: push now uses caps to be more resilient --- .../commands/snapshot/command/push_shared.rb | 47 ++++++++----------- templates/locales/en.yml | 6 ++- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/plugins/commands/snapshot/command/push_shared.rb b/plugins/commands/snapshot/command/push_shared.rb index 880f8bcf5..bbaba8dbc 100644 --- a/plugins/commands/snapshot/command/push_shared.rb +++ b/plugins/commands/snapshot/command/push_shared.rb @@ -24,40 +24,31 @@ module VagrantPlugins # Save the snapshot. This will raise an exception if it fails. machine.action(:snapshot_save, snapshot_name: snapshot_name) - - # Success! Write the resulting stack out - modify_snapshot_stack(machine) do |stack| - stack << snapshot_name - end end def pop(machine) - modify_snapshot_stack(machine) do |stack| - name = stack.pop - - # Restore the snapshot and tell the provider to delete it as well. - machine.action( - :snapshot_restore, - snapshot_name: name, - snapshot_delete: true) + # By reverse sorting, we should be able to find the first + # pushed snapshot. + name = nil + snapshots = machine.provider.capability(:snapshot_list) + snapshots.sort.reverse.each do |snapshot| + if snapshot =~ /^push_\d+_\d+$/ + name = snapshot + break + end end - end - protected - - def modify_snapshot_stack(machine) - # Get the stack - snapshot_stack = [] - snapshot_file = machine.data_dir.join("snapshot_stack") - snapshot_stack = JSON.parse(snapshot_file.read) if snapshot_file.file? - - # Yield it so it can be modified - yield snapshot_stack - - # Write it out - snapshot_file.open("w+") do |f| - f.write(JSON.dump(snapshot_stack)) + # If no snapshot was found, we never pushed + if !name + machine.ui.info(I18n.t("vagrant.commands.snapshot.no_push_snapshot")) + return end + + # Restore the snapshot and tell the provider to delete it as well. + machine.action( + :snapshot_restore, + snapshot_name: name, + snapshot_delete: true) end end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index c57f78688..929962453 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1501,13 +1501,17 @@ en: Post install message from the '%{name}' plugin: %{message} - snapshot: |- + snapshot: not_supported: |- This provider doesn't support snapshots. This may be intentional or this may be a bug. If this provider should support snapshots, then please report this as a bug to the maintainer of the provider. + no_push_snapshot: |- + No pushed snapshot found! + + Use `vagrant snapshot push` to push a snapshot to restore to. status: aborted: |- The VM is in an aborted state. This means that it was abruptly From 6e187aaefbbe7b22938960d2c7cf29458409ecef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 08:57:21 -0400 Subject: [PATCH 093/484] providers/virtualbox: v4.2 and 4.3 support --- .../virtualbox/driver/version_4_2.rb | 79 +++++++++++++++++++ .../virtualbox/driver/version_4_3.rb | 75 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/plugins/providers/virtualbox/driver/version_4_2.rb b/plugins/providers/virtualbox/driver/version_4_2.rb index 0f1f79698..b3f33f110 100644 --- a/plugins/providers/virtualbox/driver/version_4_2.rb +++ b/plugins/providers/virtualbox/driver/version_4_2.rb @@ -608,6 +608,85 @@ module VagrantPlugins execute("showvminfo", uuid) return true end + + def create_snapshot(machine_id, snapshot_name) + execute("snapshot", machine_id, "take", snapshot_name) + end + + def delete_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + # Snapshot and report the % progress + execute("snapshot", machine_id, "delete", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + def list_snapshots(machine_id) + output = execute( + "snapshot", machine_id, "list", "--machinereadable", + retryable: true) + + result = [] + output.split("\n").each do |line| + if line =~ /^SnapshotName.*?="(.+?)"$/i + result << $1.to_s + end + end + + result.sort + rescue Vagrant::Errors::VBoxManageError => e + return [] if e.extra_data[:stdout].include?("does not have") + raise + end + + def restore_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + execute("snapshot", machine_id, "restore", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end end end end diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index bc2c87478..b51eec37a 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -90,6 +90,81 @@ module VagrantPlugins execute("snapshot", machine_id, "take", snapshot_name) end + def delete_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + # Snapshot and report the % progress + execute("snapshot", machine_id, "delete", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + def list_snapshots(machine_id) + output = execute( + "snapshot", machine_id, "list", "--machinereadable", + retryable: true) + + result = [] + output.split("\n").each do |line| + if line =~ /^SnapshotName.*?="(.+?)"$/i + result << $1.to_s + end + end + + result.sort + rescue Vagrant::Errors::VBoxManageError => e + return [] if e.extra_data[:stdout].include?("does not have") + raise + end + + def restore_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + execute("snapshot", machine_id, "restore", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + def delete execute("unregistervm", @uuid, "--delete") end From 31ae00cfc39a485835e9b4a32843bb7901d67dec Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 09:10:55 -0400 Subject: [PATCH 094/484] test: more tests for snapshots --- .../commands/snapshot/command/pop_test.rb | 52 +++++++++++++++++++ .../commands/snapshot/command/push_test.rb | 46 ++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 test/unit/plugins/commands/snapshot/command/pop_test.rb create mode 100644 test/unit/plugins/commands/snapshot/command/push_test.rb diff --git a/test/unit/plugins/commands/snapshot/command/pop_test.rb b/test/unit/plugins/commands/snapshot/command/pop_test.rb new file mode 100644 index 000000000..66687868f --- /dev/null +++ b/test/unit/plugins/commands/snapshot/command/pop_test.rb @@ -0,0 +1,52 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/snapshot/command/pop") + +describe VagrantPlugins::CommandSnapshot::Command::Pop do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:guest) { double("guest") } + let(:host) { double("host") } + let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + + let(:argv) { [] } + + subject { described_class.new(argv, iso_env) } + + before do + allow(subject).to receive(:with_target_vms) { |&block| block.call machine } + end + + describe "execute" do + it "calls snapshot_restore with the last pushed snapshot" do + machine.id = "foo" + + allow(machine.provider).to receive(:capability). + with(:snapshot_list).and_return(["push_2_0", "push_1_0"]) + + expect(machine).to receive(:action) do |name, opts| + expect(name).to eq(:snapshot_restore) + expect(opts[:snapshot_name]).to eq("push_2_0") + end + + expect(subject.execute).to eq(0) + end + + it "isn't an error if no matching snapshot" do + machine.id = "foo" + + allow(machine.provider).to receive(:capability). + with(:snapshot_list).and_return(["foo"]) + + expect(machine).to_not receive(:action) + expect(subject.execute).to eq(0) + end + end +end diff --git a/test/unit/plugins/commands/snapshot/command/push_test.rb b/test/unit/plugins/commands/snapshot/command/push_test.rb new file mode 100644 index 000000000..3da907cfa --- /dev/null +++ b/test/unit/plugins/commands/snapshot/command/push_test.rb @@ -0,0 +1,46 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/snapshot/command/push") + +describe VagrantPlugins::CommandSnapshot::Command::Push do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:guest) { double("guest") } + let(:host) { double("host") } + let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + + let(:argv) { [] } + + subject { described_class.new(argv, iso_env) } + + before do + allow(subject).to receive(:with_target_vms) { |&block| block.call machine } + end + + describe "execute" do + it "calls snapshot_save with a random snapshot name" do + machine.id = "foo" + + expect(machine).to receive(:action) do |name, opts| + expect(name).to eq(:snapshot_save) + expect(opts[:snapshot_name]).to match(/^push_/) + end + + expect(subject.execute).to eq(0) + end + + it "doesn't snapshot a non-existent machine" do + machine.id = nil + + expect(machine).to_not receive(:action) + expect(subject.execute).to eq(0) + end + end +end From 99d29f17fa88b46e91bce85723ba0a305f4a05e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 10:35:36 -0400 Subject: [PATCH 095/484] commands/cap --- plugins/commands/cap/command.rb | 68 +++++++++++++++++++ plugins/commands/cap/plugin.rb | 17 +++++ .../unit/plugins/commands/cap/command_test.rb | 51 ++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 plugins/commands/cap/command.rb create mode 100644 plugins/commands/cap/plugin.rb create mode 100644 test/unit/plugins/commands/cap/command_test.rb diff --git a/plugins/commands/cap/command.rb b/plugins/commands/cap/command.rb new file mode 100644 index 000000000..0c2d04e86 --- /dev/null +++ b/plugins/commands/cap/command.rb @@ -0,0 +1,68 @@ +require 'optparse' + +module VagrantPlugins + module CommandCap + class Command < Vagrant.plugin("2", :command) + def self.synopsis + "checks and executes capability" + end + + def execute + options = {} + options[:check] = false + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant cap [options] TYPE NAME [args]" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("--check", "Only checks for a capability, does not execute") do |f| + options[:check] = f + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.length < 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + type = argv.shift.to_sym + name = argv.shift.to_sym + + # Get the proper capability host to check + cap_host = nil + if type == :host + cap_host = @env.host + else + with_target_vms([]) do |vm| + cap_host = case type + when :provider + vm.provider + when :guest + vm.guest + else + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + end + end + + # If we're just checking, then just return exit codes + if options[:check] + return 0 if cap_host.capability?(name) + return 1 + end + + # Otherwise, call it + cap_host.capability(name, *argv) + + # Success, exit status 0 + 0 + end + end + end +end diff --git a/plugins/commands/cap/plugin.rb b/plugins/commands/cap/plugin.rb new file mode 100644 index 000000000..dd8ffbb23 --- /dev/null +++ b/plugins/commands/cap/plugin.rb @@ -0,0 +1,17 @@ +require "vagrant" + +module VagrantPlugins + module CommandCap + class Plugin < Vagrant.plugin("2") + name "cap command" + description <<-DESC + The `cap` command checks and executes arbitrary capabilities. + DESC + + command("cap", primary: false) do + require File.expand_path("../command", __FILE__) + Command + end + end + end +end diff --git a/test/unit/plugins/commands/cap/command_test.rb b/test/unit/plugins/commands/cap/command_test.rb new file mode 100644 index 000000000..9e3394fec --- /dev/null +++ b/test/unit/plugins/commands/cap/command_test.rb @@ -0,0 +1,51 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/cap/command") + +describe VagrantPlugins::CommandCap::Command do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:guest) { double("guest") } + let(:host) { double("host") } + let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + + let(:argv) { [] } + + subject { described_class.new(argv, iso_env) } + + before do + allow(subject).to receive(:with_target_vms) { |&block| block.call machine } + end + + describe "execute" do + context "--check provider foo (exists)" do + let(:argv) { ["--check", "provider", "foo"] } + let(:cap) { Class.new } + + before do + register_plugin do |p| + p.provider_capability(:dummy, :foo) { cap } + end + end + + it "exits with 0 if it exists" do + expect(subject.execute).to eq(0) + end + end + + context "--check provider foo (doesn't exists)" do + let(:argv) { ["--check", "provider", "foo"] } + + it "exits with 1" do + expect(subject.execute).to eq(1) + end + end + end +end From 0b2d60ac399527d896b6c21c99f6fde133cff62e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 10:36:44 -0400 Subject: [PATCH 096/484] commands/cap: better help --- plugins/commands/cap/command.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/commands/cap/command.rb b/plugins/commands/cap/command.rb index 0c2d04e86..43a80cfb5 100644 --- a/plugins/commands/cap/command.rb +++ b/plugins/commands/cap/command.rb @@ -14,6 +14,13 @@ module VagrantPlugins opts = OptionParser.new do |o| o.banner = "Usage: vagrant cap [options] TYPE NAME [args]" o.separator "" + o.separator "This is an advanced command. If you don't know what this" + o.separator "does and you aren't explicitly trying to use it, you probably" + o.separator "don't want to use this." + o.separator "" + o.separator "This command checks or executes arbitrary capabilities that" + o.separator "Vagrant has for hosts, guests, and providers." + o.separator "" o.separator "Options:" o.separator "" From 9e371277a9138e40908e1347750412946d4a345c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 10:38:18 -0400 Subject: [PATCH 097/484] core: IsEnvSet remove invert opt --- lib/vagrant/action/builtin/is_env_set.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/vagrant/action/builtin/is_env_set.rb b/lib/vagrant/action/builtin/is_env_set.rb index d08ed9632..269a6479c 100644 --- a/lib/vagrant/action/builtin/is_env_set.rb +++ b/lib/vagrant/action/builtin/is_env_set.rb @@ -8,7 +8,6 @@ module Vagrant @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::is_env_set") @key = key - @invert = !!opts[:invert] end def call(env) From 0a52e0629859cd310b0773ab88de2e3f008c420b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 10:40:46 -0400 Subject: [PATCH 098/484] commands/snapshot: use require relative --- plugins/commands/snapshot/command/push_shared.rb | 1 + plugins/commands/snapshot/command/root.rb | 12 ++++++------ plugins/commands/snapshot/plugin.rb | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/commands/snapshot/command/push_shared.rb b/plugins/commands/snapshot/command/push_shared.rb index bbaba8dbc..a82779ee5 100644 --- a/plugins/commands/snapshot/command/push_shared.rb +++ b/plugins/commands/snapshot/command/push_shared.rb @@ -16,6 +16,7 @@ module VagrantPlugins end end + # Success, exit with 0 0 end diff --git a/plugins/commands/snapshot/command/root.rb b/plugins/commands/snapshot/command/root.rb index 2ed460fa4..4ced72277 100644 --- a/plugins/commands/snapshot/command/root.rb +++ b/plugins/commands/snapshot/command/root.rb @@ -15,32 +15,32 @@ module VagrantPlugins @subcommands = Vagrant::Registry.new @subcommands.register(:save) do - require File.expand_path("../save", __FILE__) + require_relative "save" Save end @subcommands.register(:restore) do - require File.expand_path("../restore", __FILE__) + require_relative "restore" Restore end @subcommands.register(:delete) do - require File.expand_path("../delete", __FILE__) + require_relative "delete" Delete end @subcommands.register(:list) do - require File.expand_path("../list", __FILE__) + require_relative "list" List end @subcommands.register(:push) do - require File.expand_path("../push", __FILE__) + require_relative "push" Push end @subcommands.register(:pop) do - require File.expand_path("../pop", __FILE__) + require_relative "pop" Pop end end diff --git a/plugins/commands/snapshot/plugin.rb b/plugins/commands/snapshot/plugin.rb index e83be5bca..5b2af81df 100644 --- a/plugins/commands/snapshot/plugin.rb +++ b/plugins/commands/snapshot/plugin.rb @@ -7,7 +7,7 @@ module VagrantPlugins description "The `snapshot` command gives you a way to manage snapshots." command("snapshot") do - require File.expand_path("../command/root", __FILE__) + require_relative "command/root" Command::Root end end From 50638b2e55a2323fed9cb487b90c1a5819e95c9c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 10:42:10 -0400 Subject: [PATCH 099/484] commands/cap: require_relative --- plugins/commands/cap/plugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/commands/cap/plugin.rb b/plugins/commands/cap/plugin.rb index dd8ffbb23..21199c9a9 100644 --- a/plugins/commands/cap/plugin.rb +++ b/plugins/commands/cap/plugin.rb @@ -9,7 +9,7 @@ module VagrantPlugins DESC command("cap", primary: false) do - require File.expand_path("../command", __FILE__) + require_relative "command" Command end end From 6c55fef21d421b592e8be1acd61eaa25da2dda80 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 10:53:07 -0400 Subject: [PATCH 100/484] update CHANGELOG --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8840cc522..c794b3ce0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ FEATURES: - - **IPv6 Private Networks**: Private networking now supports IPv6. This - only works with VirtualBox and VMware at this point. [GH-6342] - **Linked Clones**: VirtualBox and VMware providers now support linked clones for very fast (millisecond) imports on up. [GH-4484] + - **Snapshots**: The `vagrant snapshot` command can be used to checkpoint + and restore point-in-time snapshots. + - **IPv6 Private Networks**: Private networking now supports IPv6. This + only works with VirtualBox and VMware at this point. [GH-6342] IMPROVEMENTS: From e45ba11e14c01090c4ad9e3e059fbcfe983b7487 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 11:14:32 -0400 Subject: [PATCH 101/484] up version for dev --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 10c088013..31b38cacd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.7.4 +1.8.0.dev From 44d484e2e09bcb4ea6d266c78f09fd5ee0e74bf6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 11:58:47 -0400 Subject: [PATCH 102/484] providers/virtualbox: ability to customize linked clone snapshot --- .../providers/virtualbox/action/create_clone.rb | 13 +++++++++++-- .../providers/virtualbox/action/import_master.rb | 14 ++++++++++---- plugins/providers/virtualbox/config.rb | 10 ++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/plugins/providers/virtualbox/action/create_clone.rb b/plugins/providers/virtualbox/action/create_clone.rb index 9cc8f7ed5..a76e23794 100644 --- a/plugins/providers/virtualbox/action/create_clone.rb +++ b/plugins/providers/virtualbox/action/create_clone.rb @@ -12,9 +12,18 @@ module VagrantPlugins def call(env) @logger.info("Creating linked clone from master '#{env[:master_id]}'") - env[:ui].info I18n.t("vagrant.actions.vm.clone.creating", name: env[:machine].box.name) + # Get the snapshot to base the linked clone on. This defaults + # to "base" which is automatically setup with linked clones. + snapshot = "base" + if env[:machine].provider_config.linked_clone_snapshot + snapshot = env[:machine].provider_config.linked_clone_snapshot + end + + # Do the actual clone + env[:ui].info I18n.t( + "vagrant.actions.vm.clone.creating", name: env[:machine].box.name) env[:machine].id = env[:machine].provider.driver.clonevm( - env[:master_id], env[:machine].box.name, "base") do |progress| + env[:master_id], env[:machine].box.name, snapshot) do |progress| env[:ui].clear_line env[:ui].report_progress(progress, 100, false) end diff --git a/plugins/providers/virtualbox/action/import_master.rb b/plugins/providers/virtualbox/action/import_master.rb index 5b9ae7790..985a346ba 100644 --- a/plugins/providers/virtualbox/action/import_master.rb +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -59,10 +59,16 @@ module VagrantPlugins "Imported box #{env[:machine].box.name} as master vm " + "with id #{env[:master_id]}") - @logger.info("Creating base snapshot for master VM.") - env[:machine].provider.driver.create_snapshot(env[:master_id], "base") do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) + if !env[:machine].provider_config.linked_clone_snapshot + snapshots = env[:machine].provider.driver.list_snapshots(env[:master_id]) + if !snapshots.include?("base") + @logger.info("Creating base snapshot for master VM.") + env[:machine].provider.driver.create_snapshot( + env[:master_id], "base") do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + end end @logger.debug("Writing id of master VM '#{env[:master_id]}' to #{master_id_file}") diff --git a/plugins/providers/virtualbox/config.rb b/plugins/providers/virtualbox/config.rb index 12b882b01..d549606ec 100644 --- a/plugins/providers/virtualbox/config.rb +++ b/plugins/providers/virtualbox/config.rb @@ -38,6 +38,14 @@ module VagrantPlugins # @return [Boolean] attr_accessor :linked_clone + # The snapshot to base the linked clone from. If this isn't set + # a snapshot will be made with the name of "base" which will be used. + # + # If this is set, then the snapshot must already exist. + # + # @return [String] + attr_accessor :linked_clone_snapshot + # This should be set to the name of the machine in the VirtualBox # GUI. # @@ -66,6 +74,7 @@ module VagrantPlugins @network_adapters = {} @gui = UNSET_VALUE @linked_clone = UNSET_VALUE + @linked_clone_snapshot = UNSET_VALUE # We require that network adapter 1 is a NAT device. network_adapter(1, :nat) @@ -145,6 +154,7 @@ module VagrantPlugins # Do not create linked clone by default @linked_clone = false if @linked_clone == UNSET_VALUE + @linked_clone_snapshot = nil if @linked_clone_snapshot == UNSET_VALUE # The default name is just nothing, and we default it @name = nil if @name == UNSET_VALUE From 06f8595bc05646fec4534d010f05815037fa14ce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 11:37:35 -0400 Subject: [PATCH 103/484] kernel/v2: clone option --- plugins/kernel_v2/config/vm.rb | 7 +++++++ templates/locales/en.yml | 1 + test/unit/plugins/kernel_v2/config/vm_test.rb | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 2de1b343f..acd21f20d 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -29,6 +29,7 @@ module VagrantPlugins attr_accessor :box_download_client_cert attr_accessor :box_download_insecure attr_accessor :box_download_location_trusted + attr_accessor :clone attr_accessor :communicator attr_accessor :graceful_halt_timeout attr_accessor :guest @@ -54,6 +55,7 @@ module VagrantPlugins @box_download_location_trusted = UNSET_VALUE @box_url = UNSET_VALUE @box_version = UNSET_VALUE + @clone = UNSET_VALUE @communicator = UNSET_VALUE @graceful_halt_timeout = UNSET_VALUE @guest = UNSET_VALUE @@ -367,6 +369,7 @@ module VagrantPlugins @box_download_location_trusted = false if @box_download_location_trusted == UNSET_VALUE @box_url = nil if @box_url == UNSET_VALUE @box_version = nil if @box_version == UNSET_VALUE + @clone = nil if @clone == UNSET_VALUE @communicator = nil if @communicator == UNSET_VALUE @graceful_halt_timeout = 60 if @graceful_halt_timeout == UNSET_VALUE @guest = nil if @guest == UNSET_VALUE @@ -558,6 +561,10 @@ module VagrantPlugins errors << I18n.t("vagrant.config.vm.box_missing") end + if box && clone + errors << I18n.t("vagrant.config.vm.clone_and_box") + end + errors << I18n.t("vagrant.config.vm.hostname_invalid_characters") if \ @hostname && @hostname !~ /^[a-z0-9][-.a-z0-9]*$/i diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 929962453..ff2f1fac1 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1404,6 +1404,7 @@ en: box_download_checksum_notblank: |- Checksum specified but must also specify "box_download_checksum_type" box_missing: "A box must be specified." + clone_and_box: "Only one of clone or box can be specified." hostname_invalid_characters: |- The hostname set for the VM should only contain letters, numbers, hyphens or dots. It cannot start with a hyphen or dot. diff --git a/test/unit/plugins/kernel_v2/config/vm_test.rb b/test/unit/plugins/kernel_v2/config/vm_test.rb index 3d9b01213..146b3f344 100644 --- a/test/unit/plugins/kernel_v2/config/vm_test.rb +++ b/test/unit/plugins/kernel_v2/config/vm_test.rb @@ -65,6 +65,12 @@ describe VagrantPlugins::Kernel_V2::VMConfig do subject.finalize! assert_valid end + + it "is invalid if clone is set" do + subject.clone = "foo" + subject.finalize! + assert_invalid + end end context "#box_check_update" do From 20310dce0ca82872d72d8f33540ae35aca1e3054 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 11:50:06 -0400 Subject: [PATCH 104/484] noop --- plugins/kernel_v2/config/vm.rb | 4 +++- plugins/providers/virtualbox/action.rb | 1 + plugins/providers/virtualbox/action/create_clone.rb | 2 -- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index acd21f20d..3d87e2b87 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -29,7 +29,6 @@ module VagrantPlugins attr_accessor :box_download_client_cert attr_accessor :box_download_insecure attr_accessor :box_download_location_trusted - attr_accessor :clone attr_accessor :communicator attr_accessor :graceful_halt_timeout attr_accessor :guest @@ -38,6 +37,9 @@ module VagrantPlugins attr_accessor :usable_port_range attr_reader :provisioners + # This is an experimental feature that isn't public yet. + attr_accessor :clone + def initialize @logger = Log4r::Logger.new("vagrant::config::vm") diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 9c4d68c72..a64782973 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -394,6 +394,7 @@ module VagrantPlugins b2.use MatchMACAddress end end + b.use action_start end end diff --git a/plugins/providers/virtualbox/action/create_clone.rb b/plugins/providers/virtualbox/action/create_clone.rb index a76e23794..eae9eda5e 100644 --- a/plugins/providers/virtualbox/action/create_clone.rb +++ b/plugins/providers/virtualbox/action/create_clone.rb @@ -10,8 +10,6 @@ module VagrantPlugins end def call(env) - @logger.info("Creating linked clone from master '#{env[:master_id]}'") - # Get the snapshot to base the linked clone on. This defaults # to "base" which is automatically setup with linked clones. snapshot = "base" From 9f05d22eb063d3bedf0648a0447eaa690be63c34 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 12:03:58 -0400 Subject: [PATCH 105/484] providers/virtualbox: cloning can do a non-linked clone --- .../providers/virtualbox/action/create_clone.rb | 14 ++++++++------ plugins/providers/virtualbox/driver/version_4_3.rb | 14 ++++++++------ plugins/providers/virtualbox/driver/version_5_0.rb | 14 ++++++++------ templates/locales/en.yml | 2 +- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/plugins/providers/virtualbox/action/create_clone.rb b/plugins/providers/virtualbox/action/create_clone.rb index eae9eda5e..ac3ca519e 100644 --- a/plugins/providers/virtualbox/action/create_clone.rb +++ b/plugins/providers/virtualbox/action/create_clone.rb @@ -12,16 +12,18 @@ module VagrantPlugins def call(env) # Get the snapshot to base the linked clone on. This defaults # to "base" which is automatically setup with linked clones. - snapshot = "base" - if env[:machine].provider_config.linked_clone_snapshot - snapshot = env[:machine].provider_config.linked_clone_snapshot + snapshot = nil + if env[:machine].provider_config.linked_clone + snapshot = "base" + if env[:machine].provider_config.linked_clone_snapshot + snapshot = env[:machine].provider_config.linked_clone_snapshot + end end # Do the actual clone - env[:ui].info I18n.t( - "vagrant.actions.vm.clone.creating", name: env[:machine].box.name) + env[:ui].info I18n.t("vagrant.actions.vm.clone.creating") env[:machine].id = env[:machine].provider.driver.clonevm( - env[:master_id], env[:machine].box.name, snapshot) do |progress| + env[:master_id], snapshot) do |progress| env[:ui].clear_line env[:ui].report_progress(progress, 100, false) end diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index b51eec37a..f895739d3 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -35,13 +35,15 @@ module VagrantPlugins end end - def clonevm(master_id, box_name, snapshot_name) - @logger.debug("Creating linked clone from master vm with id #{master_id} from snapshot '#{snapshot_name}'") + def clonevm(master_id, snapshot_name) + machine_name = "temp_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" + args = ["--register", "--name", machine_name] + if snapshot_name + args += ["--snapshot", snapshot_name, "--options", "link"] + end - machine_name = "#{box_name}_#{snapshot_name}_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" - execute("clonevm", master_id, "--snapshot", snapshot_name, "--options", "link", "--register", "--name", machine_name) - - return get_machine_id machine_name + execute("clonevm", master_id, *args) + return get_machine_id(machine_name) end def create_dhcp_server(network, options) diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index 9762eda42..d13330c00 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -34,13 +34,15 @@ module VagrantPlugins end end - def clonevm(master_id, box_name, snapshot_name) - @logger.debug("Creating linked clone from master vm with id #{master_id} from snapshot '#{snapshot_name}'") + def clonevm(master_id, snapshot_name) + machine_name = "temp_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" + args = ["--register", "--name", machine_name] + if snapshot_name + args += ["--snapshot", snapshot_name, "--options", "link"] + end - machine_name = "#{box_name}_#{snapshot_name}_clone_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" - execute("clonevm", master_id, "--snapshot", snapshot_name, "--options", "link", "--register", "--name", machine_name) - - return get_machine_id machine_name + execute("clonevm", master_id, *args) + return get_machine_id(machine_name) end def create_dhcp_server(network, options) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index ff2f1fac1..d5fa23d2c 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1636,7 +1636,7 @@ en: This is a one time operation. Once the master VM is prepared, it will be used as a base for linked clones, making the creation of new VMs take milliseconds on a modern system. - creating: Creating linked clone... + creating: Cloning VM... failure: Creation of the linked clone failed. create_master: failure: |- From c5c3ba616bc8b8ef51d04286e3a760ab10ef5fac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 12:09:46 -0400 Subject: [PATCH 106/484] providers/virtualbox: some progress --- plugins/providers/virtualbox/action.rb | 9 +++++++- .../virtualbox/action/prepare_clone.rb | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 plugins/providers/virtualbox/action/prepare_clone.rb diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index a64782973..186f8debd 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -35,6 +35,7 @@ module VagrantPlugins autoload :NetworkFixIPv6, File.expand_path("../action/network_fix_ipv6", __FILE__) autoload :Package, File.expand_path("../action/package", __FILE__) autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__) + autoload :PrepareClone, File.expand_path("../action/prepare_clone", __FILE__) autoload :PrepareNFSSettings, File.expand_path("../action/prepare_nfs_settings", __FILE__) autoload :PrepareNFSValidIds, File.expand_path("../action/prepare_nfs_valid_ids", __FILE__) autoload :PrepareForwardedPortCollisionParams, File.expand_path("../action/prepare_forwarded_port_collision_params", __FILE__) @@ -384,10 +385,16 @@ module VagrantPlugins b2.use CheckAccessible b2.use Customize, "pre-import" - if env[:machine].provider_config.linked_clone + if env[:machine].config.vm.clone + # We are cloning from another Vagrant environment + b2.use PrepareClone + b2.use CreateClone + elsif env[:machine].provider_config.linked_clone + # We are cloning from the box b2.use ImportMaster b2.use CreateClone else + # We are just doing a normal import from a box b2.use Import end diff --git a/plugins/providers/virtualbox/action/prepare_clone.rb b/plugins/providers/virtualbox/action/prepare_clone.rb new file mode 100644 index 000000000..e3451cfef --- /dev/null +++ b/plugins/providers/virtualbox/action/prepare_clone.rb @@ -0,0 +1,21 @@ +require "log4r" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class PrepareClone + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::vm::prepare_clone") + end + + def call(env) + # We need to get the machine ID from this Vagrant environment + + # Continue + @app.call(env) + end + end + end + end +end From f0ddac8c9a2a6c89b44fbc8cdd931a5fad3ac89c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 12:33:55 -0400 Subject: [PATCH 107/484] providers/virtualbox: clone --- lib/vagrant/environment.rb | 22 +++++++ lib/vagrant/errors.rb | 12 +++- plugins/providers/virtualbox/action.rb | 3 + .../virtualbox/action/create_clone.rb | 12 +--- .../virtualbox/action/import_master.rb | 26 ++------ .../virtualbox/action/prepare_clone.rb | 11 ++++ .../action/prepare_clone_snapshot.rb | 65 +++++++++++++++++++ templates/locales/en.yml | 11 ++++ 8 files changed, 130 insertions(+), 32 deletions(-) create mode 100644 plugins/providers/virtualbox/action/prepare_clone_snapshot.rb diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index c3dabd676..0981d9df6 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -9,6 +9,7 @@ require 'log4r' require 'vagrant/util/file_mode' require 'vagrant/util/platform' +require "vagrant/util/silence_warnings" require "vagrant/vagrantfile" require "vagrant/version" @@ -413,6 +414,27 @@ module Vagrant @config_loader end + # Loads another environment for the given Vagrantfile, sharing as much + # useful state from this Environment as possible (such as UI and paths). + # Any initialization options can be overidden using the opts hash. + # + # @param [String] vagrantfile Path to a Vagrantfile + # @return [Environment] + def environment(vagrantfile, **opts) + path = File.expand_path(vagrantfile, root_path) + file = File.basename(path) + path = File.dirname(path) + + Util::SilenceWarnings.silence! do + Environment.new({ + cwd: path, + home_path: home_path, + ui_class: ui_class, + vagrantfile_name: file, + }.merge(opts)) + end + end + # This defines a hook point where plugin action hooks that are registered # against the given name will be run in the context of this environment. # diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 2ce133618..906b8d9bc 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -288,6 +288,14 @@ module Vagrant error_key(:cli_invalid_options) end + class CloneNotFound < VagrantError + error_key(:clone_not_found) + end + + class CloneMachineNotFound < VagrantError + error_key(:clone_machine_not_found) + end + class CommandUnavailable < VagrantError error_key(:command_unavailable) end @@ -787,11 +795,11 @@ module Vagrant class VMCloneFailure < VagrantError error_key(:failure, "vagrant.actions.vm.clone") end - + class VMCreateMasterFailure < VagrantError error_key(:failure, "vagrant.actions.vm.clone.create_master") end - + class VMCustomizationFailed < VagrantError error_key(:failure, "vagrant.actions.vm.customize") end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 186f8debd..9a67f0b37 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -36,6 +36,7 @@ module VagrantPlugins autoload :Package, File.expand_path("../action/package", __FILE__) autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__) autoload :PrepareClone, File.expand_path("../action/prepare_clone", __FILE__) + autoload :PrepareCloneSnapshot, File.expand_path("../action/prepare_clone_snapshot", __FILE__) autoload :PrepareNFSSettings, File.expand_path("../action/prepare_nfs_settings", __FILE__) autoload :PrepareNFSValidIds, File.expand_path("../action/prepare_nfs_valid_ids", __FILE__) autoload :PrepareForwardedPortCollisionParams, File.expand_path("../action/prepare_forwarded_port_collision_params", __FILE__) @@ -388,10 +389,12 @@ module VagrantPlugins if env[:machine].config.vm.clone # We are cloning from another Vagrant environment b2.use PrepareClone + b2.use PrepareCloneSnapshot b2.use CreateClone elsif env[:machine].provider_config.linked_clone # We are cloning from the box b2.use ImportMaster + b2.use PrepareCloneSnapshot b2.use CreateClone else # We are just doing a normal import from a box diff --git a/plugins/providers/virtualbox/action/create_clone.rb b/plugins/providers/virtualbox/action/create_clone.rb index ac3ca519e..526b5a5c1 100644 --- a/plugins/providers/virtualbox/action/create_clone.rb +++ b/plugins/providers/virtualbox/action/create_clone.rb @@ -10,20 +10,10 @@ module VagrantPlugins end def call(env) - # Get the snapshot to base the linked clone on. This defaults - # to "base" which is automatically setup with linked clones. - snapshot = nil - if env[:machine].provider_config.linked_clone - snapshot = "base" - if env[:machine].provider_config.linked_clone_snapshot - snapshot = env[:machine].provider_config.linked_clone_snapshot - end - end - # Do the actual clone env[:ui].info I18n.t("vagrant.actions.vm.clone.creating") env[:machine].id = env[:machine].provider.driver.clonevm( - env[:master_id], snapshot) do |progress| + env[:clone_id], env[:clone_snapshot]) do |progress| env[:ui].clear_line env[:ui].report_progress(progress, 100, false) end diff --git a/plugins/providers/virtualbox/action/import_master.rb b/plugins/providers/virtualbox/action/import_master.rb index 985a346ba..17296bbcc 100644 --- a/plugins/providers/virtualbox/action/import_master.rb +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -37,14 +37,14 @@ module VagrantPlugins master_id_file = env[:machine].box.directory.join("master_id") # Read the master ID if we have it in the file. - env[:master_id] = master_id_file.read.chomp if master_id_file.file? + env[:clone_id] = master_id_file.read.chomp if master_id_file.file? # If we have the ID and the VM exists already, then we # have nothing to do. Success! - if env[:master_id] && env[:machine].provider.driver.vm_exists?(env[:master_id]) + if env[:clone_id] && env[:machine].provider.driver.vm_exists?(env[:clone_id]) @logger.info( "Master VM for '#{env[:machine].box.name}' already exists " + - " (id=#{env[:master_id]}) - skipping import step.") + " (id=#{env[:clone_id]}) - skipping import step.") return end @@ -53,27 +53,15 @@ module VagrantPlugins # Import the virtual machine import_env = env[:action_runner].run(Import, env.dup.merge(skip_machine: true)) - env[:master_id] = import_env[:machine_id] + env[:clone_id] = import_env[:machine_id] @logger.info( "Imported box #{env[:machine].box.name} as master vm " + - "with id #{env[:master_id]}") + "with id #{env[:clone_id]}") - if !env[:machine].provider_config.linked_clone_snapshot - snapshots = env[:machine].provider.driver.list_snapshots(env[:master_id]) - if !snapshots.include?("base") - @logger.info("Creating base snapshot for master VM.") - env[:machine].provider.driver.create_snapshot( - env[:master_id], "base") do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - end - end - - @logger.debug("Writing id of master VM '#{env[:master_id]}' to #{master_id_file}") + @logger.debug("Writing id of master VM '#{env[:clone_id]}' to #{master_id_file}") master_id_file.open("w+") do |f| - f.write(env[:master_id]) + f.write(env[:clone_id]) end end end diff --git a/plugins/providers/virtualbox/action/prepare_clone.rb b/plugins/providers/virtualbox/action/prepare_clone.rb index e3451cfef..6b3bf34dd 100644 --- a/plugins/providers/virtualbox/action/prepare_clone.rb +++ b/plugins/providers/virtualbox/action/prepare_clone.rb @@ -11,6 +11,17 @@ module VagrantPlugins def call(env) # We need to get the machine ID from this Vagrant environment + clone_env = env[:machine].env.environment( + env[:machine].config.vm.clone) + raise Vagrant::Errors::CloneNotFound if !clone_env.root_path + + # Get the machine itself + clone_machine = clone_env.machine( + clone_env.primary_machine_name, env[:machine].provider_name) + raise Vagrant::Errors::CloneMachineNotFound if !clone_machine.id + + # Set the ID of the master so we know what to clone from + env[:clone_id] = clone_machine.id # Continue @app.call(env) diff --git a/plugins/providers/virtualbox/action/prepare_clone_snapshot.rb b/plugins/providers/virtualbox/action/prepare_clone_snapshot.rb new file mode 100644 index 000000000..08ce16a6d --- /dev/null +++ b/plugins/providers/virtualbox/action/prepare_clone_snapshot.rb @@ -0,0 +1,65 @@ +require "log4r" + +require "digest/md5" + +module VagrantPlugins + module ProviderVirtualBox + module Action + class PrepareCloneSnapshot + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::action::vm::prepare_clone") + end + + def call(env) + if !env[:clone_id] + @logger.info("no clone master, not preparing clone snapshot") + return @app.call(env) + end + + # If we're not doing a linked clone, snapshots don't matter + if !env[:machine].provider_config.linked_clone + return @app.call(env) + end + + # We lock so that we don't snapshot in parallel + lock_key = Digest::MD5.hexdigest("#{env[:clone_id]}-snapshot") + env[:machine].env.lock(lock_key, retry: true) do + prepare_snapshot(env) + end + + # Continue + @app.call(env) + end + + protected + + def prepare_snapshot(env) + name = env[:machine].provider_config.linked_clone_snapshot + name_set = !!name + name = "base" if !name + env[:clone_snapshot] = name + + # Get the snapshots. We're done if it already exists + snapshots = env[:machine].provider.driver.list_snapshots(env[:clone_id]) + if snapshots.include?(name) + @logger.info("clone snapshot already exists, doing nothing") + return + end + + # If they asked for a specific snapshot, it is an error + if name_set + # TODO: Error + end + + @logger.info("Creating base snapshot for master VM.") + env[:machine].provider.driver.create_snapshot( + env[:clone_id], name) do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + end + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index d5fa23d2c..9e8c680aa 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -650,6 +650,17 @@ en: available below. %{help} + clone_not_found: |- + The specified Vagrantfile to clone from was not found. Please verify + the `config.vm.clone` setting points to a valid Vagrantfile. + clone_machine_not_found: |- + The clone environment hasn't been created yet. To clone from + another Vagrantfile, it must already be created with `vagrant up`. + It doesn't need to be running. + + Additionally, the created environment must be started with a provider + matching this provider. For example, if you're using VirtualBox, + the clone environment must also be using VirtualBox. command_unavailable: |- The executable '%{file}' Vagrant is trying to run was not found in the PATH variable. This is an error. Please verify From dbcc936a713d2d7683dfbe2d691a758754fd8083 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 12:41:16 -0400 Subject: [PATCH 108/484] kernel/v2: box is optional if clone is set --- plugins/kernel_v2/config/vm.rb | 2 +- plugins/providers/virtualbox/action/match_mac_address.rb | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 3d87e2b87..3020611f4 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -559,7 +559,7 @@ module VagrantPlugins def validate(machine) errors = _detected_errors - if !box && !machine.provider_options[:box_optional] + if !box && !clone && !machine.provider_options[:box_optional] errors << I18n.t("vagrant.config.vm.box_missing") end diff --git a/plugins/providers/virtualbox/action/match_mac_address.rb b/plugins/providers/virtualbox/action/match_mac_address.rb index a66f40bad..10e998b2e 100644 --- a/plugins/providers/virtualbox/action/match_mac_address.rb +++ b/plugins/providers/virtualbox/action/match_mac_address.rb @@ -7,6 +7,9 @@ module VagrantPlugins end def call(env) + # If we cloned, we don't need a base mac, it is already set! + return @app.call(env) if env[:machine].config.vm.clone + raise Vagrant::Errors::VMBaseMacNotSpecified if !env[:machine].config.vm.base_mac # Create the proc which we want to use to modify the virtual machine From e9922d17546fb9b0c5f72cf72e4bfc6e0c8ab092 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 12:50:02 -0400 Subject: [PATCH 109/484] providers/virtualbox: discard state if cloning --- plugins/providers/virtualbox/action.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 9a67f0b37..e102625dc 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -391,11 +391,13 @@ module VagrantPlugins b2.use PrepareClone b2.use PrepareCloneSnapshot b2.use CreateClone + b2.use DiscardState elsif env[:machine].provider_config.linked_clone # We are cloning from the box b2.use ImportMaster b2.use PrepareCloneSnapshot b2.use CreateClone + b2.use DiscardState else # We are just doing a normal import from a box b2.use Import From 4908cd9cd956f1179b956787d3f4b7b043a7fe30 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 12:58:06 -0400 Subject: [PATCH 110/484] providers/virtualbox: copy SSH key --- plugins/providers/virtualbox/action.rb | 6 +++--- .../action/{create_clone.rb => clone.rb} | 14 +++++++++++++- .../providers/virtualbox/action/prepare_clone.rb | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) rename plugins/providers/virtualbox/action/{create_clone.rb => clone.rb} (78%) diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index e102625dc..f7984dca4 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -12,7 +12,7 @@ module VagrantPlugins autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__) autoload :ClearForwardedPorts, File.expand_path("../action/clear_forwarded_ports", __FILE__) autoload :ClearNetworkInterfaces, File.expand_path("../action/clear_network_interfaces", __FILE__) - autoload :CreateClone, File.expand_path("../action/create_clone", __FILE__) + autoload :Clone, File.expand_path("../action/clone", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) autoload :Customize, File.expand_path("../action/customize", __FILE__) autoload :Destroy, File.expand_path("../action/destroy", __FILE__) @@ -390,13 +390,13 @@ module VagrantPlugins # We are cloning from another Vagrant environment b2.use PrepareClone b2.use PrepareCloneSnapshot - b2.use CreateClone + b2.use Clone b2.use DiscardState elsif env[:machine].provider_config.linked_clone # We are cloning from the box b2.use ImportMaster b2.use PrepareCloneSnapshot - b2.use CreateClone + b2.use Clone b2.use DiscardState else # We are just doing a normal import from a box diff --git a/plugins/providers/virtualbox/action/create_clone.rb b/plugins/providers/virtualbox/action/clone.rb similarity index 78% rename from plugins/providers/virtualbox/action/create_clone.rb rename to plugins/providers/virtualbox/action/clone.rb index 526b5a5c1..6b389d4f1 100644 --- a/plugins/providers/virtualbox/action/create_clone.rb +++ b/plugins/providers/virtualbox/action/clone.rb @@ -1,9 +1,11 @@ require "log4r" +require "fileutils" + module VagrantPlugins module ProviderVirtualBox module Action - class CreateClone + class Clone def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::vm::clone") @@ -25,6 +27,16 @@ module VagrantPlugins # Flag as erroneous and return if clone failed raise Vagrant::Errors::VMCloneFailure if !env[:machine].id + # Copy the SSH key from the clone machine if we can + if env[:clone_machine] + key_path = env[:clone_machine].data_dir.join("private_key") + if key_path.file? + FileUtils.cp( + key_path, + env[:machine].data_dir.join("private_key")) + end + end + # Continue @app.call(env) end diff --git a/plugins/providers/virtualbox/action/prepare_clone.rb b/plugins/providers/virtualbox/action/prepare_clone.rb index 6b3bf34dd..c306b9b54 100644 --- a/plugins/providers/virtualbox/action/prepare_clone.rb +++ b/plugins/providers/virtualbox/action/prepare_clone.rb @@ -22,6 +22,7 @@ module VagrantPlugins # Set the ID of the master so we know what to clone from env[:clone_id] = clone_machine.id + env[:clone_machine] = clone_machine # Continue @app.call(env) From 5ea24e39d021875c2d6cc3830236364f42f78f9f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 13:07:05 -0400 Subject: [PATCH 111/484] providers/virtualbox: unify import/clone --- plugins/providers/virtualbox/action.rb | 13 +--- plugins/providers/virtualbox/action/clone.rb | 63 ------------------- plugins/providers/virtualbox/action/import.rb | 38 +++++++++++ 3 files changed, 41 insertions(+), 73 deletions(-) delete mode 100644 plugins/providers/virtualbox/action/clone.rb diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index f7984dca4..d0ef01bff 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -12,7 +12,6 @@ module VagrantPlugins autoload :CleanMachineFolder, File.expand_path("../action/clean_machine_folder", __FILE__) autoload :ClearForwardedPorts, File.expand_path("../action/clear_forwarded_ports", __FILE__) autoload :ClearNetworkInterfaces, File.expand_path("../action/clear_network_interfaces", __FILE__) - autoload :Clone, File.expand_path("../action/clone", __FILE__) autoload :Created, File.expand_path("../action/created", __FILE__) autoload :Customize, File.expand_path("../action/customize", __FILE__) autoload :Destroy, File.expand_path("../action/destroy", __FILE__) @@ -389,20 +388,14 @@ module VagrantPlugins if env[:machine].config.vm.clone # We are cloning from another Vagrant environment b2.use PrepareClone - b2.use PrepareCloneSnapshot - b2.use Clone - b2.use DiscardState elsif env[:machine].provider_config.linked_clone # We are cloning from the box b2.use ImportMaster - b2.use PrepareCloneSnapshot - b2.use Clone - b2.use DiscardState - else - # We are just doing a normal import from a box - b2.use Import end + b2.use PrepareCloneSnapshot + b2.use Import + b2.use DiscardState b2.use MatchMACAddress end end diff --git a/plugins/providers/virtualbox/action/clone.rb b/plugins/providers/virtualbox/action/clone.rb deleted file mode 100644 index 6b389d4f1..000000000 --- a/plugins/providers/virtualbox/action/clone.rb +++ /dev/null @@ -1,63 +0,0 @@ -require "log4r" - -require "fileutils" - -module VagrantPlugins - module ProviderVirtualBox - module Action - class Clone - def initialize(app, env) - @app = app - @logger = Log4r::Logger.new("vagrant::action::vm::clone") - end - - def call(env) - # Do the actual clone - env[:ui].info I18n.t("vagrant.actions.vm.clone.creating") - env[:machine].id = env[:machine].provider.driver.clonevm( - env[:clone_id], env[:clone_snapshot]) do |progress| - env[:ui].clear_line - env[:ui].report_progress(progress, 100, false) - end - - # Clear the line one last time since the progress meter doesn't - # disappear immediately. - env[:ui].clear_line - - # Flag as erroneous and return if clone failed - raise Vagrant::Errors::VMCloneFailure if !env[:machine].id - - # Copy the SSH key from the clone machine if we can - if env[:clone_machine] - key_path = env[:clone_machine].data_dir.join("private_key") - if key_path.file? - FileUtils.cp( - key_path, - env[:machine].data_dir.join("private_key")) - end - end - - # Continue - @app.call(env) - end - - def recover(env) - if env[:machine].state.id != :not_created - return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError) - - # If we're not supposed to destroy on error then just return - return if !env[:destroy_on_error] - - # Interrupted, destroy the VM. We note that we don't want to - # validate the configuration here, and we don't want to confirm - # we want to destroy. - destroy_env = env.clone - destroy_env[:config_validate] = false - destroy_env[:force_confirm_destroy] = true - env[:action_runner].run(Action.action_destroy, destroy_env) - end - end - end - end - end -end diff --git a/plugins/providers/virtualbox/action/import.rb b/plugins/providers/virtualbox/action/import.rb index 92e004f01..fb540f50a 100644 --- a/plugins/providers/virtualbox/action/import.rb +++ b/plugins/providers/virtualbox/action/import.rb @@ -7,6 +7,44 @@ module VagrantPlugins end def call(env) + if env[:clone_id] + clone(env) + else + import(env) + end + end + + def clone(env) + # Do the actual clone + env[:ui].info I18n.t("vagrant.actions.vm.clone.creating") + env[:machine].id = env[:machine].provider.driver.clonevm( + env[:clone_id], env[:clone_snapshot]) do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + # Clear the line one last time since the progress meter doesn't + # disappear immediately. + env[:ui].clear_line + + # Flag as erroneous and return if clone failed + raise Vagrant::Errors::VMCloneFailure if !env[:machine].id + + # Copy the SSH key from the clone machine if we can + if env[:clone_machine] + key_path = env[:clone_machine].data_dir.join("private_key") + if key_path.file? + FileUtils.cp( + key_path, + env[:machine].data_dir.join("private_key")) + end + end + + # Continue + @app.call(env) + end + + def import(env) env[:ui].info I18n.t("vagrant.actions.vm.import.importing", name: env[:machine].box.name) From 681b50060e01ee05457b5c64e3225c3dea2d1263 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 8 Oct 2015 13:52:21 -0400 Subject: [PATCH 112/484] Add shopt globs to include hidden files --- scripts/website_push_docs.sh | 4 +++- scripts/website_push_www.sh | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/website_push_docs.sh b/scripts/website_push_docs.sh index 0f7674252..99ee08705 100755 --- a/scripts/website_push_docs.sh +++ b/scripts/website_push_docs.sh @@ -16,7 +16,8 @@ while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" # Copy into tmpdir -cp -R $DIR/website/docs/ $DEPLOY/ +shopt -s dotglob +cp -R $DIR/website/docs/* $DEPLOY/ # Change into that directory cd $DEPLOY @@ -25,6 +26,7 @@ cd $DEPLOY touch .gitignore echo ".sass-cache" >> .gitignore echo "build" >> .gitignore +echo "vendor" >> .gitignore # Add everything git init . diff --git a/scripts/website_push_www.sh b/scripts/website_push_www.sh index ad74a7b63..f7f2c6ce4 100755 --- a/scripts/website_push_www.sh +++ b/scripts/website_push_www.sh @@ -16,7 +16,8 @@ while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" # Copy into tmpdir -cp -R $DIR/website/www/ $DEPLOY/ +shopt -s dotglob +cp -R $DIR/website/www/* $DEPLOY/ # Change into that directory cd $DEPLOY @@ -25,6 +26,7 @@ cd $DEPLOY touch .gitignore echo ".sass-cache" >> .gitignore echo "build" >> .gitignore +echo "vendor" >> .gitignore # Add everything git init . From 70e9079449333fbb11dd0a5cb7026d1986cc505b Mon Sep 17 00:00:00 2001 From: Jurnell Cockhren Date: Thu, 8 Oct 2015 14:00:38 -0500 Subject: [PATCH 113/484] Revert "Fix alignment of initializer" Refers to issues #6276, #5973 and #5936 This reverts commit 27d751863673435693b775bf44e79b9f688a8968. --- plugins/provisioners/salt/config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/provisioners/salt/config.rb b/plugins/provisioners/salt/config.rb index 9c564179e..084dc457c 100644 --- a/plugins/provisioners/salt/config.rb +++ b/plugins/provisioners/salt/config.rb @@ -95,7 +95,7 @@ module VagrantPlugins @install_args = nil if @install_args == UNSET_VALUE @install_master = nil if @install_master == UNSET_VALUE @install_syndic = nil if @install_syndic == UNSET_VALUE - @install_command = nil if @install_command == UNSET_VALUE + @install_command = nil if @install_command == UNSET_VALUE @no_minion = nil if @no_minion == UNSET_VALUE @bootstrap_options = nil if @bootstrap_options == UNSET_VALUE @config_dir = nil if @config_dir == UNSET_VALUE From fb611c7389ea068a9aed010fd7693ff7d9c87b25 Mon Sep 17 00:00:00 2001 From: Jurnell Cockhren Date: Thu, 8 Oct 2015 14:04:44 -0500 Subject: [PATCH 114/484] Revert "Initialize the install_command salt config var" Refers to issues #6276, #5973 and #5936 This reverts commit ccd735466524e5cdea353714e06d9a3c88d17bdc. --- plugins/provisioners/salt/config.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/provisioners/salt/config.rb b/plugins/provisioners/salt/config.rb index 084dc457c..4d9550406 100644 --- a/plugins/provisioners/salt/config.rb +++ b/plugins/provisioners/salt/config.rb @@ -33,7 +33,6 @@ module VagrantPlugins attr_accessor :install_args attr_accessor :install_master attr_accessor :install_syndic - attr_accessor :install_command attr_accessor :no_minion attr_accessor :bootstrap_options attr_accessor :version @@ -62,7 +61,6 @@ module VagrantPlugins @install_args = UNSET_VALUE @install_master = UNSET_VALUE @install_syndic = UNSET_VALUE - @install_command = UNSET_VALUE @no_minion = UNSET_VALUE @bootstrap_options = UNSET_VALUE @config_dir = UNSET_VALUE @@ -95,7 +93,6 @@ module VagrantPlugins @install_args = nil if @install_args == UNSET_VALUE @install_master = nil if @install_master == UNSET_VALUE @install_syndic = nil if @install_syndic == UNSET_VALUE - @install_command = nil if @install_command == UNSET_VALUE @no_minion = nil if @no_minion == UNSET_VALUE @bootstrap_options = nil if @bootstrap_options == UNSET_VALUE @config_dir = nil if @config_dir == UNSET_VALUE From 1a7c6dcfeb7db6e726c4918a0c271c7cfe8a99cb Mon Sep 17 00:00:00 2001 From: Jurnell Cockhren Date: Thu, 8 Oct 2015 14:51:40 -0500 Subject: [PATCH 115/484] Revert "Salt Provisioner: refactor custom install_type option to add install_command instead" Refers to issues #6276, #5973, #5936 and #5435 This reverts commit 72e63767ac5807277dfe7f2202d3ab25804fe0df. Conflicts: website/docs/source/v2/provisioning/salt.html.md --- plugins/provisioners/salt/provisioner.rb | 13 +++++-------- website/docs/source/v2/provisioning/salt.html.md | 7 +------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/plugins/provisioners/salt/provisioner.rb b/plugins/provisioners/salt/provisioner.rb index 72365776e..fb021d418 100644 --- a/plugins/provisioners/salt/provisioner.rb +++ b/plugins/provisioners/salt/provisioner.rb @@ -136,20 +136,17 @@ module VagrantPlugins end if @config.install_type - options = "%s %s" % [options, @config.install_type] + # Allow passing install_args as an arbitrary string rather + # than trying to format it based on known options + if @config.install_type != "custom" + options = "%s %s" % [options, @config.install_type] + end end if @config.install_args options = "%s %s" % [options, @config.install_args] end - if @config.install_command - # If this is defined, we will ignore both install_type and - # install_args and use this instead. Every necessary command option - # will need to be specified by the user. - options = @config.install_command - end - if @config.verbose @machine.env.ui.info "Using Bootstrap Options: %s" % options end diff --git a/website/docs/source/v2/provisioning/salt.html.md b/website/docs/source/v2/provisioning/salt.html.md index 938fec155..aab48b7bd 100644 --- a/website/docs/source/v2/provisioning/salt.html.md +++ b/website/docs/source/v2/provisioning/salt.html.md @@ -54,19 +54,14 @@ on this machine. Not supported on Windows guest machines. * `install_syndic` (boolean) - Install the salt-syndic, default `false`. Not supported on Windows guest machines. -* `install_type` (stable | git | daily | testing) - Whether to install from a +* `install_type` (stable | git | daily | testing | custom) - Whether to install from a distribution's stable package manager, git tree-ish, daily ppa, or testing repository. -Not supported on Windows guest machines. * `install_args` (develop) - When performing a git install, you can specify a branch, tag, or any treeish. If using the `custom` install type, you can also specify a different repository to install from. Not supported on Windows guest machines. -* `install_command` (string) - Allow specifying an arbitrary string of arguments -to the bootstrap script. This will completely ignore `install_type` and `install_args` -to allow more flexibility with the bootstrap process. - * `always_install` (boolean) - Installs salt binaries even if they are already detected, default `false` From 86e56aeac334c4d1d1ce9e20d1935a05af81549a Mon Sep 17 00:00:00 2001 From: Jurnell Cockhren Date: Thu, 8 Oct 2015 14:56:48 -0500 Subject: [PATCH 116/484] Revert "Salt Provisioner: Added a 'custom' option to install_type to allow more flexibility in passing arguments to the bootstrap script. Updated the docs." This reverts commit 0289ab986c053f7d3bc825142c82a07bcf0ae3ec. Refers to issues #6276, #5973, #5936 and #5435 Conflicts: website/docs/source/v2/provisioning/salt.html.md --- plugins/provisioners/salt/provisioner.rb | 6 +----- website/docs/source/v2/provisioning/salt.html.md | 7 ++----- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/plugins/provisioners/salt/provisioner.rb b/plugins/provisioners/salt/provisioner.rb index fb021d418..f7b4406e9 100644 --- a/plugins/provisioners/salt/provisioner.rb +++ b/plugins/provisioners/salt/provisioner.rb @@ -136,11 +136,7 @@ module VagrantPlugins end if @config.install_type - # Allow passing install_args as an arbitrary string rather - # than trying to format it based on known options - if @config.install_type != "custom" - options = "%s %s" % [options, @config.install_type] - end + options = "%s %s" % [options, @config.install_type] end if @config.install_args diff --git a/website/docs/source/v2/provisioning/salt.html.md b/website/docs/source/v2/provisioning/salt.html.md index aab48b7bd..d4d13bf61 100644 --- a/website/docs/source/v2/provisioning/salt.html.md +++ b/website/docs/source/v2/provisioning/salt.html.md @@ -54,13 +54,10 @@ on this machine. Not supported on Windows guest machines. * `install_syndic` (boolean) - Install the salt-syndic, default `false`. Not supported on Windows guest machines. -* `install_type` (stable | git | daily | testing | custom) - Whether to install from a +* `install_type` (stable | git | daily | testing) - Whether to install from a distribution's stable package manager, git tree-ish, daily ppa, or testing repository. -* `install_args` (develop) - When performing a git install, -you can specify a branch, tag, or any treeish. If using the `custom` install type, -you can also specify a different repository to install from. -Not supported on Windows guest machines. +* `install_args` (develop) - When performing a git install, you can specify a branch, tag, or any treeish. Not supported on Windows. * `always_install` (boolean) - Installs salt binaries even if they are already detected, default `false` From 36cfc77167f0beb460e592908d6837e5f2b3d877 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Oct 2015 16:02:37 -0400 Subject: [PATCH 117/484] providers/virtualbox: make prepare clone a core thing --- lib/vagrant/action.rb | 1 + .../vagrant/action/builtin}/prepare_clone.rb | 15 ++++++++++----- plugins/providers/virtualbox/action.rb | 7 ++----- 3 files changed, 13 insertions(+), 10 deletions(-) rename {plugins/providers/virtualbox/action => lib/vagrant/action/builtin}/prepare_clone.rb (70%) diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index db4875dbb..91b7959c1 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -24,6 +24,7 @@ module Vagrant autoload :IsState, "vagrant/action/builtin/is_state" autoload :Lock, "vagrant/action/builtin/lock" autoload :Message, "vagrant/action/builtin/message" + autoload :PrepareClone, "vagrant/action/builtin/prepare_clone" autoload :Provision, "vagrant/action/builtin/provision" autoload :ProvisionerCleanup, "vagrant/action/builtin/provisioner_cleanup" autoload :SetHostname, "vagrant/action/builtin/set_hostname" diff --git a/plugins/providers/virtualbox/action/prepare_clone.rb b/lib/vagrant/action/builtin/prepare_clone.rb similarity index 70% rename from plugins/providers/virtualbox/action/prepare_clone.rb rename to lib/vagrant/action/builtin/prepare_clone.rb index c306b9b54..696d28bfb 100644 --- a/plugins/providers/virtualbox/action/prepare_clone.rb +++ b/lib/vagrant/action/builtin/prepare_clone.rb @@ -1,8 +1,8 @@ require "log4r" -module VagrantPlugins - module ProviderVirtualBox - module Action +module Vagrant + module Action + module Builtin class PrepareClone def initialize(app, env) @app = app @@ -10,15 +10,20 @@ module VagrantPlugins end def call(env) + # If we aren't cloning, then do nothing + if !env[:machine].config.vm.clone + return @app.call(env) + end + # We need to get the machine ID from this Vagrant environment clone_env = env[:machine].env.environment( env[:machine].config.vm.clone) - raise Vagrant::Errors::CloneNotFound if !clone_env.root_path + raise Errors::CloneNotFound if !clone_env.root_path # Get the machine itself clone_machine = clone_env.machine( clone_env.primary_machine_name, env[:machine].provider_name) - raise Vagrant::Errors::CloneMachineNotFound if !clone_machine.id + raise Errors::CloneMachineNotFound if !clone_machine.id # Set the ID of the master so we know what to clone from env[:clone_id] = clone_machine.id diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index d0ef01bff..85f52f887 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -34,7 +34,6 @@ module VagrantPlugins autoload :NetworkFixIPv6, File.expand_path("../action/network_fix_ipv6", __FILE__) autoload :Package, File.expand_path("../action/package", __FILE__) autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __FILE__) - autoload :PrepareClone, File.expand_path("../action/prepare_clone", __FILE__) autoload :PrepareCloneSnapshot, File.expand_path("../action/prepare_clone_snapshot", __FILE__) autoload :PrepareNFSSettings, File.expand_path("../action/prepare_nfs_settings", __FILE__) autoload :PrepareNFSValidIds, File.expand_path("../action/prepare_nfs_valid_ids", __FILE__) @@ -385,14 +384,12 @@ module VagrantPlugins b2.use CheckAccessible b2.use Customize, "pre-import" - if env[:machine].config.vm.clone - # We are cloning from another Vagrant environment - b2.use PrepareClone - elsif env[:machine].provider_config.linked_clone + if env[:machine].provider_config.linked_clone # We are cloning from the box b2.use ImportMaster end + b2.use PrepareClone b2.use PrepareCloneSnapshot b2.use Import b2.use DiscardState From 1b19ce1cc3c5ce454e9ecd99fd542abe7d0ddb5c Mon Sep 17 00:00:00 2001 From: Pat O'Shea Date: Thu, 8 Oct 2015 23:56:34 -0600 Subject: [PATCH 118/484] Add windows minion requirement for ipc_mode If this is not set to tpc, then the minion will fail to communicate with the master. --- website/docs/source/v2/provisioning/salt.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/source/v2/provisioning/salt.html.md b/website/docs/source/v2/provisioning/salt.html.md index 938fec155..5d41b302f 100644 --- a/website/docs/source/v2/provisioning/salt.html.md +++ b/website/docs/source/v2/provisioning/salt.html.md @@ -90,7 +90,7 @@ a custom salt minion config file. * `minion_pub` (salt/key/minion.pub) - Path to your minion public key -* `grains_config` (string) - Path to a custom salt grains file. +* `grains_config` (string) - Path to a custom salt grains file. On Windows, the minion needs `ipc_mode: tcp` set otherwise it will [fail to communicate](https://github.com/saltstack/salt/issues/22796) with the master. * `masterless` (boolean) - Calls state.highstate in local mode. Uses `minion_id` and `pillar_data` when provided. From e426455309597857d687678e0fc8dbd044e7fa7b Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Fri, 9 Oct 2015 14:54:10 +0300 Subject: [PATCH 119/484] guests/darwin: Configure network following the MAC addresses matching Currently `configure_networks` guest cap configures NICs following the device order and fails when the device order is mixed. We should detect the appropriate NIC by its MAC address. --- .../guests/darwin/cap/configure_networks.rb | 108 ++++++++++++++---- 1 file changed, 86 insertions(+), 22 deletions(-) diff --git a/plugins/guests/darwin/cap/configure_networks.rb b/plugins/guests/darwin/cap/configure_networks.rb index fefd0a2e2..1f43acd6a 100644 --- a/plugins/guests/darwin/cap/configure_networks.rb +++ b/plugins/guests/darwin/cap/configure_networks.rb @@ -6,45 +6,109 @@ module VagrantPlugins module GuestDarwin module Cap class ConfigureNetworks + @@logger = Log4r::Logger.new("vagrant::guest::darwin::configure_networks") + include Vagrant::Util def self.configure_networks(machine, networks) - # Slightly different than other plugins, using the template to build commands - # rather than templating the files. + if !machine.provider.capability?(:nic_mac_addresses) + raise Errors::CantReadMACAddresses, + provider: machine.provider_name.to_s + end - machine.communicate.sudo("networksetup -detectnewhardware") - machine.communicate.sudo("networksetup -listnetworkserviceorder > /tmp/vagrant.interfaces") - tmpints = File.join(Dir.tmpdir, File.basename("#{machine.id}.interfaces")) - machine.communicate.download("/tmp/vagrant.interfaces",tmpints) + nic_mac_addresses = machine.provider.capability(:nic_mac_addresses) + @@logger.debug("mac addresses: #{nic_mac_addresses.inspect}") - devlist = [] - ints = ::IO.read(tmpints) + mac_service_map = create_mac_service_map(machine) + + networks.each do |network| + mac_address = nic_mac_addresses[network[:interface]+1] + if mac_address.nil? + @@logger.warn("Could not find mac address for network #{network.inspect}") + next + end + + service_name = mac_service_map[mac_address] + if service_name.nil? + @@logger.warn("Could not find network service for mac address #{mac_address}") + next + end + + network_type = network[:type].to_sym + if network_type == :static + command = "networksetup -setmanual \"#{service_name}\" #{network[:ip]} #{network[:netmask]}" + elsif network_type == :dhcp + command = "networksetup -setdhcp \"#{service_name}\"" + else + raise "#{network_type} network type is not supported, try static or dhcp" + end + + machine.communicate.sudo(command) + end + end + + # Creates a hash mapping MAC addresses to network service name + # Example: { "00C100A1B2C3" => "Thunderbolt Ethernet" } + def self.create_mac_service_map(machine) + tmp_ints = File.join(Dir.tmpdir, File.basename("#{machine.id}.interfaces")) + tmp_hw = File.join(Dir.tmpdir, File.basename("#{machine.id}.hardware")) + + machine.communicate.tap do |comm| + comm.sudo("networksetup -detectnewhardware") + comm.sudo("networksetup -listnetworkserviceorder > /tmp/vagrant.interfaces") + comm.sudo("networksetup -listallhardwareports > /tmp/vagrant.hardware") + comm.download("/tmp/vagrant.interfaces", tmp_ints) + comm.download("/tmp/vagrant.hardware", tmp_hw) + end + + interface_map = {} + ints = ::IO.read(tmp_ints) ints.split(/\n\n/m).each do |i| - if i.match(/Hardware/) and not i.match(/Ethernet/).nil? - devmap = {} + if i.match(/Hardware/) && i.match(/Ethernet/) # Ethernet, should be 2 lines, # (3) Thunderbolt Ethernet # (Hardware Port: Thunderbolt Ethernet, Device: en1) # multiline, should match "Thunderbolt Ethernet", "en1" devicearry = i.match(/\([0-9]+\) (.+)\n.*Device: (.+)\)/m) - devmap[:interface] = devicearry[2] - devmap[:service] = devicearry[1] - devlist << devmap + service = devicearry[1] + interface = devicearry[2] + + # Should map interface to service { "en1" => "Thunderbolt Ethernet" } + interface_map[interface] = service end end - File.delete(tmpints) + File.delete(tmp_ints) - networks.each do |network| - service_name = devlist[network[:interface]][:service] - if network[:type].to_sym == :static - command = "networksetup -setmanual \"#{service_name}\" #{network[:ip]} #{network[:netmask]}" - elsif network[:type].to_sym == :dhcp - command = "networksetup -setdhcp \"#{service_name}\"" + mac_service_map = {} + macs = ::IO.read(tmp_hw) + macs.split(/\n\n/m).each do |i| + if i.match(/Hardware/) && i.match(/Ethernet/) + # Ethernet, should be 3 lines, + # Hardware Port: Thunderbolt 1 + # Device: en1 + # Ethernet Address: a1:b2:c3:d4:e5:f6 + + # multiline, should match "en1", "00:c1:00:a1:b2:c3" + devicearry = i.match(/Device: (.+)\nEthernet Address: (.+)/m) + interface = devicearry[1] + naked_mac = devicearry[2].gsub(':','').upcase + + # Skip hardware ports without MAC (bridges, bluetooth, etc.) + next if naked_mac == "N/A" + + if !interface_map[interface] + @@logger.warn("Could not find network service for interface #{interface}") + next + end + + # Should map MAC to service, { "00C100A1B2C3" => "Thunderbolt Ethernet" } + mac_service_map[naked_mac] = interface_map[interface] end - - machine.communicate.sudo(command) end + File.delete(tmp_hw) + + mac_service_map end end end From f930fa94af0b339b6f4498844a0ba429703074e7 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Fri, 9 Oct 2015 14:56:18 +0300 Subject: [PATCH 120/484] Move "cant_read_mac_addresses" error to the global space Now it is used not only by Windows, but by Darwin guests as well. --- lib/vagrant/errors.rb | 4 ++++ plugins/guests/darwin/cap/configure_networks.rb | 2 +- plugins/guests/windows/cap/configure_networks.rb | 2 +- plugins/guests/windows/errors.rb | 4 ---- templates/locales/en.yml | 8 ++++++++ templates/locales/guest_windows.yml | 8 -------- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 906b8d9bc..82f8a5e24 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -248,6 +248,10 @@ module Vagrant error_key(:bundler_error) end + class CantReadMACAddresses < VagrantError + error_key(:cant_read_mac_addresses) + end + class CapabilityHostExplicitNotDetected < VagrantError error_key(:capability_host_explicit_not_detected) end diff --git a/plugins/guests/darwin/cap/configure_networks.rb b/plugins/guests/darwin/cap/configure_networks.rb index 1f43acd6a..1be71c82f 100644 --- a/plugins/guests/darwin/cap/configure_networks.rb +++ b/plugins/guests/darwin/cap/configure_networks.rb @@ -12,7 +12,7 @@ module VagrantPlugins def self.configure_networks(machine, networks) if !machine.provider.capability?(:nic_mac_addresses) - raise Errors::CantReadMACAddresses, + raise Vagrant::Errors::CantReadMACAddresses, provider: machine.provider_name.to_s end diff --git a/plugins/guests/windows/cap/configure_networks.rb b/plugins/guests/windows/cap/configure_networks.rb index 8e766a319..a4f4685d9 100644 --- a/plugins/guests/windows/cap/configure_networks.rb +++ b/plugins/guests/windows/cap/configure_networks.rb @@ -53,7 +53,7 @@ module VagrantPlugins def self.create_vm_interface_map(machine, guest_network) if !machine.provider.capability?(:nic_mac_addresses) - raise Errors::CantReadMACAddresses, + raise Vagrant::Errors::CantReadMACAddresses, provider: machine.provider_name.to_s end diff --git a/plugins/guests/windows/errors.rb b/plugins/guests/windows/errors.rb index d5be14ce6..e76753646 100644 --- a/plugins/guests/windows/errors.rb +++ b/plugins/guests/windows/errors.rb @@ -6,10 +6,6 @@ module VagrantPlugins error_namespace("vagrant_windows.errors") end - class CantReadMACAddresses < WindowsError - error_key(:cant_read_mac_addresses) - end - class NetworkWinRMRequired < WindowsError error_key(:network_winrm_required) end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 9e8c680aa..94f47371d 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -606,6 +606,14 @@ en: issues. The error from Bundler is: %{message} + cant_read_mac_addresses: |- + The provider you are using ('%{provider}') doesn't support the + "nic_mac_addresses" provider capability which is required + for advanced networking to work with this guest OS. Please inform + the author of the provider to add this feature. + + Until then, you must remove any networking configurations other + than forwarded ports from your Vagrantfile for Vagrant to continue. capability_host_explicit_not_detected: |- The explicit capability host specified of '%{value}' could not be found. diff --git a/templates/locales/guest_windows.yml b/templates/locales/guest_windows.yml index a648f5a09..f9a3bf997 100644 --- a/templates/locales/guest_windows.yml +++ b/templates/locales/guest_windows.yml @@ -1,14 +1,6 @@ en: vagrant_windows: errors: - cant_read_mac_addresses: |- - The provider being used to start Windows ('%{provider}') - doesn't support the "nic_mac_addresses" capability which is required - for advanced networking to work with Windows guests. Please inform - the author of the provider to add this feature. - - Until then, you must remove any networking configurations other - than forwarded ports from your Vagrantfile for Vagrant to continue. network_winrm_required: |- Configuring networks on Windows requires the communicator to be set to WinRM. To do this, add the following to your Vagrantfile: From 99985449953d7c21fc2c4f6a4bb12f5de0f0128d Mon Sep 17 00:00:00 2001 From: Johannes Graf Date: Sat, 10 Oct 2015 20:48:31 +0200 Subject: [PATCH 121/484] Fix for #6151 / provisioner puppet_server with Puppet Collection 1 puppet_server provisioner fails with Puppet Collection 1 with the following error: ```bash ==> default: Running provisioner: puppet_server... The `puppet` binary appears not to be in the PATH of the guest. This could be because the PATH is not properly setup or perhaps Puppet is not installed on this guest. Puppet provisioning can not continue without Puppet properly installed. ``` --- .../provisioners/puppet/config/puppet_server.rb | 6 ++++++ .../puppet/provisioner/puppet_server.rb | 15 +++++++++++++-- .../source/v2/provisioning/puppet_agent.html.md | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/plugins/provisioners/puppet/config/puppet_server.rb b/plugins/provisioners/puppet/config/puppet_server.rb index 9c53672d5..5cab589b6 100644 --- a/plugins/provisioners/puppet/config/puppet_server.rb +++ b/plugins/provisioners/puppet/config/puppet_server.rb @@ -2,6 +2,10 @@ module VagrantPlugins module Puppet module Config class PuppetServer < Vagrant.plugin("2", :config) + # The path to Puppet's bin/ directory. + # @return [String] + attr_accessor :binary_path + attr_accessor :client_cert_path attr_accessor :client_private_key_path attr_accessor :facter @@ -12,6 +16,7 @@ module VagrantPlugins def initialize super + @binary_path = UNSET_VALUE @client_cert_path = UNSET_VALUE @client_private_key_path = UNSET_VALUE @facter = {} @@ -29,6 +34,7 @@ module VagrantPlugins def finalize! super + @binary_path = nil if @binary_path == UNSET_VALUE @client_cert_path = nil if @client_cert_path == UNSET_VALUE @client_private_key_path = nil if @client_private_key_path == UNSET_VALUE @puppet_node = nil if @puppet_node == UNSET_VALUE diff --git a/plugins/provisioners/puppet/provisioner/puppet_server.rb b/plugins/provisioners/puppet/provisioner/puppet_server.rb index 9c9e9e6b7..29c1c23dc 100644 --- a/plugins/provisioners/puppet/provisioner/puppet_server.rb +++ b/plugins/provisioners/puppet/provisioner/puppet_server.rb @@ -17,8 +17,14 @@ module VagrantPlugins end def verify_binary(binary) + if @config.binary_path + test_cmd = "test -x #{@config.binary_path}/#{binary}" + else + test_cmd = "which #{binary}" + end + @machine.communicate.sudo( - "which #{binary}", + test_cmd, error_class: PuppetServerError, error_key: :not_detected, binary: binary) @@ -83,8 +89,13 @@ module VagrantPlugins facter = "#{facts.join(" ")} " end + + puppet_bin = "puppet" + if @config.binary_path + puppet_bin = File.join(@config.binary_path, puppet_bin) + end options = options.join(" ") - command = "#{facter}puppet agent --onetime --no-daemonize #{options} " + + command = "#{facter} #{puppet_bin} agent --onetime --no-daemonize #{options} " + "--server #{config.puppet_server} --detailed-exitcodes || [ $? -eq 2 ]" @machine.ui.info I18n.t("vagrant.provisioners.puppet_server.running_puppetd") diff --git a/website/docs/source/v2/provisioning/puppet_agent.html.md b/website/docs/source/v2/provisioning/puppet_agent.html.md index 14d2b8a7c..26f43cf2a 100644 --- a/website/docs/source/v2/provisioning/puppet_agent.html.md +++ b/website/docs/source/v2/provisioning/puppet_agent.html.md @@ -26,6 +26,8 @@ the set of modules and manifests from there. The `puppet_server` provisioner takes various options. None are strictly required. They are listed below: +* `binary_path` (string) - Path on the guest to Puppet's `bin/` directory. + * `client_cert_path` (string) - Path to the client certificate for the node on your disk. This defaults to nothing, in which case a client cert won't be uploaded. From 07a42daf9018e0d84bb2be52224289e25d32bd73 Mon Sep 17 00:00:00 2001 From: Gemma Peter Date: Sun, 11 Oct 2015 16:11:33 +0100 Subject: [PATCH 122/484] Fix typo --- website/docs/source/v2/provisioning/shell.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/source/v2/provisioning/shell.html.md b/website/docs/source/v2/provisioning/shell.html.md index 978d8cbda..ef4119668 100644 --- a/website/docs/source/v2/provisioning/shell.html.md +++ b/website/docs/source/v2/provisioning/shell.html.md @@ -136,7 +136,7 @@ end If you're running a Batch of PowerShell script for Windows, make sure that the external path has the proper extension (".bat" or ".ps1"), because -Windows uses this to determine what kind fo file it is to execute. If you +Windows uses this to determine what kind of file it is to execute. If you exclude this extension, it likely won't work. To run a script already available on the guest you can use an inline script to From 29e60882ca87a006c29d0e3bb4f37746f75f8477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadej=20Jane=C5=BE?= Date: Sun, 11 Oct 2015 23:09:10 +0200 Subject: [PATCH 123/484] Fixes Fedora network issues when biosdevname command is not present. Previously, configuring and enabling network interfaces failed with: "The following SSH command responded with a non-zero exit status. Vagrant assumes that this means the command failed! /usr/sbin/biosdevname --policy=all_ethN -i bash: /usr/sbin/biosdevname: No such file or directory Stdout from the command: bash: /usr/sbin/biosdevname: No such file or directory" The previous attempt to fix this (ccc4162) doesn't work since it doesn't properly parse the 'bash: /usr/sbin/biosdevname: No such file or directory' error message. This patch works around that problem and adds a comment explaining the meaning of the return codes. --- plugins/guests/fedora/cap/configure_networks.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/guests/fedora/cap/configure_networks.rb b/plugins/guests/fedora/cap/configure_networks.rb index 963996edc..364b744e4 100644 --- a/plugins/guests/fedora/cap/configure_networks.rb +++ b/plugins/guests/fedora/cap/configure_networks.rb @@ -17,7 +17,10 @@ module VagrantPlugins virtual = false interface_names = Array.new interface_names_by_slot = Array.new - machine.communicate.sudo("/usr/sbin/biosdevname; echo $?") do |_, result| + machine.communicate.sudo("/usr/sbin/biosdevname &>/dev/null; echo $?") do |_, result| + # The above command returns: + # - '4' if /usr/sbin/biosdevname detects it is running in a virtual machine + # - '127' if /usr/sbin/biosdevname doesn't exist virtual = true if ['4', '127'].include? result.chomp end From 8ee97d5f81797f7bab973655e282bed080d525ea Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 12 Oct 2015 13:16:19 -0400 Subject: [PATCH 124/484] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c794b3ce0..aa52318c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ BUG FIXES: - communicators/ssh: use the same SSH args for `vagrant ssh` with and without a command [GH-4986, GH-5928] - guests/fedora: networks can be configured without nmcli [GH-5931] + - guests/fedora: biosdevname can return 4 or 127 [GH-6139] - guests/redhat: systemd detection should happen on guest [GH-5948] - guests/ubuntu: setting hostname fixed in 12.04 [GH-5937] - hosts/linux: NFS can be configured without `$TMP` set on the host [GH-5954] From 8c8a70798dc6d725afe509a82425e76d8bbe4870 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 12 Oct 2015 15:50:47 -0400 Subject: [PATCH 125/484] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa52318c1..f78eac8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ IMPROVEMENTS: BUG FIXES: + - communicator/winrm: respect `boot_timeout` setting [GH-6229] - provisioners/ansible: use quotes for the `ansible_ssh_private_key_file` value in the generated inventory [GH-6209] From 8e879905996eb6e2f9bb75d0d16b892f2f405a12 Mon Sep 17 00:00:00 2001 From: Marc Siegfriedt Date: Fri, 21 Aug 2015 13:37:10 -0700 Subject: [PATCH 126/484] add the option to make elevated interactive scripts --- plugins/communicators/winrm/communicator.rb | 7 ++- .../winrm/scripts/elevated_shell.ps1.erb | 8 ++-- plugins/communicators/winrm/shell.rb | 1 + plugins/provisioners/shell/config.rb | 43 +++++++++++-------- plugins/provisioners/shell/provisioner.rb | 2 +- templates/locales/en.yml | 1 + .../docs/source/v2/provisioning/shell.html.md | 10 ++++- 7 files changed, 45 insertions(+), 27 deletions(-) mode change 100644 => 100755 plugins/communicators/winrm/communicator.rb mode change 100644 => 100755 plugins/communicators/winrm/scripts/elevated_shell.ps1.erb mode change 100644 => 100755 plugins/communicators/winrm/shell.rb mode change 100644 => 100755 plugins/provisioners/shell/config.rb mode change 100644 => 100755 plugins/provisioners/shell/provisioner.rb mode change 100644 => 100755 templates/locales/en.yml mode change 100644 => 100755 website/docs/source/v2/provisioning/shell.html.md diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb old mode 100644 new mode 100755 index b14b28efa..d4d14e064 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -136,10 +136,11 @@ module VagrantPlugins error_key: nil, # use the error_class message key good_exit: 0, shell: :powershell, + interactive: false, }.merge(opts || {}) opts[:good_exit] = Array(opts[:good_exit]) - command = wrap_in_scheduled_task(command) if opts[:elevated] + command = wrap_in_scheduled_task(command, opts[:interactive]) if opts[:elevated] output = shell.send(opts[:shell], command, &block) execution_output(output, opts) end @@ -195,7 +196,9 @@ module VagrantPlugins # @return The wrapper command to execute def wrap_in_scheduled_task(command) path = File.expand_path("../scripts/elevated_shell.ps1", __FILE__) - script = Vagrant::Util::TemplateRenderer.render(path) + script = Vagrant::Util::TemplateRenderer.render(path, options: { + interactive: interactive, + }) guest_script_path = "c:/tmp/vagrant-elevated-shell.ps1" file = Tempfile.new(["vagrant-elevated-shell", "ps1"]) begin diff --git a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb old mode 100644 new mode 100755 index 17767e436..778dcb0cd --- a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb +++ b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb @@ -12,8 +12,8 @@ $task_xml = @' - {username} - Password + {user} + <%= options[:interactive] ? 'InteractiveTokenOrPassword' : 'Password' %> HighestAvailable @@ -55,7 +55,7 @@ $schedule.Connect() $task = $schedule.NewTask($null) $task.XmlText = $task_xml $folder = $schedule.GetFolder("\") -$folder.RegisterTaskDefinition($task_name, $task, 6, $username, $password, 1, $null) | Out-Null +$folder.RegisterTaskDefinition($task_name, $task, 6, $user, $password, <%= options[:interactive] ? 3 : 1 %>, $null) | Out-Null $registered_task = $folder.GetTask("\$task_name") $registered_task.Run($null) | Out-Null @@ -71,7 +71,7 @@ function SlurpOutput($out_file, $cur_line) { if (Test-Path $out_file) { get-content $out_file | select -skip $cur_line | ForEach { $cur_line += 1 - Write-Host "$_" + Write-Host "$_" } } return $cur_line diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb old mode 100644 new mode 100755 index 9b0f4e302..1d277829a --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -23,6 +23,7 @@ module VagrantPlugins HTTPClient::KeepAliveDisconnected, WinRM::WinRMHTTPTransportError, WinRM::WinRMAuthorizationError, + WinRM::WinRMWSManFault, Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, diff --git a/plugins/provisioners/shell/config.rb b/plugins/provisioners/shell/config.rb old mode 100644 new mode 100755 index 2f4957214..19d5180a9 --- a/plugins/provisioners/shell/config.rb +++ b/plugins/provisioners/shell/config.rb @@ -12,29 +12,32 @@ module VagrantPlugins attr_accessor :keep_color attr_accessor :name attr_accessor :powershell_args + attr_accessor :elevated_interactive def initialize - @args = UNSET_VALUE - @inline = UNSET_VALUE - @path = UNSET_VALUE - @upload_path = UNSET_VALUE - @privileged = UNSET_VALUE - @binary = UNSET_VALUE - @keep_color = UNSET_VALUE - @name = UNSET_VALUE - @powershell_args = UNSET_VALUE + @args = UNSET_VALUE + @inline = UNSET_VALUE + @path = UNSET_VALUE + @upload_path = UNSET_VALUE + @privileged = UNSET_VALUE + @binary = UNSET_VALUE + @keep_color = UNSET_VALUE + @name = UNSET_VALUE + @powershell_args = UNSET_VALUE + @elevated_interactive = UNSET_VALUE end def finalize! - @args = nil if @args == UNSET_VALUE - @inline = nil if @inline == UNSET_VALUE - @path = nil if @path == UNSET_VALUE - @upload_path = "/tmp/vagrant-shell" if @upload_path == UNSET_VALUE - @privileged = true if @privileged == UNSET_VALUE - @binary = false if @binary == UNSET_VALUE - @keep_color = false if @keep_color == UNSET_VALUE - @name = nil if @name == UNSET_VALUE - @powershell_args = "-ExecutionPolicy Bypass" if @powershell_args == UNSET_VALUE + @args = nil if @args == UNSET_VALUE + @inline = nil if @inline == UNSET_VALUE + @path = nil if @path == UNSET_VALUE + @upload_path = "/tmp/vagrant-shell" if @upload_path == UNSET_VALUE + @privileged = true if @privileged == UNSET_VALUE + @binary = false if @binary == UNSET_VALUE + @keep_color = false if @keep_color == UNSET_VALUE + @name = nil if @name == UNSET_VALUE + @powershell_args = "-ExecutionPolicy Bypass" if @powershell_args == UNSET_VALUE + @elevated_interactive = false if @elevated_interactive == UNSET_VALUE if @args && args_valid? @args = @args.is_a?(Array) ? @args.map { |a| a.to_s } : @args.to_s @@ -78,6 +81,10 @@ module VagrantPlugins errors << I18n.t("vagrant.provisioners.shell.args_bad_type") end + if @elevated_interactive == true && @privileged == false + errors << I18n.t("vagrant.provisioners.shell.interactive_not_elevated") + end + { "shell provisioner" => errors } end diff --git a/plugins/provisioners/shell/provisioner.rb b/plugins/provisioners/shell/provisioner.rb old mode 100644 new mode 100755 index 1b89d1f29..1abfc4889 --- a/plugins/provisioners/shell/provisioner.rb +++ b/plugins/provisioners/shell/provisioner.rb @@ -137,7 +137,7 @@ module VagrantPlugins end # Execute it with sudo - comm.sudo(command, elevated: config.privileged) do |type, data| + comm.sudo(command, { elevated: config.privileged, interactive: config.elevated_interactive }) do |type, data| handle_comm(type, data) end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml old mode 100644 new mode 100755 index 9e8c680aa..56846292f --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1996,6 +1996,7 @@ en: running: "Running: %{script}" runningas: "Running: %{local} as %{remote}" upload_path_not_set: "`upload_path` must be set for the shell provisioner." + interactive_not_elevated: "To be interactive, it must also be privileged." ansible: no_playbook: "`playbook` must be set for the Ansible provisioner." diff --git a/website/docs/source/v2/provisioning/shell.html.md b/website/docs/source/v2/provisioning/shell.html.md old mode 100644 new mode 100755 index ef4119668..783ce948e --- a/website/docs/source/v2/provisioning/shell.html.md +++ b/website/docs/source/v2/provisioning/shell.html.md @@ -45,8 +45,9 @@ The remainder of the available options are optional: defaults to "true". * `privileged` (boolean) - Specifies whether to execute the shell script - as a privileged user or not (`sudo`). By default this is "true". This has - no effect for Windows guests. + as a privileged user or not (`sudo`). By default this is "true". Windows + guests use a scheduled task to run as a true administrator without the + WinRM limitations. * `upload_path` (string) - Is the remote path where the shell script will be uploaded to. The script is uploaded as the SSH user over SCP, so this @@ -65,6 +66,11 @@ The remainder of the available options are optional: * `powershell_args` (string) - Extra arguments to pass to `PowerShell` if you're provisioning with PowerShell on Windows. +* `elevated_interactive` (boolean) - Run an elevated script in interactive mode + on Windows. By default this is "false". Must also be `privileged`. Be sure to + enable auto-login for Windows as the user must be logged in for interactive + mode to work. + ## Inline Scripts From 9d87be51daa69abea4cc82457a96dfdb9b5bfd92 Mon Sep 17 00:00:00 2001 From: Dan Dunckel Date: Mon, 12 Oct 2015 17:55:48 -0700 Subject: [PATCH 127/484] Small refactor on conditional check and add tests --- plugins/provisioners/shell/config.rb | 2 +- .../communicators/winrm/communicator_test.rb | 9 +++++++++ test/unit/plugins/provisioners/shell/config_test.rb | 13 +++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/plugins/provisioners/shell/config.rb b/plugins/provisioners/shell/config.rb index 19d5180a9..aaadb8081 100755 --- a/plugins/provisioners/shell/config.rb +++ b/plugins/provisioners/shell/config.rb @@ -81,7 +81,7 @@ module VagrantPlugins errors << I18n.t("vagrant.provisioners.shell.args_bad_type") end - if @elevated_interactive == true && @privileged == false + if elevated_interactive && !privileged errors << I18n.t("vagrant.provisioners.shell.interactive_not_elevated") end diff --git a/test/unit/plugins/communicators/winrm/communicator_test.rb b/test/unit/plugins/communicators/winrm/communicator_test.rb index e98f646b3..829170bd6 100644 --- a/test/unit/plugins/communicators/winrm/communicator_test.rb +++ b/test/unit/plugins/communicators/winrm/communicator_test.rb @@ -93,6 +93,15 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do expect(subject.execute("dir", { elevated: true })).to eq(0) end + it "wraps command in elevated and interactive shell script when elevated and interactive are true" do + expect(shell).to receive(:upload).with(kind_of(String), "c:/tmp/vagrant-elevated-shell.ps1") + expect(shell).to receive(:powershell) do |cmd| + expect(cmd).to eq("powershell -executionpolicy bypass -file \"c:/tmp/vagrant-elevated-shell.ps1\" " + + "-username \"vagrant\" -password \"password\" -encoded_command \"ZABpAHIAOwAgAGUAeABpAHQAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAA==\"") + end.and_return({ exitcode: 0 }) + expect(subject.execute("dir", { elevated: true, interactive: true })).to eq(0) + end + it "can use cmd shell" do expect(shell).to receive(:cmd).with(kind_of(String)).and_return({ exitcode: 0 }) expect(subject.execute("dir", { shell: :cmd })).to eq(0) diff --git a/test/unit/plugins/provisioners/shell/config_test.rb b/test/unit/plugins/provisioners/shell/config_test.rb index 946b4c2a8..582740162 100644 --- a/test/unit/plugins/provisioners/shell/config_test.rb +++ b/test/unit/plugins/provisioners/shell/config_test.rb @@ -85,6 +85,19 @@ describe "VagrantPlugins::Shell::Config" do I18n.t("vagrant.provisioners.shell.args_bad_type") ]) end + + it "returns an error if elevated_interactive is true but privileged is false" do + subject.path = file_that_exists + subject.elevated_interactive = true + subject.privileged = false + subject.finalize! + + result = subject.validate(machine) + + expect(result["shell provisioner"]).to eq([ + I18n.t("vagrant.provisioners.shell.interactive_not_elevated") + ]) + end end describe 'finalize!' do From d859a3b75207f75f31165aacf0ec0dc137fae5b1 Mon Sep 17 00:00:00 2001 From: Dan Dunckel Date: Thu, 15 Oct 2015 12:36:19 -0700 Subject: [PATCH 128/484] Somehow I missed this param while resolving conflicts --- plugins/communicators/winrm/communicator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb index d4d14e064..ade021aad 100755 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -194,7 +194,7 @@ module VagrantPlugins # in place. # # @return The wrapper command to execute - def wrap_in_scheduled_task(command) + def wrap_in_scheduled_task(command, interactive) path = File.expand_path("../scripts/elevated_shell.ps1", __FILE__) script = Vagrant::Util::TemplateRenderer.render(path, options: { interactive: interactive, From aec65b5d66f48fa0036a5d1abe13557fbb1a0700 Mon Sep 17 00:00:00 2001 From: Dan Dunckel Date: Thu, 15 Oct 2015 12:41:08 -0700 Subject: [PATCH 129/484] Fix user to username that was lost in merge conflict resolution --- plugins/communicators/winrm/scripts/elevated_shell.ps1.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb index 778dcb0cd..d98bc7554 100755 --- a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb +++ b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb @@ -12,7 +12,7 @@ $task_xml = @' - {user} + {username} <%= options[:interactive] ? 'InteractiveTokenOrPassword' : 'Password' %> HighestAvailable @@ -55,7 +55,7 @@ $schedule.Connect() $task = $schedule.NewTask($null) $task.XmlText = $task_xml $folder = $schedule.GetFolder("\") -$folder.RegisterTaskDefinition($task_name, $task, 6, $user, $password, <%= options[:interactive] ? 3 : 1 %>, $null) | Out-Null +$folder.RegisterTaskDefinition($task_name, $task, 6, $username, $password, <%= options[:interactive] ? 3 : 1 %>, $null) | Out-Null $registered_task = $folder.GetTask("\$task_name") $registered_task.Run($null) | Out-Null From 13be9731ab89a13fd3450bf6e56b66a7fa846bf1 Mon Sep 17 00:00:00 2001 From: Timotei Dolean Date: Fri, 16 Oct 2015 16:20:15 +0300 Subject: [PATCH 130/484] Remove back tick in puppet facts definitions #6403 Starting with vagrant 1.7.3 (commit 1152b4e1df97fb5f468491954932d4f0c09875b1) we don't save the command to be executed in the file anymore, but we send it as a parameter, thus the back tick makes things worse. --- plugins/provisioners/puppet/provisioner/puppet.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/provisioners/puppet/provisioner/puppet.rb b/plugins/provisioners/puppet/provisioner/puppet.rb index f1987dbd5..89d39000f 100644 --- a/plugins/provisioners/puppet/provisioner/puppet.rb +++ b/plugins/provisioners/puppet/provisioner/puppet.rb @@ -212,7 +212,7 @@ module VagrantPlugins # If we're on Windows, we need to use the PowerShell style if windows? - facts.map! { |v| "`$env:#{v};" } + facts.map! { |v| "$env:#{v};" } end facter = "#{facts.join(" ")} " From c17efbed755df621d6571742e163e8dff61dd119 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Oct 2015 18:38:08 -0400 Subject: [PATCH 131/484] core: ignore VAGRANT_DOTFILE_PATH if a child environment This causes issues since the child environment almost certainly doesn't share data with the parent. In a larger scope, we should find a way to encode the data path somehow on `vagrant up`. --- lib/vagrant/environment.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 0981d9df6..015e4b95f 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -159,7 +159,7 @@ module Vagrant # Setup the local data directory. If a configuration path is given, # then it is expanded relative to the working directory. Otherwise, # we use the default which is expanded relative to the root path. - opts[:local_data_path] ||= ENV["VAGRANT_DOTFILE_PATH"] + opts[:local_data_path] ||= ENV["VAGRANT_DOTFILE_PATH"] if !opts[:child] opts[:local_data_path] ||= root_path.join(DEFAULT_LOCAL_DATA) if !root_path.nil? if opts[:local_data_path] @local_data_path = Pathname.new(File.expand_path(opts[:local_data_path], @cwd)) @@ -427,6 +427,7 @@ module Vagrant Util::SilenceWarnings.silence! do Environment.new({ + child: true, cwd: path, home_path: home_path, ui_class: ui_class, From efa01abb124361daa19aa6e5683b7805aae08839 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Oct 2015 10:30:04 -0700 Subject: [PATCH 132/484] providers/virtualbox: if no box, don't import the master --- plugins/providers/virtualbox/action/import_master.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/providers/virtualbox/action/import_master.rb b/plugins/providers/virtualbox/action/import_master.rb index 17296bbcc..ae8f52f32 100644 --- a/plugins/providers/virtualbox/action/import_master.rb +++ b/plugins/providers/virtualbox/action/import_master.rb @@ -12,6 +12,11 @@ module VagrantPlugins end def call(env) + # If we don't have a box, nothing to do + if !env[:machine].box + return @app.call(env) + end + # Do the import while locked so that nobody else imports # a master at the same time. This is a no-op if we already # have a master that exists. From 61466c8e65b476b2d580a0eca06c7512026118c0 Mon Sep 17 00:00:00 2001 From: Markus Perl Date: Sat, 17 Oct 2015 23:00:39 +0200 Subject: [PATCH 133/484] #5186: Warning: Authentication failure. Retrying... after packaging box --- plugins/providers/virtualbox/provider.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index 969fb4718..b8b88d9ca 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -59,12 +59,20 @@ module VagrantPlugins # If the VM is not running that we can't possibly SSH into it return nil if state.id != :running + # If the insecure key was automatically replaced with a newly generated key pair, + # use this key to connect to the machine + private_key_path = nil + if @machine.data_dir.join("private_key").file? + private_key_path = @machine.data_dir.join("private_key") + end + # Return what we know. The host is always "127.0.0.1" because # VirtualBox VMs are always local. The port we try to discover # by reading the forwarded ports. return { host: "127.0.0.1", - port: @driver.ssh_port(@machine.config.ssh.guest_port) + port: @driver.ssh_port(@machine.config.ssh.guest_port), + private_key_path: private_key_path } end From a59dc04b6953c08c96d4f015be9b65599b6b7140 Mon Sep 17 00:00:00 2001 From: Joseph Frazier Date: Sat, 17 Oct 2015 19:42:58 -0400 Subject: [PATCH 134/484] handle_forwarded_port_collisions.rb: fix typo: "Reparied" -> "Repaired" --- lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb b/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb index b53ba35c2..cf6477915 100644 --- a/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb +++ b/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb @@ -128,7 +128,7 @@ module Vagrant port_checker[repaired_port] || lease_check(repaired_port) if in_use - @logger.info("Reparied port also in use: #{repaired_port}. Trying another...") + @logger.info("Repaired port also in use: #{repaired_port}. Trying another...") next end From d142640d853d8885117eea6cb7688abdddf43f1f Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 22 Oct 2015 12:18:43 -0400 Subject: [PATCH 135/484] Fix SHASUM file name --- scripts/bintray_upload.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/bintray_upload.sh b/scripts/bintray_upload.sh index 030d56300..83a30e4e2 100755 --- a/scripts/bintray_upload.sh +++ b/scripts/bintray_upload.sh @@ -22,9 +22,13 @@ if [ -z $BINTRAY_API_KEY ]; then exit 1 fi -# Calculate the checksums -pushd ./dist -shasum -a256 * > ./${VERSION}_SHA256SUMS +# Make the checksums +pushd ./pkg/dist +shasum -a256 * > ./vagrant_${VERSION}_SHA256SUMS +if [ -z $NOSIGN ]; then + echo "==> Signing..." + gpg --default-key 348FFC4C --detach-sig ./vagrant_${VERSION}_SHA256SUMS +fi popd # Upload From 23990e34e93812ab98be1b8d51e73d86b36465fb Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 26 Oct 2015 11:29:54 -0400 Subject: [PATCH 136/484] Use releases for releases --- website/www/Gemfile.lock | 34 +++--- website/www/config.rb | 11 +- website/www/config.ru | 4 + website/www/helpers/download_helpers.rb | 85 --------------- .../www/helpers/middleman_hashicorp_shim.rb | 103 ++++++++++++++++++ website/www/lib/redirect_to_releases.rb | 37 +++++++ website/www/source/_sidebar_downloads.erb | 2 +- .../source/download-archive-single.html.erb | 51 --------- website/www/source/downloads-archive.html.erb | 32 ------ website/www/source/downloads.html.erb | 76 +++++++------ website/www/source/images/fastly_logo.png | Bin 0 -> 3995 bytes .../www/source/images/icons/icon_centos.png | Bin 0 -> 5002 bytes .../www/source/images/icons/icon_darwin.png | Bin 848 -> 1850 bytes .../www/source/images/icons/icon_debian.png | Bin 32226 -> 2906 bytes .../www/source/images/icons/icon_freebsd.png | Bin 27639 -> 5852 bytes .../www/source/images/icons/icon_hashios.png | Bin 0 -> 1842 bytes .../www/source/images/icons/icon_linux.png | Bin 16563 -> 4229 bytes .../www/source/images/icons/icon_macosx.png | Bin 0 -> 1956 bytes .../www/source/images/icons/icon_openbsd.png | Bin 27660 -> 10634 bytes website/www/source/images/icons/icon_rpm.png | Bin 36773 -> 5016 bytes .../www/source/images/icons/icon_windows.png | Bin 12626 -> 3292 bytes 21 files changed, 207 insertions(+), 228 deletions(-) delete mode 100644 website/www/helpers/download_helpers.rb create mode 100644 website/www/helpers/middleman_hashicorp_shim.rb create mode 100644 website/www/lib/redirect_to_releases.rb delete mode 100644 website/www/source/download-archive-single.html.erb delete mode 100644 website/www/source/downloads-archive.html.erb create mode 100644 website/www/source/images/fastly_logo.png create mode 100644 website/www/source/images/icons/icon_centos.png create mode 100644 website/www/source/images/icons/icon_hashios.png create mode 100644 website/www/source/images/icons/icon_macosx.png diff --git a/website/www/Gemfile.lock b/website/www/Gemfile.lock index 112d0d72d..946a40701 100644 --- a/website/www/Gemfile.lock +++ b/website/www/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - activesupport (3.2.21) + activesupport (3.2.22) i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) builder (3.2.2) @@ -9,7 +9,7 @@ GEM coffee-script (2.2.0) coffee-script-source execjs - coffee-script-source (1.9.1) + coffee-script-source (1.9.1.1) commonjs (0.2.7) compass (1.0.3) chunky_png (~> 1.2) @@ -23,20 +23,20 @@ GEM sass (>= 3.3.0, < 3.5) compass-import-once (1.0.5) sass (>= 3.2, < 3.5) - daemons (1.1.9) - eventmachine (1.0.7) + daemons (1.2.3) + eventmachine (1.0.8) execjs (1.4.1) multi_json (~> 1.0) - ffi (1.9.6) - haml (4.0.6) + ffi (1.9.10) + haml (4.0.7) tilt highline (1.6.21) hike (1.2.3) i18n (0.6.11) - kramdown (1.5.0) + kramdown (1.9.0) less (2.2.2) commonjs (~> 0.2.6) - libv8 (3.16.14.7) + libv8 (3.16.14.13) listen (1.3.1) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) @@ -75,25 +75,25 @@ GEM sprockets-helpers (~> 1.0.0) sprockets-sass (~> 1.0.0) mini_portile (0.6.2) - multi_json (1.10.1) + multi_json (1.11.2) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) - rack (1.6.0) + rack (1.6.4) rack-contrib (1.1.0) rack (>= 0.9.1) rack-protection (1.5.3) rack rack-test (0.6.3) rack (>= 1.0) - rb-fsevent (0.9.4) + rb-fsevent (0.9.6) rb-inotify (0.9.5) ffi (>= 0.5.0) - rb-kqueue (0.2.3) + rb-kqueue (0.2.4) ffi (>= 0.5.0) redcarpet (3.0.0) - ref (1.0.5) - sass (3.4.13) - sprockets (2.12.3) + ref (2.0.0) + sass (3.4.19) + sprockets (2.12.4) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) @@ -103,7 +103,7 @@ GEM sprockets-sass (1.0.3) sprockets (~> 2.0) tilt (~> 1.1) - therubyracer (0.12.1) + therubyracer (0.12.2) libv8 (~> 3.16.14.0) ref thin (1.5.1) @@ -112,7 +112,7 @@ GEM rack (>= 1.0.0) thor (0.19.1) tilt (1.3.7) - tzinfo (0.3.43) + tzinfo (0.3.45) uglifier (2.1.2) execjs (>= 0.3.0) multi_json (~> 1.0, >= 1.0.2) diff --git a/website/www/config.rb b/website/www/config.rb index d30b7bd51..a5b2fa61a 100644 --- a/website/www/config.rb +++ b/website/www/config.rb @@ -1,13 +1,10 @@ -require File.expand_path("../helpers/download_helpers", __FILE__) +require_relative "helpers/middleman_hashicorp_shim" +helpers MiddlemanHashiCorpHelpers page "/blog_feed.xml", layout: false -ignore "/download-archive-single.html" -# Archived download pages -$vagrant_versions.each do |version| - proxy "/download-archive/v#{version}.html", "/download-archive-single.html", - locals: { version: version }, ignore: true -end +# The version of Vagrant - update this to update the downloads page +set :latest_version, '1.7.4' set :css_dir, 'stylesheets' set :js_dir, 'javascripts' diff --git a/website/www/config.ru b/website/www/config.ru index 2cef78935..49f749c15 100644 --- a/website/www/config.ru +++ b/website/www/config.ru @@ -6,6 +6,7 @@ require "rack/contrib/try_static" require "rack/protection" require File.expand_path("../lib/legacy_redirect", __FILE__) +require File.expand_path("../lib/redirect_to_releases", __FILE__) # Protect against various bad things use Rack::Protection::JsonCsrf @@ -23,6 +24,9 @@ use Rack::Deflater # Redirect the legacy URLs that point to www.vagrantup.com use HashiCorp::Rack::LegacyRedirect +# Redirect the archive page to releases +use HashiCorp::Rack::RedirectToReleases + # Set the "forever expire" cache headers for these static assets. Since # we hash the contents of the assets to determine filenames, this is safe # to do. diff --git a/website/www/helpers/download_helpers.rb b/website/www/helpers/download_helpers.rb deleted file mode 100644 index 9244d9af4..000000000 --- a/website/www/helpers/download_helpers.rb +++ /dev/null @@ -1,85 +0,0 @@ -require "json" -require "net/http" -require "net/https" - -$vagrant_files = {} -$vagrant_os = [] - -$vagrant_os_mappings = { - ".deb" => "debian", - ".dmg" => "darwin", - ".msi" => "windows", - ".rpm" => "rpm", -} - -$vagrant_os_order = ["darwin", "windows", "debian", "rpm"] -$vagrant_downloads = {} -$vagrant_versions = [] - -if ENV["VAGRANT_VERSION"] - puts "Finding downloads for Vagrant" - raise "BINTRAY_API_KEY must be set." if !ENV["BINTRAY_API_KEY"] - http = Net::HTTP.new("bintray.com", 443) - http.use_ssl = true - req = Net::HTTP::Get.new("/api/v1/packages/mitchellh/vagrant/vagrant/files") - req.basic_auth "mitchellh", ENV["BINTRAY_API_KEY"] - response = http.request(req) - data = JSON.parse(response.body) - - data.each do |file| - filename = file["name"] - - # Ignore any files that don't appear to have a version in it - next if filename !~ /[-_]?(\d+\.\d+\.\d+[^-_.]*)/ - version = Gem::Version.new($1.to_s) - $vagrant_downloads[version] ||= {} - - $vagrant_os_mappings.each do |suffix, os| - if filename.end_with?(suffix) - $vagrant_downloads[version][os] ||= [] - $vagrant_downloads[version][os] << filename - end - end - end - - $vagrant_versions = $vagrant_downloads.keys.sort.reverse - $vagrant_versions.each do |v| - puts "- Version #{v} found" - end -else - puts "Not generating downloads." -end - -module DownloadHelpers - def download_arch(file) - if file.include?("i686") - return "32-bit" - elsif file.include?("x86_64") - return "64-bit" - else - return "Universal (32 and 64-bit)" - end - end - - def download_os_human(os) - if os == "darwin" - return "Mac OS X" - elsif os == "debian" - return "Linux (Deb)" - elsif os == "rpm" - return "Linux (RPM)" - elsif os == "windows" - return "Windows" - else - return os - end - end - - def download_url(file) - "https://dl.bintray.com/mitchellh/vagrant/#{file}" - end - - def latest_version - $vagrant_versions.first - end -end diff --git a/website/www/helpers/middleman_hashicorp_shim.rb b/website/www/helpers/middleman_hashicorp_shim.rb new file mode 100644 index 000000000..9ac10b4d0 --- /dev/null +++ b/website/www/helpers/middleman_hashicorp_shim.rb @@ -0,0 +1,103 @@ +# This file is a shim that mirrors the behavior or middleman-hashicorp without +# fully importing it. Vagrant is somewhat of a beast and cannot be easily +# updated due to older versions of bootstrap and javascript and whatnot. + +require "open-uri" + +class MiddlemanHashiCorpReleases + RELEASES_URL = "https://releases.hashicorp.com".freeze + + class Build < Struct.new(:name, :version, :os, :arch, :url); end + + def self.fetch(product, version) + url = "#{RELEASES_URL}/#{product}/#{version}/index.json" + r = JSON.parse(open(url).string, + create_additions: false, + symbolize_names: true, + ) + + # Convert the builds into the following format: + # + # { + # "os" => { + # "arch" => "https://download.url" + # } + # } + # + {}.tap do |h| + r[:builds].each do |b| + build = Build.new(*b.values_at(*Build.members)) + + h[build.os] ||= {} + h[build.os][build.arch] = build.url + end + end + end +end + +module MiddlemanHashiCorpHelpers + # + # Output an image that corresponds to the given operating system using the + # vendored image icons. + # + # @return [String] (html) + # + def system_icon(name) + image_tag("icons/icon_#{name.to_s.downcase}.png") + end + + # + # The formatted operating system name. + # + # @return [String] + # + def pretty_os(os) + case os + when /darwin/ + "Mac OS X" + when /freebsd/ + "FreeBSD" + when /openbsd/ + "OpenBSD" + when /linux/ + "Linux" + when /windows/ + "Windows" + else + os.capitalize + end + end + + # + # The formatted architecture name. + # + # @return [String] + # + def pretty_arch(arch) + case arch + when /all/ + "Universal (32 and 64-bit)" + when /686/, /386/ + "32-bit" + when /86_64/, /amd64/ + "64-bit" + else + parts = arch.split("_") + + if parts.empty? + raise "Could not determine pretty arch `#{arch}'!" + end + + parts.last.capitalize + end + end + + # + # Query the Bintray API to get the real product download versions. + # + # @return [Hash] + # + def product_versions + MiddlemanHashiCorpReleases.fetch("vagrant", latest_version) + end +end diff --git a/website/www/lib/redirect_to_releases.rb b/website/www/lib/redirect_to_releases.rb new file mode 100644 index 000000000..655c58d2e --- /dev/null +++ b/website/www/lib/redirect_to_releases.rb @@ -0,0 +1,37 @@ +module HashiCorp + module Rack + # This redirects to releases.hashicorp.com when a user tries to download + # an old version of Vagrant. + class RedirectToReleases + def initialize(app) + @app = app + end + + def call(env) + if env["PATH_INFO"] =~ /^\/downloads-archive/ + headers = { + "Content-Type" => "text/html", + "Location" => "https://releases.hashicorp.com/vagrant", + } + + message = "Redirecting to releases archive..." + + return [301, headers, [message]] + end + + if env["PATH_INFO"] =~ /^\/download-archive\/v(.+)\.html/ + headers = { + "Content-Type" => "text/html", + "Location" => "https://releases.hashicorp.com/vagrant/#{$1}", + } + + message = "Redirecting to releases archive..." + + return [301, headers, [message]] + end + + @app.call(env) + end + end + end +end diff --git a/website/www/source/_sidebar_downloads.erb b/website/www/source/_sidebar_downloads.erb index faf7b8f3b..939cd078a 100644 --- a/website/www/source/_sidebar_downloads.erb +++ b/website/www/source/_sidebar_downloads.erb @@ -1,4 +1,4 @@ diff --git a/website/www/source/download-archive-single.html.erb b/website/www/source/download-archive-single.html.erb deleted file mode 100644 index 41b8e7fac..000000000 --- a/website/www/source/download-archive-single.html.erb +++ /dev/null @@ -1,51 +0,0 @@ ---- -layout: "inner" -sidebar_current: "downloads" -sidebar_template: "downloads" -sidebar_title: "Download" -page_title: "Download Old Vagrant Version" ---- - -

    Old Vagrant Version: <%= version %>

    - -
    -This is the downloads page for an old version of Vagrant. -The latest version of Vagrant can always be found on the -main downloads page. -
    - -
    -
    -

    -You can find the SHA256 checksums for this version of Vagrant -here. -

    -
    - -<% $vagrant_os_order.each do |os| %> - <% downloads = $vagrant_downloads[version] %> - <% if downloads[os] && !downloads[os].empty? %> -
    -
    -
    <%= image_tag "/images/icons/icon_#{os}.png" %>
    -
    -

    <%= download_os_human(os) %>

    - -
    -
    -
    -
    - <% end %> -<% end %> - -
    - - - -
    -
    - diff --git a/website/www/source/downloads-archive.html.erb b/website/www/source/downloads-archive.html.erb deleted file mode 100644 index 2e8da7f97..000000000 --- a/website/www/source/downloads-archive.html.erb +++ /dev/null @@ -1,32 +0,0 @@ ---- -layout: "inner" -sidebar_current: "downloads-archive" -sidebar_template: "downloads" -sidebar_title: "Download" -page_title: "Download Old Versions of Vagrant" ---- - -

    Old Versions

    - -

    -This webpage lists the older versions of Vagrant that are available for -download. Some even older versions are available from the -legacy downloads page. -

    - -

    -The latest version of Vagrant can be found on the -main downloads page. -

    - -<% if $vagrant_versions.length > 1 %> - -<% else %> -

    -No old Vagrant versions could be found. -

    -<% end %> diff --git a/website/www/source/downloads.html.erb b/website/www/source/downloads.html.erb index 252b325a2..55b10484e 100644 --- a/website/www/source/downloads.html.erb +++ b/website/www/source/downloads.html.erb @@ -9,41 +9,47 @@ page_title: "Download Vagrant"

    Download Vagrant

    -
    -

    -Below are all available downloads for the latest version of Vagrant -(<%= latest_version %>). Please download the proper package for your -operating system and architecture. You can find SHA256 checksums -for packages here, -and you can find the version changelog here. -

    -
    +
    +

    + Below are the available downloads for the latest version of Vagrant + (<%= latest_version %>). Please download the proper package for your + operating system and architecture. +

    +

    + You can find the + + SHA256 checksums for Vagrant <%= latest_version %> + + online and you can + + verify the checksums signature file + + which has been signed using HashiCorp's GPG key. + You can also download older versions of Vagrant from the releases service. +

    +
    -<% if latest_version %> -<% $vagrant_os_order.each do |os| %> - <% downloads = $vagrant_downloads[latest_version] %> - <% if downloads[os] && !downloads[os].empty? %> -
    -
    -
    <%= image_tag "/images/icons/icon_#{os}.png" %>
    -
    -

    <%= download_os_human(os) %>

    - -
    -
    -
    -
    - <% end %> -<% end %> -<% end %> + <% product_versions.each do |os, arches| %> + <% next if os == "web" %> +
    +
    +
    <%= system_icon(os) %>
    +
    +

    <%= pretty_os(os) %>

    + +
    +
    +
    +
    + <% end %> -
    - - - -
    +
    diff --git a/website/www/source/images/fastly_logo.png b/website/www/source/images/fastly_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..de82b3c0d00b2c16c2a04c6c4616cfff43ddfc77 GIT binary patch literal 3995 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

    HGW5xRvfA4Ag zm#Y943%$}{@@XY7smI_i2eT_+5di7?r`8RbDN!);Z=k%!MErS!UnSk3#L4#A54t)NBQ z4`8u4IC34&CQ4Fz3FLK%BS1qog4VF~sjn{McmD2gv0oqP6sf5W@)9;i#L^2h8LkQ_ z&zFPgm1+o@+N5{arvCSf2>@UvB;J{v6#+=}J5_Dv^DaQ#8fRnx+(p3H!^~oQ4|UrP z0^^lf$MUbW*7{<7iwdj*`Z0GDbr$$FcST<>;f|#GwECk&!Wisy12CI2a&gdE8QiZX zrW@JPEo^F|O_c=bkeI`!I9ry1Yv@&H(-W{7)-iYf(CXwdP&uNl#1maiJvBKXrVw}2 zsj?;B>!J~8{Ke?RRiMA4zU3a{bXd0;x1*lcGDG&p?7mjnE-iUu1P72?mMD3^m`~FR zq&`^0`*FP{?Aw)~i(E1az9*hT(UtFLNEanI7P;Ncfzn)LKS{w6O6P!eCrpqNaXo?3I+vAgw={!)Yjby5 zzJqB30^op7>+>f?Rx?{GB_c{2kI+$;xC9(S3xa>;VS0 z@%cuW|Np_YUJFm~N%o0}({fqK%H+ayAZBn>38Ik7Ah$zDE91sZ?mWs$Rwftb-#bz6 zSPBZ3M{&QuQzt7~VQI~}W#&Vga8=C;kCxJPk;_U}B(b}toyC3Q%m{Aa<%xi~tYk%2 zcnnigVA%!L13--m&I?P%^k=f(>r3zPu5GL}mJFy20t{{w7qpjU8vaYFzA002ovPDHLkV1kfk Bx3mBN literal 0 HcmV?d00001 diff --git a/website/www/source/images/icons/icon_centos.png b/website/www/source/images/icons/icon_centos.png new file mode 100644 index 0000000000000000000000000000000000000000..53d423cff44264be05ab33bcde4bc857e350c30f GIT binary patch literal 5002 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 UNyD7~asU7T07*qoM6N<$f?RKc^Z)<= literal 0 HcmV?d00001 diff --git a/website/www/source/images/icons/icon_darwin.png b/website/www/source/images/icons/icon_darwin.png index c9ad9fe041b552bb30f3f4f461739a4136ceaa8e..f5516bb8defdbd9a472ceec669d0c26b8e082e00 100644 GIT binary patch literal 1850 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 Eg3D!#RsaA1 literal 32226 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�H$jEt#K;~+Mx1&rDGAt}9U#CnlO#)( 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 diff --git a/website/www/source/images/icons/icon_hashios.png b/website/www/source/images/icons/icon_hashios.png new file mode 100644 index 0000000000000000000000000000000000000000..48cdefc685a72c15345aae73754f1b42c1667d55 GIT binary patch literal 1842 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 literal 16563 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

    oCOiY7uJlC}Ryg z^1x;u1oC;GzB%2@Tt1s+{UsJJ>F3aSy&T#g&D>IfB3X!y4^z$9qxiKJv1PVk$zw8E z@qCvgc7{YjXa-VHvt&rjtePcD&_IWP!JTmb28^%?xCeL`cn%mhWC#HpPc?N5A_E0@ z?nlhCJKL_qG#E^D5-nFIiCdpqbIG?*l zpPTFE+0sBu1=t8Yi zeVA?EzLyJs{VaD`lS-4rz+LEb-?@Y!g(%a?2T@MlAwudqcm3RVW>MdeTf`DIQlZ+w zGf(f}DIk~e=w3F!+@t2O;;6YCedugf%AVvb$P~}Xv%t6CXUCI6{P)-d ze^u4CqgCv2Kttnlkki>;_ z+in^X#1g@cJ~*G@l(ya)>3nNmkTZ26Nxjke&ouDlkJs^&`8~{CIiHosF66``=5ox; zZn~ljOc;mWM7vC+D}mJ{I1X#938H698@`ADTAtW7xEv z^^Lap%=#TX4fMHzg=S?6l>2UaCM%|!QKS;YLIAQ^5WvDcVS$1Ixn+#44Q#yS32r}P zfW^ly;@A_GuzJBv4li}$Ck~}&LdF?q)^xN;EFm_l&2@7Cd;w?-wA=sV9xmQC&UTD5 zoYLvI1K*Z=l?v;-#Awn!((zNGn;+xGl?yn&yOWtg6_i;~_d~E|l`e%7>TUpQV(E6t zOL*ov9=hUA*8FJ~8y$Zmp2EOa?&nV5$EIXDhIysaajh)us1){0 zsRkaq`4N8dlDQmyQiCWf7jWXdKITgw>K-T`e4FKJomOd6G6J*am{aikKXcR9?&P9! zoe-E|wT0}W#GGQ;zYQ_BxUYTJ9zqAzvj|&R&!pdgBj*(H9X&7l6F9X z!rp8vBgp8Xs_uG*|N72v`1f5E#%)_5H72X4f1dK0b@E?85lX53e)ohcV0{M1q1133 z%#^x?8_`K02rd8upaytzx)?ZiA;+v*zzK&8@Y236X619H7Yq%RB?&@7RzRgn_14F^ z;>L%#ej-2uohh^AWR58=TGAqJ8yFI#K)7|MEyh>O<*>IL%X<%-!|Fl~p#(%$Cp*WH z>gTKg0hFsmTZVby7mxA%d!OaLSmFakJCIRLG)SCQy6+sgyDq0CK|~r63`7%BxZiO* z-b*2d=i2UA5qAB7%;H|=ub9X3B{NyMY=9#Nx|x^H&?5vMfRqGdHHMxZ;{H3<^W)!d z;z6JUWL*+8nE_P`po=^~C}$^>X;EE(SVs^k#fI5=%=! zEQupUwH7e?+&F_`)tu9k0-MCK%;s)=y`t4C6}S(d1O&2si=%p*s*1lZBn5OIXoavv6)>{%OYJw zGtJ?)Q)-)}PZ%Ch2daST$kNdu$c$o+Qbv$76NHkAI9AlF0p-Clb_Ow3YsQiVa7kh7 zsfb&v8yI=95d=4GZ_(70@}1ucheV_iBu#^)M9Tt0n9vC_|DCcKN#a(CV?YW)rgtuu(|9f+>grTM;o>wr%loDeQqT5K2G(mhIY5AA|O zewQ2oaavn6)v)%RecxQBIYHd;oF#;EAS^j7(#)y7B!?hfnukr(HHd1NAQmdcq>{Fs zkSRV#ym!HhbdsbjY^oGU1C>%bCTUB74PNX7aUm^I{SDDNLBK6j21PTdy&4p@bZ1ZH zni?q%tOD4x7PX{EiQth+j`a2rd)XEM(k%_t6f|tgr16(V5DV#;SLx2CsXNbcE5vAZ zFtn;gQgVr8)4`EG5MoQprco)Grdg+MZZ=OJN+F7BMvxXK3Jwy)A<7gYGNK7cDfdN# zkdkHE>ddJ~msrF47cIBV$uaddX<~wky@YCc(nu!;P5cqkHAlJ}BuIK^Sh6%H2Y{E7O3oDgkd}Rsv{dc= z8Q^eCNq-9Dpos=iPTeI4t!t9~-zEnMl0H!5DGUat>ZH5mXxaXL;YFI#)wVcgDlzua zFC$%>rW4{IL0(|Ow0P7IQ+3jtn-#WfA8l(%Fcs~Ql1!>vB}ht}93)6Pk;cW82{PRz now%Zfx&lN~g1pG({{z2vbROxp34;S3M!?|d>gTe~DWM4fTMXN` diff --git a/website/www/source/images/icons/icon_macosx.png b/website/www/source/images/icons/icon_macosx.png new file mode 100644 index 0000000000000000000000000000000000000000..c5311e7cefc7038ba56520ef347fad70dc1410b5 GIT binary patch literal 1956 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

    4X;NC`CRE*R-cCcx{w@lZMuy_ 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 LNkvXXu0mjfjC}{$ diff --git a/website/www/source/images/icons/icon_rpm.png b/website/www/source/images/icons/icon_rpm.png index 871df56e04d0920fa7b6a57f4765b25c3fac06e8..f72f3c106ef443c6f59242403465792926613fe6 100644 GIT binary patch literal 5016 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 b`Qi0k|1NEM?9z#r}v6CK?Rss1`q? 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 diff --git a/website/www/source/images/icons/icon_windows.png b/website/www/source/images/icons/icon_windows.png index d7748c0095e29d21f2817b2ce74ef29cadbaea7d..68a912815c41cfa17837ab908f56d92c50033063 100644 GIT binary patch literal 3292 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 a3H}FMpn>uWp@hKz0000S)W@ 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 Date: Mon, 26 Oct 2015 18:10:26 -0700 Subject: [PATCH 137/484] core: machine-readable output should include standard UI output As a "ui" type --- lib/vagrant/ui.rb | 6 ++++++ test/unit/vagrant/ui_test.rb | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/vagrant/ui.rb b/lib/vagrant/ui.rb index ba982bc05..fb5929f32 100644 --- a/lib/vagrant/ui.rb +++ b/lib/vagrant/ui.rb @@ -103,6 +103,12 @@ module Vagrant raise Errors::UIExpectsTTY end + [:detail, :warn, :error, :info, :output, :success].each do |method| + define_method(method) do |message, *opts| + machine("ui", method.to_s, message) + end + end + def machine(type, *data) opts = {} opts = data.pop if data.last.kind_of?(Hash) diff --git a/test/unit/vagrant/ui_test.rb b/test/unit/vagrant/ui_test.rb index e7f2299dd..61bfe7270 100644 --- a/test/unit/vagrant/ui_test.rb +++ b/test/unit/vagrant/ui_test.rb @@ -215,6 +215,24 @@ describe Vagrant::UI::MachineReadable do end end + [:detail, :warn, :error, :info, :output, :success].each do |method| + describe "##{method}" do + it "outputs UI type to the machine-readable output" do + expect(subject).to receive(:safe_puts).with { |message| + parts = message.split(",") + expect(parts.length).to eq(5) + expect(parts[1]).to eq("") + expect(parts[2]).to eq("ui") + expect(parts[3]).to eq(method.to_s) + expect(parts[4]).to eq("foo") + true + } + + subject.send(method, "foo") + end + end + end + describe "#machine" do it "is formatted properly" do expect(subject).to receive(:safe_puts).with { |message| From 8392752bee81fb481e0e342893e753776db5fe2e Mon Sep 17 00:00:00 2001 From: glamouracademy Date: Tue, 27 Oct 2015 13:06:35 -0400 Subject: [PATCH 138/484] Add links for OS links in Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a361da9b0..0584d273d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ between Windows, Mac OS X, and Linux. For the quick-start, we'll bring up a development machine on [VirtualBox](http://www.virtualbox.org) because it is free and works on all major platforms. Vagrant can, however, work with almost any -system such as OpenStack, VMware, Docker, etc. +system such as [OpenStack] (https://www.openstack.org/), [VMware] (http://www.vmware.com/), [Docker] (https://docs.docker.com/), etc. First, make sure your development machine has [VirtualBox](http://www.virtualbox.org) From 8e7a297fb51bb49686b59af9cfb78a8f94709c54 Mon Sep 17 00:00:00 2001 From: matthewcodes Date: Wed, 28 Oct 2015 16:39:19 +0000 Subject: [PATCH 139/484] Fix for interpolated strings being used for username and passwords, this fix was made in commit 1dd081d but was removed by 1152b4e. This was causing passwords with $ in them to stop working as the dollar sign was getting stripped out --- plugins/communicators/winrm/communicator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb index b14b28efa..0131678a5 100644 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -214,7 +214,7 @@ module VagrantPlugins "#{command}; exit $LASTEXITCODE".encode('UTF-16LE', 'UTF-8')) "powershell -executionpolicy bypass -file \"#{guest_script_path}\" " + - "-username \"#{shell.username}\" -password \"#{shell.password}\" " + + "-username \'#{shell.username}\' -password \'#{shell.password}\' " + "-encoded_command \"#{wrapped_encoded_command}\"" end From cbb03a02d4b42e350ed92f73ced05f3046100ae8 Mon Sep 17 00:00:00 2001 From: matthewcodes Date: Wed, 28 Oct 2015 16:46:43 +0000 Subject: [PATCH 140/484] Updating tests to check for single quote --- test/unit/plugins/communicators/winrm/communicator_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/plugins/communicators/winrm/communicator_test.rb b/test/unit/plugins/communicators/winrm/communicator_test.rb index e98f646b3..29c775463 100644 --- a/test/unit/plugins/communicators/winrm/communicator_test.rb +++ b/test/unit/plugins/communicators/winrm/communicator_test.rb @@ -88,7 +88,7 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do expect(shell).to receive(:upload).with(kind_of(String), "c:/tmp/vagrant-elevated-shell.ps1") expect(shell).to receive(:powershell) do |cmd| expect(cmd).to eq("powershell -executionpolicy bypass -file \"c:/tmp/vagrant-elevated-shell.ps1\" " + - "-username \"vagrant\" -password \"password\" -encoded_command \"ZABpAHIAOwAgAGUAeABpAHQAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAA==\"") + "-username \'vagrant\' -password \'password\' -encoded_command \"ZABpAHIAOwAgAGUAeABpAHQAIAAkAEwAQQBTAFQARQBYAEkAVABDAE8ARABFAA==\"") end.and_return({ exitcode: 0 }) expect(subject.execute("dir", { elevated: true })).to eq(0) end From 36d6c430d2304518c206a35285ede235feeb17f1 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 29 Oct 2015 16:10:22 -0400 Subject: [PATCH 141/484] Link to releases --- website/www/source/_sidebar_downloads.erb | 2 +- website/www/source/downloads.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/www/source/_sidebar_downloads.erb b/website/www/source/_sidebar_downloads.erb index 939cd078a..84d1dda6d 100644 --- a/website/www/source/_sidebar_downloads.erb +++ b/website/www/source/_sidebar_downloads.erb @@ -1,4 +1,4 @@

    diff --git a/website/www/source/downloads.html.erb b/website/www/source/downloads.html.erb index 55b10484e..b84d966ab 100644 --- a/website/www/source/downloads.html.erb +++ b/website/www/source/downloads.html.erb @@ -25,7 +25,7 @@ page_title: "Download Vagrant" verify the checksums signature file which has been signed using HashiCorp's GPG key. - You can also download older versions of Vagrant from the releases service. + You can also download older versions of Vagrant from the releases service.

    From 2124b85b92793129b31715a824c9cd7ff8f4be3b Mon Sep 17 00:00:00 2001 From: Karthik Gaekwad Date: Sat, 31 Oct 2015 16:47:31 -0500 Subject: [PATCH 142/484] fixes simple spelling errors --- website/docs/source/v2/cli/plugin.html.md | 2 +- website/docs/source/v2/provisioning/salt.html.md | 4 ++-- website/www/source/vmware/index.html.erb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/source/v2/cli/plugin.html.md b/website/docs/source/v2/cli/plugin.html.md index 45bb75a4f..a92cc42cb 100644 --- a/website/docs/source/v2/cli/plugin.html.md +++ b/website/docs/source/v2/cli/plugin.html.md @@ -50,7 +50,7 @@ This command accepts optional command-line flags: * `--plugin-version VERSION` - The version of the plugin to install. By default, this command will install the latest version. You can constrain the version using this flag. You can set it to a specific version, such as "1.2.3" or - you can set it to a version contraint, such as "> 1.0.2". You can set it + you can set it to a version constraint, such as "> 1.0.2". You can set it to a more complex constraint by comma-separating multiple constraints: "> 1.0.2, < 1.1.0" (don't forget to quote these on the command-line). diff --git a/website/docs/source/v2/provisioning/salt.html.md b/website/docs/source/v2/provisioning/salt.html.md index 938fec155..37c70e4fd 100644 --- a/website/docs/source/v2/provisioning/salt.html.md +++ b/website/docs/source/v2/provisioning/salt.html.md @@ -121,9 +121,9 @@ Either of the following may be used to actually execute runners during provisioning. * `run_overstate` - (boolean) Executes `state.over` on -vagrant up. Can be applied to the master only. This is superceded by orchestrate. Not supported on Windows guest machines. +vagrant up. Can be applied to the master only. This is superseded by orchestrate. Not supported on Windows guest machines. * `orchestrations` - (boolean) Executes `state.orchestrate` on -vagrant up. Can be applied to the master only. This is supercedes by run_overstate. Not supported on Windows guest machines. +vagrant up. Can be applied to the master only. This is supersedes by run_overstate. Not supported on Windows guest machines. ## Output Control diff --git a/website/www/source/vmware/index.html.erb b/website/www/source/vmware/index.html.erb index f3d6c783c..9852d7bd5 100644 --- a/website/www/source/vmware/index.html.erb +++ b/website/www/source/vmware/index.html.erb @@ -380,7 +380,7 @@ page_title: "VMware Vagrant Environments" manually. Occassionally you must accept the license agreement before VMware will run. If you do not see any errors when opening the VMware GUI, you may need to purchase the full version to use the - plugin. We apologize for the inconvience. + plugin. We apologize for the inconvenience.

    From dde94a3ce7bc6f89eabbad4bde2b6f8665a37c72 Mon Sep 17 00:00:00 2001 From: Gilles Cornu Date: Mon, 2 Nov 2015 09:03:15 +0100 Subject: [PATCH 143/484] provisioners/ansible: add force_remote_user option The benefits of the following "breaking change" are the following: - default behaviour naturally fits with most common usage (i.e. always connect with Vagrant SSH settings) - the autogenerated inventory is more consistent by providing both the SSH username and private key. - no longer needed to explain how to override Ansible `remote_user` parameters Important: With the `force_remote_user` option, people still can fall back to the former behavior (prior to Vagrant 1.8.0), which means that Vagrant integration capabilities are still quite open and flexible. --- CHANGELOG.md | 14 ++++ plugins/provisioners/ansible/config.rb | 5 +- plugins/provisioners/ansible/provisioner.rb | 21 +++-- .../provisioners/ansible/config_test.rb | 5 ++ .../provisioners/ansible/provisioner_test.rb | 80 ++++++++++++++----- .../source/v2/provisioning/ansible.html.md | 31 +------ 6 files changed, 105 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f78eac8bc..602956e8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,22 @@ FEATURES: - **IPv6 Private Networks**: Private networking now supports IPv6. This only works with VirtualBox and VMware at this point. [GH-6342] +BREAKING CHANGES: + + - the `ansible` provisioner now can override the effective ansible remote user + (i.e. `ansible_ssh_user` setting) to always correspond to the vagrant ssh + username. This change is enabled by default, but we expect this to affect + only a tiny number of people as it corresponds to the common usage. + If you however use different remote usernames in your Ansible plays, tasks, + or custom inventories, you can simply set the option `force_remote_user` to + false to make Vagrant behave the same as before. + + IMPROVEMENTS: + - provisioners/ansible: add new `force_remote_user` option to control whether + `ansible_ssh_user` parameter should be applied or not [GH-6348] + BUG FIXES: - communicator/winrm: respect `boot_timeout` setting [GH-6229] diff --git a/plugins/provisioners/ansible/config.rb b/plugins/provisioners/ansible/config.rb index 784cf48f7..f3aa99deb 100644 --- a/plugins/provisioners/ansible/config.rb +++ b/plugins/provisioners/ansible/config.rb @@ -2,6 +2,7 @@ module VagrantPlugins module Ansible class Config < Vagrant.plugin("2", :config) attr_accessor :playbook + attr_accessor :force_remote_user attr_accessor :extra_vars attr_accessor :inventory_path attr_accessor :ask_sudo_pass @@ -24,6 +25,7 @@ module VagrantPlugins def initialize @playbook = UNSET_VALUE + @force_remote_user = UNSET_VALUE @extra_vars = UNSET_VALUE @inventory_path = UNSET_VALUE @ask_sudo_pass = UNSET_VALUE @@ -44,6 +46,7 @@ module VagrantPlugins def finalize! @playbook = nil if @playbook == UNSET_VALUE + @force_remote_user = true if @force_remote_user != false @extra_vars = nil if @extra_vars == UNSET_VALUE @inventory_path = nil if @inventory_path == UNSET_VALUE @ask_sudo_pass = false unless @ask_sudo_pass == true @@ -56,7 +59,7 @@ module VagrantPlugins @tags = nil if @tags == UNSET_VALUE @skip_tags = nil if @skip_tags == UNSET_VALUE @start_at_task = nil if @start_at_task == UNSET_VALUE - @groups = {} if @groups == UNSET_VALUE + @groups = {} if @groups == UNSET_VALUE @host_key_checking = false unless @host_key_checking == true @raw_arguments = nil if @raw_arguments == UNSET_VALUE @raw_ssh_args = nil if @raw_ssh_args == UNSET_VALUE diff --git a/plugins/provisioners/ansible/provisioner.rb b/plugins/provisioners/ansible/provisioner.rb index 6dd1313e0..296345b39 100644 --- a/plugins/provisioners/ansible/provisioner.rb +++ b/plugins/provisioners/ansible/provisioner.rb @@ -20,13 +20,10 @@ module VagrantPlugins # Ansible provisioner options # - # By default, connect with Vagrant SSH username - options = %W[--user=#{@ssh_info[:username]}] - # Connect with native OpenSSH client # Other modes (e.g. paramiko) are not officially supported, # but can be enabled via raw_arguments option. - options << "--connection=ssh" + options = %W[--connection=ssh] # Increase the SSH connection timeout, as the Ansible default value (10 seconds) # is a bit demanding for some overloaded developer boxes. This is particularly @@ -34,6 +31,16 @@ module VagrantPlugins # is not controlled during vagrant boot process. options << "--timeout=30" + 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. + options << "--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) + options << "--extra-vars=ansible_ssh_user='#{@ssh_info[:username]}'" + end + # By default we limit by the current machine, but # this can be overridden by the `limit` option. if config.limit @@ -127,7 +134,11 @@ module VagrantPlugins m = @machine.env.machine(*am) m_ssh_info = m.ssh_info if !m_ssh_info.nil? - inventory += "#{m.name} ansible_ssh_host=#{m_ssh_info[:host]} ansible_ssh_port=#{m_ssh_info[:port]} ansible_ssh_private_key_file='#{m_ssh_info[:private_key_path][0]}'\n" + forced_ssh_user = "" + if config.force_remote_user + forced_ssh_user = "ansible_ssh_user='#{m_ssh_info[:username]}' " + end + inventory += "#{m.name} ansible_ssh_host=#{m_ssh_info[:host]} ansible_ssh_port=#{m_ssh_info[:port]} #{forced_ssh_user}ansible_ssh_private_key_file='#{m_ssh_info[:private_key_path][0]}'\n" inventory_machines[m.name] = m else @logger.error("Auto-generated inventory: Impossible to get SSH information for machine '#{m.name} (#{m.provider_name})'. This machine should be recreated.") diff --git a/test/unit/plugins/provisioners/ansible/config_test.rb b/test/unit/plugins/provisioners/ansible/config_test.rb index ab1b6dca0..fdfc54c0a 100644 --- a/test/unit/plugins/provisioners/ansible/config_test.rb +++ b/test/unit/plugins/provisioners/ansible/config_test.rb @@ -18,6 +18,7 @@ describe VagrantPlugins::Ansible::Config do supported_options = %w( ask_sudo_pass ask_vault_pass extra_vars + force_remote_user groups host_key_checking inventory_path @@ -41,6 +42,7 @@ describe VagrantPlugins::Ansible::Config do expect(subject.playbook).to be_nil expect(subject.extra_vars).to be_nil + expect(subject.force_remote_user).to be_true expect(subject.ask_sudo_pass).to be_false expect(subject.ask_vault_pass).to be_false expect(subject.vault_password_file).to be_nil @@ -57,6 +59,9 @@ describe VagrantPlugins::Ansible::Config do expect(subject.raw_ssh_args).to be_nil end + describe "force_remote_user option" do + it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :force_remote_user, true + end describe "host_key_checking option" do it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :host_key_checking, false end diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index 24863adc1..2dce27611 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -67,15 +67,17 @@ VF # def self.it_should_set_arguments_and_environment_variables( - expected_args_count = 6, expected_vars_count = 4, expected_host_key_checking = false, expected_transport_mode = "ssh") + expected_args_count = 5, + expected_vars_count = 4, + expected_host_key_checking = false, + expected_transport_mode = "ssh") it "sets implicit arguments in a specific order" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| expect(args[0]).to eq("ansible-playbook") - expect(args[1]).to eq("--user=#{machine.ssh_info[:username]}") - expect(args[2]).to eq("--connection=ssh") - expect(args[3]).to eq("--timeout=30") + expect(args[1]).to eq("--connection=ssh") + expect(args[2]).to eq("--timeout=30") inventory_count = args.count { |x| x =~ /^--inventory-file=.+$/ } expect(inventory_count).to be > 0 @@ -162,13 +164,17 @@ VF end end - def self.it_should_create_and_use_generated_inventory + def self.it_should_create_and_use_generated_inventory(with_ssh_user = true) it "generates an inventory with all active machines" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| expect(config.inventory_path).to be_nil expect(File.exists?(generated_inventory_file)).to be_true inventory_content = File.read(generated_inventory_file) - 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") + 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") + 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") + 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") } end @@ -276,7 +282,7 @@ VF config.host_key_checking = true end - it_should_set_arguments_and_environment_variables 6, 4, true + it_should_set_arguments_and_environment_variables 5, 4, true end describe "with boolean (flag) options disabled" do @@ -288,7 +294,7 @@ VF config.sudo_user = 'root' end - it_should_set_arguments_and_environment_variables 7 + it_should_set_arguments_and_environment_variables 6 it_should_set_optional_arguments({ "sudo_user" => "--sudo-user=root" }) it "it does not set boolean flag when corresponding option is set to false" do @@ -303,6 +309,7 @@ VF describe "with raw_arguments option" do before do config.sudo = false + config.force_remote_user = false config.skip_tags = %w(foo bar) config.limit = "all" config.raw_arguments = ["--connection=paramiko", @@ -352,12 +359,29 @@ VF it_should_set_arguments_and_environment_variables end + context "with force_remote_user option disabled" do + before do + config.force_remote_user = false + end + + it_should_create_and_use_generated_inventory false # i.e. without setting ansible_ssh_user in inventory + + 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 { |*args| + expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") + expect(args).to include("--user=#{machine.ssh_info[:username]}") + } + end + end + describe "with inventory_path option" do before do config.inventory_path = existing_file end - it_should_set_arguments_and_environment_variables + 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 { |*args| @@ -366,6 +390,26 @@ VF expect(File.exists?(generated_inventory_file)).to be_false } end + + it "uses an --extra-vars argument to force ansible_ssh_user parameter" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + expect(args).not_to include("--user=#{machine.ssh_info[:username]}") + expect(args).to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") + } + end + + describe "with force_remote_user option disabled" do + before do + config.force_remote_user = false + end + + it "uses a --user argument to set a default remote user" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") + expect(args).to include("--user=#{machine.ssh_info[:username]}") + } + end + end end describe "with ask_vault_pass option" do @@ -373,7 +417,7 @@ VF config.ask_vault_pass = true end - it_should_set_arguments_and_environment_variables 7 + it_should_set_arguments_and_environment_variables 6 it "should ask the vault password" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| @@ -387,7 +431,7 @@ VF config.vault_password_file = existing_file end - it_should_set_arguments_and_environment_variables 7 + it_should_set_arguments_and_environment_variables 6 it "uses the given vault password file" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| @@ -401,7 +445,7 @@ VF config.raw_ssh_args = ['-o ControlMaster=no', '-o ForwardAgent=no'] end - it_should_set_arguments_and_environment_variables 6, 4 + it_should_set_arguments_and_environment_variables it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "passes custom SSH options via ANSIBLE_SSH_ARGS with the highest priority" do @@ -435,7 +479,7 @@ VF ssh_info[:private_key_path] = ['/path/to/my/key', '/an/other/identity', '/yet/an/other/key'] end - it_should_set_arguments_and_environment_variables 6, 4 + it_should_set_arguments_and_environment_variables it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "passes additional Identity Files via ANSIBLE_SSH_ARGS" do @@ -452,7 +496,7 @@ VF ssh_info[:forward_agent] = true end - it_should_set_arguments_and_environment_variables 6, 4 + it_should_set_arguments_and_environment_variables it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "enables SSH-Forwarding via ANSIBLE_SSH_ARGS" do @@ -468,12 +512,12 @@ VF config.verbose = 'v' end - it_should_set_arguments_and_environment_variables 7 + it_should_set_arguments_and_environment_variables 6 it_should_set_optional_arguments({ "verbose" => "-v" }) it "shows the ansible-playbook command" do expect(machine.env.ui).to receive(:detail).with { |full_command| - expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_FORCE_COLOR=true ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --user=testuser --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} -v playbook.yml") + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_FORCE_COLOR=true ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} -v playbook.yml") } end end @@ -520,7 +564,7 @@ VF config.raw_ssh_args = ['-o ControlMaster=no'] end - it_should_set_arguments_and_environment_variables 21, 4, true + it_should_set_arguments_and_environment_variables 20, 4, true it_should_explicitly_enable_ansible_ssh_control_persist_defaults it_should_set_optional_arguments({ "extra_vars" => "--extra-vars=@#{File.expand_path(__FILE__)}", "sudo" => "--sudo", @@ -547,7 +591,7 @@ VF it "shows the ansible-playbook command, with additional quotes when required" do expect(machine.env.ui).to receive(:detail).with { |full_command| - expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_FORCE_COLOR=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 --user=testuser --connection=ssh --timeout=30 --limit='machine*:&vagrant:!that_one' --inventory-file=#{generated_inventory_dir} --extra-vars=@#{File.expand_path(__FILE__)} --sudo --sudo-user=deployer -vvv --ask-sudo-pass --ask-vault-pass --vault-password-file=#{File.expand_path(__FILE__)} --tags=db,www --skip-tags=foo,bar --start-at-task='an awesome task' --why-not --su-user=foot --ask-su-pass --limit='all' --private-key=./myself.key playbook.yml") + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_FORCE_COLOR=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 --limit='machine*:&vagrant:!that_one' --inventory-file=#{generated_inventory_dir} --extra-vars=@#{File.expand_path(__FILE__)} --sudo --sudo-user=deployer -vvv --ask-sudo-pass --ask-vault-pass --vault-password-file=#{File.expand_path(__FILE__)} --tags=db,www --skip-tags=foo,bar --start-at-task='an awesome task' --why-not --su-user=foot --ask-su-pass --limit='all' --private-key=./myself.key playbook.yml") } end end diff --git a/website/docs/source/v2/provisioning/ansible.html.md b/website/docs/source/v2/provisioning/ansible.html.md index 381cf5bd1..719e893fe 100644 --- a/website/docs/source/v2/provisioning/ansible.html.md +++ b/website/docs/source/v2/provisioning/ansible.html.md @@ -61,8 +61,8 @@ Vagrant would generate an inventory file that might look like: ``` # Generated by Vagrant -machine1 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2200 ansible_ssh_private_key_file=/home/.../.vagrant/machines/machine1/virtualbox/private_key -machine2 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2201 ansible_ssh_private_key_file=/home/.../.vagrant/machines/machine2/virtualbox/private_key +machine1 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2200 ansible_ssh_user='vagrant' ansible_ssh_private_key_file='/home/.../'.vagrant/machines/machine1/virtualbox/private_key +machine2 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2201 ansible_ssh_user='vagrant' ansible_ssh_private_key_file='/home/.../'.vagrant/machines/machine2/virtualbox/private_key [group1] machine1 @@ -80,6 +80,7 @@ group2 * The generation of group variables blocks (e.g. `[group1:vars]`) are intentionally not supported, as it is [not recommended to store group variables in the main inventory file](http://docs.ansible.com/intro_inventory.html#splitting-out-host-and-group-specific-data). A good practice is to store these group (or host) variables in `YAML` files stored in `group_vars/` or `host_vars/` directories in the playbook (or inventory) directory. * Unmanaged machines and undefined groups are not added to the inventory, to avoid useless Ansible errors (e.g. *unreachable host* or *undefined child group*) * Prior to Vagrant 1.7.3, the `ansible_ssh_private_key_file` variable was not set in generated inventory, but passed as command line argument to `ansible-playbook` command. + * Prior to Vagrant 1.8.0, the `ansible_ssh_user` variable was not set in generated inventory, but passed as command line argument to `ansible-playbook` command. See also the `force_remote_user` option to enable the former behavior. For example, `machine3`, `group3` and `group1:vars` in the example below would not be added to the generated inventory file: @@ -220,6 +221,7 @@ by the sudo command. * `ansible.raw_arguments` can be set to an array of strings corresponding to a list of `ansible-playbook` arguments (e.g. `['--check', '-M /my/modules']`). It is an *unsafe wildcard* that can be used to apply Ansible options that are not (yet) supported by this Vagrant provisioner. As of Vagrant 1.7, `raw_arguments` has the highest priority and its values can potentially override or break other Vagrant settings. * `ansible.raw_ssh_args` can be set to an array of strings corresponding to a list of OpenSSH client parameters (e.g. `['-o ControlMaster=no']`). It is an *unsafe wildcard* that can be used to pass additional SSH settings to Ansible via `ANSIBLE_SSH_ARGS` environment variable. * `ansible.host_key_checking` can be set to `true` which will enable host key checking. As of Vagrant 1.5, the default value is `false` and as of Vagrant 1.7 the user known host file (e.g. `~/.ssh/known_hosts`) is no longer read nor modified. In other words: by default, the Ansible provisioner behaves the same as Vagrant native commands (e.g `vagrant ssh`). +* `ansible.force_remote_user` can be set to `false` which will enable the `remote_user` parameters of your Ansible plays or tasks. Otherwise, Vagrant will set the `ansible_ssh_user` setting in the generated inventory, or as an extra variable when a static inventory is used. In this case, all the Ansible `remote_user` parameters will be overridden by the value of `config.ssh.username` of the [Vagrant SSH Settings](/v2/vagrantfile/ssh_settings.html). ## Tips and Tricks @@ -273,31 +275,6 @@ As `ansible-playbook` command looks for local `ansible.cfg` configuration file i Note that it is also possible to reference an Ansible configuration file via `ANSIBLE_CONFIG` environment variable, if you want to be flexible about the location of this file. -### Why does the Ansible provisioner connect as the wrong user? - -It is good to know that the following Ansible settings always override the `config.ssh.username` option defined in [Vagrant SSH Settings](/v2/vagrantfile/ssh_settings.html): - -* `ansible_ssh_user` variable -* `remote_user` (or `user`) play attribute -* `remote_user` task attribute - -Be aware that copying snippets from the Ansible documentation might lead to this problem, as `root` is used as the remote user in many [examples](http://docs.ansible.com/playbooks_intro.html#hosts-and-users). - -Example of an SSH error (with `vvv` log level), where an undefined remote user `xyz` has replaced `vagrant`: - -``` -TASK: [my_role | do something] ***************** -<127.0.0.1> ESTABLISH CONNECTION FOR USER: xyz -<127.0.0.1> EXEC ['ssh', '-tt', '-vvv', '-o', 'ControlMaster=auto',... -fatal: [ansible-devbox] => SSH encountered an unknown error. We recommend you re-run the command using -vvvv, which will enable SSH debugging output to help diagnose the issue. -``` - -In a situation like the above, to override the `remote_user` specified in a play you can use the following line in your Vagrantfile `vm.provision` block: - -``` -ansible.extra_vars = { ansible_ssh_user: 'vagrant' } -``` - ### Force Paramiko Connection Mode The Ansible provisioner is implemented with native OpenSSH support in mind, and there is no official support for [paramiko](https://github.com/paramiko/paramiko/) (A native Python SSHv2 protocol library). From 84408c1682a9c5cb2915c2bc0f1a36510bfe3401 Mon Sep 17 00:00:00 2001 From: Aneesh Agrawal Date: Mon, 2 Nov 2015 20:34:14 -0500 Subject: [PATCH 144/484] Enable log_level and colorize for salt masterless --- plugins/provisioners/salt/provisioner.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/provisioners/salt/provisioner.rb b/plugins/provisioners/salt/provisioner.rb index 72365776e..84bf17e7c 100644 --- a/plugins/provisioners/salt/provisioner.rb +++ b/plugins/provisioners/salt/provisioner.rb @@ -329,11 +329,10 @@ module VagrantPlugins def call_masterless @machine.env.ui.info "Calling state.highstate in local mode... (this may take a while)" - cmd = "salt-call state.highstate --local" + cmd = "salt-call state.highstate --local#{get_loglevel}#{get_colorize}#{get_pillar}" if @config.minion_id cmd += " --id #{@config.minion_id}" end - cmd += " -l debug#{get_pillar}" @machine.communicate.sudo(cmd) do |type, data| if @config.verbose @machine.env.ui.info(data) From 879977832cb4161638eb3bcf125eb05c495da48a Mon Sep 17 00:00:00 2001 From: Eric Winkelmann Date: Mon, 2 Nov 2015 23:39:06 -0800 Subject: [PATCH 145/484] Prefer xfreerdp for RDP connections on Linux hosts. Rather than only using rdesktop (which does not work properly with newer versions of RDP), use xfreerdp if available and fall back to rdesktop if not. --- plugins/hosts/linux/cap/rdp.rb | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/plugins/hosts/linux/cap/rdp.rb b/plugins/hosts/linux/cap/rdp.rb index 7146ef890..61f231f7b 100644 --- a/plugins/hosts/linux/cap/rdp.rb +++ b/plugins/hosts/linux/cap/rdp.rb @@ -5,17 +5,35 @@ module VagrantPlugins module Cap class RDP def self.rdp_client(env, rdp_info) - if !Vagrant::Util::Which.which("rdesktop") - raise Vagrant::Errors::LinuxRDesktopNotFound - end + # Detect if an RDP client is available. + # Prefer xfreerdp as it supports newer versions of RDP. + rdp_client = + if Vagrant::Util::Which.which("xfreerdp") + "xfreerdp" + elsif Vagrant::Util::Which.which("rdesktop") + "rdesktop" + else + raise Vagrant::Errors::LinuxRDesktopNotFound + end args = [] - args << "-u" << rdp_info[:username] - args << "-p" << rdp_info[:password] if rdp_info[:password] - args += rdp_info[:extra_args] if rdp_info[:extra_args] - args << "#{rdp_info[:host]}:#{rdp_info[:port]}" - Vagrant::Util::Subprocess.execute("rdesktop", *args) + # Build appropriate arguments for the RDP client. + case rdp_client + when "xfreerdp" + args << "/u:#{rdp_info[:username]}" + args << "/p:#{rdp_info[:password]}" if rdp_info[:password] + args << "/v:#{rdp_info[:host]}:#{rdp_info[:port]}" + args += rdp_info[:extra_args] if rdp_info[:extra_args] + when "rdesktop" + args << "-u" << rdp_info[:username] + args << "-p" << rdp_info[:password] if rdp_info[:password] + args += rdp_info[:extra_args] if rdp_info[:extra_args] + args << "#{rdp_info[:host]}:#{rdp_info[:port]}" + end + + # Finally, run the client. + Vagrant::Util::Subprocess.execute(rdp_client, *args) end end end From e687f81fce3ae12d8d89a6ca11811ca2c31b2c7a Mon Sep 17 00:00:00 2001 From: Eric Winkelmann Date: Mon, 2 Nov 2015 23:42:01 -0800 Subject: [PATCH 146/484] Re-word Linux RDP error to include `xfreerdp`. Changed the name of the error LinuxRDesktopNotFound to LinuxRDPClientNotFound and re-worded error text in templates/locales/en.yml to include `xfreerdp` when listing supported RDP clients. --- lib/vagrant/errors.rb | 4 ++-- plugins/hosts/linux/cap/rdp.rb | 2 +- templates/locales/en.yml | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 906b8d9bc..aee0f3472 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -408,8 +408,8 @@ module Vagrant error_key(:linux_nfs_mount_failed) end - class LinuxRDesktopNotFound < VagrantError - error_key(:linux_rdesktop_not_found) + class LinuxRDPClientNotFound < VagrantError + error_key(:linux_rdp_client_not_found) end class LocalDataDirectoryNotAccessible < VagrantError diff --git a/plugins/hosts/linux/cap/rdp.rb b/plugins/hosts/linux/cap/rdp.rb index 61f231f7b..76476fc9b 100644 --- a/plugins/hosts/linux/cap/rdp.rb +++ b/plugins/hosts/linux/cap/rdp.rb @@ -13,7 +13,7 @@ module VagrantPlugins elsif Vagrant::Util::Which.which("rdesktop") "rdesktop" else - raise Vagrant::Errors::LinuxRDesktopNotFound + raise Vagrant::Errors::LinuxRDPClientNotFound end args = [] diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 9e8c680aa..8dce0806b 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -818,10 +818,11 @@ en: that the NFS client software is properly installed, and consult any resources specific to the linux distro you're using for more information on how to do this. - linux_rdesktop_not_found: |- - The `rdesktop` application was not found. Vagrant requires this - in order to connect via RDP to the Vagrant environment. Please ensure - this application is installed and available on the path and try again. + linux_rdp_client_not_found: |- + An appropriate RDP client was not found. Vagrant requires either + `xfreerdp` or `rdesktop` in order to connect via RDP to the Vagrant + environment. Please ensure one of these applications is installed and + available on the path and try again. machine_action_locked: |- An action '%{action}' was attempted on the machine '%{name}', but another process is already executing an action on the machine. From c60e1161215592d8098776d0bb024647d29eac64 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Tue, 3 Nov 2015 17:16:31 +0200 Subject: [PATCH 147/484] Remove redundant app call from action "handle_forwarded_port_collisions" This call should not be in the `handle` helper method. It is specified in the `call` method already. --- lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb b/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb index b53ba35c2..e9ea90493 100644 --- a/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb +++ b/lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb @@ -156,8 +156,6 @@ module Vagrant new_port: repaired_port.to_s)) end end - - @app.call(env) end def lease_check(port) From d4ddb3c2f3356b4dd7392ee7d177da409545fea3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Nov 2015 14:20:48 -0800 Subject: [PATCH 148/484] commands/provider --- lib/vagrant/errors.rb | 4 ++ plugins/commands/provider/command.rb | 69 ++++++++++++++++++++++++++++ plugins/commands/provider/plugin.rb | 18 ++++++++ templates/locales/en.yml | 5 ++ 4 files changed, 96 insertions(+) create mode 100644 plugins/commands/provider/command.rb create mode 100644 plugins/commands/provider/plugin.rb diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 906b8d9bc..f46491604 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -520,6 +520,10 @@ module Vagrant error_key(:requires_directory, "vagrant.actions.general.package") end + class ProviderCantInstall < VagrantError + error_key(:provider_cant_install) + end + class ProviderNotFound < VagrantError error_key(:provider_not_found) end diff --git a/plugins/commands/provider/command.rb b/plugins/commands/provider/command.rb new file mode 100644 index 000000000..791a82246 --- /dev/null +++ b/plugins/commands/provider/command.rb @@ -0,0 +1,69 @@ +require 'optparse' + +module VagrantPlugins + module CommandProvider + class Command < Vagrant.plugin("2", :command) + def self.synopsis + "show provider for this environment" + end + + def execute + options = {} + options[:install] = false + options[:usable] = false + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant provider [options] [args]" + o.separator "" + o.separator "This command interacts with the provider for this environment." + o.separator "With no arguments, it'll output the default provider for this" + o.separator "environment." + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("--install", "Installs the provider if possible") do |f| + options[:install] = f + end + + o.on("--usable", "Checks if the named provider is usable") do |f| + options[:usable] = f + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + # Get the machine + machine = nil + with_target_vms(argv, single_target: true) do |m| + machine = m + end + + # Output some machine readable stuff + @env.ui.machine("provider-name", machine.provider_name, target: machine.name.to_s) + + # Check if we're just doing a usability check + if options[:usable] + puts machine.provider_name.to_s + return 0 if machine.provider.class.usable?(false) + return 1 + end + + # Check if we're requesting installation + if options[:install] + if !machine.provider.capability?(:install) + raise Vagrant::Errors::ProviderCantInstall, + provider: machine.provider_name.to_s + end + + machine.provider.capability(:install) + end + + # Success, exit status 0 + 0 + end + end + end +end diff --git a/plugins/commands/provider/plugin.rb b/plugins/commands/provider/plugin.rb new file mode 100644 index 000000000..173088864 --- /dev/null +++ b/plugins/commands/provider/plugin.rb @@ -0,0 +1,18 @@ +require "vagrant" + +module VagrantPlugins + module CommandProvider + class Plugin < Vagrant.plugin("2") + name "provider command" + description <<-DESC + The `provider` command is used to interact with the various providers + that are installed with Vagrant. + DESC + + command("provider", primary: false) do + require_relative "command" + Command + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 9e8c680aa..eee943b1c 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1001,6 +1001,11 @@ en: A file or directory you're attempting to include with your packaged box has symlinks in it. Vagrant cannot include symlinks in the resulting package. Please remove the symlinks and try again. + provider_cant_install: |- + The provider '%{provider}' doesn't support automatic installation. + This is a limitation of this provider. Please report this as a feature + request to the provider in question. To install this provider, you'll + have to do so manually. provider_not_found: |- The provider '%{provider}' could not be found, but was requested to back the machine '%{machine}'. Please use a provider that exists. From 72e13ee9efe9109e16eee6c8af2114f6282714ca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Nov 2015 14:26:22 -0800 Subject: [PATCH 149/484] test: add tests for provider command --- plugins/commands/provider/command.rb | 6 ++- .../plugins/commands/provider/command_test.rb | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/unit/plugins/commands/provider/command_test.rb diff --git a/plugins/commands/provider/command.rb b/plugins/commands/provider/command.rb index 791a82246..401c8be03 100644 --- a/plugins/commands/provider/command.rb +++ b/plugins/commands/provider/command.rb @@ -46,7 +46,7 @@ module VagrantPlugins # Check if we're just doing a usability check if options[:usable] - puts machine.provider_name.to_s + @env.ui.output(machine.provider_name.to_s) return 0 if machine.provider.class.usable?(false) return 1 end @@ -59,8 +59,12 @@ module VagrantPlugins end machine.provider.capability(:install) + return end + # No subtask, just output the provider name + @env.ui.output(machine.provider_name.to_s) + # Success, exit status 0 0 end diff --git a/test/unit/plugins/commands/provider/command_test.rb b/test/unit/plugins/commands/provider/command_test.rb new file mode 100644 index 000000000..c1dae2923 --- /dev/null +++ b/test/unit/plugins/commands/provider/command_test.rb @@ -0,0 +1,47 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/provider/command") + +describe VagrantPlugins::CommandProvider::Command do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:guest) { double("guest") } + let(:host) { double("host") } + let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + + let(:argv) { [] } + + subject { described_class.new(argv, iso_env) } + + before do + allow(subject).to receive(:with_target_vms) { |&block| block.call machine } + end + + describe "execute" do + context "no arguments" do + it "exits with the provider name" do + expect(subject.execute).to eq(0) + end + end + + context "--usable" do + let(:argv) { ["--usable"] } + + it "exits 0 if it is usable" do + expect(subject.execute).to eq(0) + end + + it "exits 1 if it is not usable" do + expect(machine.provider.class).to receive(:usable?).and_return(false) + expect(subject.execute).to eq(1) + end + end + end +end From dad5962ebb5584ce77c9ef3d055c34aa64d70caf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Nov 2015 15:47:56 -0800 Subject: [PATCH 150/484] hosts/darwin: support virtualbox install --- lib/vagrant/errors.rb | 4 ++ lib/vagrant/ui.rb | 3 +- plugins/commands/provider/command.rb | 5 +- .../darwin/cap/provider_install_virtualbox.rb | 51 +++++++++++++++++++ plugins/hosts/darwin/plugin.rb | 5 ++ .../darwin/scripts/install_virtualbox.sh | 15 ++++++ templates/locales/en.yml | 24 +++++++++ 7 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 plugins/hosts/darwin/cap/provider_install_virtualbox.rb create mode 100755 plugins/hosts/darwin/scripts/install_virtualbox.sh diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index f46491604..c76f3cbe9 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -524,6 +524,10 @@ module Vagrant error_key(:provider_cant_install) end + class ProviderInstallFailed < VagrantError + error_key(:provider_install_failed) + end + class ProviderNotFound < VagrantError error_key(:provider_not_found) end diff --git a/lib/vagrant/ui.rb b/lib/vagrant/ui.rb index fb5929f32..c610643a9 100644 --- a/lib/vagrant/ui.rb +++ b/lib/vagrant/ui.rb @@ -319,6 +319,7 @@ module Vagrant target = @prefix target = opts[:target] if opts.key?(:target) + target = "#{target}:" if target != "" # Get the lines. The first default is because if the message # is an empty string, then we want to still use the empty string. @@ -327,7 +328,7 @@ module Vagrant # Otherwise, make sure to prefix every line properly lines.map do |line| - "#{prefix}#{target}: #{line}" + "#{prefix}#{target} #{line}" end.join("\n") end end diff --git a/plugins/commands/provider/command.rb b/plugins/commands/provider/command.rb index 401c8be03..7fd0ef743 100644 --- a/plugins/commands/provider/command.rb +++ b/plugins/commands/provider/command.rb @@ -53,12 +53,13 @@ module VagrantPlugins # Check if we're requesting installation if options[:install] - if !machine.provider.capability?(:install) + key = "provider_install_#{machine.provider_name}".to_sym + if !@env.host.capability?(key) raise Vagrant::Errors::ProviderCantInstall, provider: machine.provider_name.to_s end - machine.provider.capability(:install) + @env.host.capability(key) return end diff --git a/plugins/hosts/darwin/cap/provider_install_virtualbox.rb b/plugins/hosts/darwin/cap/provider_install_virtualbox.rb new file mode 100644 index 000000000..c4748eecb --- /dev/null +++ b/plugins/hosts/darwin/cap/provider_install_virtualbox.rb @@ -0,0 +1,51 @@ +require "pathname" +require "tempfile" + +require "vagrant/util/downloader" +require "vagrant/util/subprocess" + +module VagrantPlugins + module HostDarwin + module Cap + class ProviderInstallVirtualBox + # The URL to download VirtualBox is hardcoded so we can have a + # known-good version to download. + URL = "http://download.virtualbox.org/virtualbox/5.0.8/VirtualBox-5.0.8-103449-OSX.dmg".freeze + VERSION = "5.0.8".freeze + + def self.provider_install_virtualbox(env) + tf = Tempfile.new("vagrant") + tf.close + + # Prefixed UI for prettiness + ui = Vagrant::UI::Prefixed.new(env.ui, "") + + # Start by downloading the file using the standard mechanism + ui.output(I18n.t( + "vagrant.hosts.darwin.virtualbox_install_download", + version: VERSION)) + ui.detail(I18n.t( + "vagrant.hosts.darwin.virtualbox_install_detail")) + dl = Vagrant::Util::Downloader.new(URL, tf.path, ui: ui) + dl.download! + + # Launch it + ui.output(I18n.t( + "vagrant.hosts.darwin.virtualbox_install_install")) + ui.detail(I18n.t( + "vagrant.hosts.darwin.virtualbox_install_install_detail")) + script = File.expand_path("../../scripts/install_virtualbox.sh", __FILE__) + result = Vagrant::Util::Subprocess.execute("bash", script, tf.path) + if result.exit_code != 0 + raise Vagrant::Errors::ProviderInstallFailed, + provider: "virtualbox", + stdout: result.stdout, + stderr: result.stderr + end + + ui.success(I18n.t("vagrant.hosts.darwin.virtualbox_install_success")) + end + end + end + end +end diff --git a/plugins/hosts/darwin/plugin.rb b/plugins/hosts/darwin/plugin.rb index 226a5bae1..f37bcd9dd 100644 --- a/plugins/hosts/darwin/plugin.rb +++ b/plugins/hosts/darwin/plugin.rb @@ -11,6 +11,11 @@ module VagrantPlugins Host end + host_capability("darwin", "provider_install_virtualbox") do + require_relative "cap/provider_install_virtualbox" + Cap::ProviderInstallVirtualBox + end + host_capability("darwin", "rdp_client") do require_relative "cap/rdp" Cap::RDP diff --git a/plugins/hosts/darwin/scripts/install_virtualbox.sh b/plugins/hosts/darwin/scripts/install_virtualbox.sh new file mode 100755 index 000000000..1ac0da126 --- /dev/null +++ b/plugins/hosts/darwin/scripts/install_virtualbox.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +hdiutil attach $1 +cd /Volumes/VirtualBox/ +sudo installer -pkg VirtualBox.pkg -target "/" +cd /tmp +flag=1 +while [ $flag -ne 0 ]; do + sleep 1 + set +e + hdiutil detach /Volumes/VirtualBox/ + flag=$? + set -e +done diff --git a/templates/locales/en.yml b/templates/locales/en.yml index eee943b1c..908af4c4c 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -280,6 +280,7 @@ en: CHANGELOG below: https://github.com/mitchellh/vagrant/blob/v%{version}/CHANGELOG.md + cfengine_config: classes_array: |- The 'classes' configuration must be an array. @@ -1006,6 +1007,14 @@ en: This is a limitation of this provider. Please report this as a feature request to the provider in question. To install this provider, you'll have to do so manually. + provider_install_failed: |- + Installation of the provider '%{provider}' failed! The stdout + and stderr are shown below. Please read the error output, resolve it, + and try again. If problem persists, please install the provider + manually. + + Stdout: %{stdout} + Stderr: %{stderr} provider_not_found: |- The provider '%{provider}' could not be found, but was requested to back the machine '%{machine}'. Please use a provider that exists. @@ -1867,6 +1876,21 @@ en: Preparing to edit /etc/exports. Administrator privileges will be required... nfs_prune: |- Pruning invalid NFS exports. Administrator privileges will be required... + darwin: + virtualbox_install_download: |- + Downloading VirtualBox %{version}... + virtualbox_install_detail: |- + This may not be the latest version of VirtualBox, but it is a version + that is known to work well. Over time, we'll update the version that + is installed. + virtualbox_install_install: |- + Installing VirtualBox. This will take a few minutes... + virtualbox_install_install_detail: |- + You may be asked for your administrator password during this time. + If you're uncomfortable entering your password here, please install + VirtualBox manually. + virtualbox_install_success: |- + VirtualBox has successfully been installed! linux: nfs_export: |- Preparing to edit /etc/exports. Administrator privileges will be required... From abb1030f10f1235d24dac03be34952c5163f7f86 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Nov 2015 19:37:55 -0800 Subject: [PATCH 151/484] hosts/windows: install VirtualBox --- .../cap/provider_install_virtualbox.rb | 51 +++++++++++++++++++ plugins/hosts/windows/plugin.rb | 5 ++ .../windows/scripts/install_virtualbox.ps1 | 37 ++++++++++++++ templates/locales/en.yml | 16 ++++++ 4 files changed, 109 insertions(+) create mode 100644 plugins/hosts/windows/cap/provider_install_virtualbox.rb create mode 100644 plugins/hosts/windows/scripts/install_virtualbox.ps1 diff --git a/plugins/hosts/windows/cap/provider_install_virtualbox.rb b/plugins/hosts/windows/cap/provider_install_virtualbox.rb new file mode 100644 index 000000000..160a6bf5d --- /dev/null +++ b/plugins/hosts/windows/cap/provider_install_virtualbox.rb @@ -0,0 +1,51 @@ +require "pathname" +require "tempfile" + +require "vagrant/util/downloader" +require "vagrant/util/subprocess" + +module VagrantPlugins + module HostWindows + module Cap + class ProviderInstallVirtualBox + # The URL to download VirtualBox is hardcoded so we can have a + # known-good version to download. + URL = "http://download.virtualbox.org/virtualbox/5.0.8/VirtualBox-5.0.8-103449-Win.exe".freeze + VERSION = "5.0.8".freeze + + def self.provider_install_virtualbox(env) + tf = Tempfile.new("vagrant") + tf.close + + # Prefixed UI for prettiness + ui = Vagrant::UI::Prefixed.new(env.ui, "") + + # Start by downloading the file using the standard mechanism + ui.output(I18n.t( + "vagrant.hosts.windows.virtualbox_install_download", + version: VERSION)) + ui.detail(I18n.t( + "vagrant.hosts.windows.virtualbox_install_detail")) + dl = Vagrant::Util::Downloader.new(URL, tf.path, ui: ui) + dl.download! + + # Launch it + ui.output(I18n.t( + "vagrant.hosts.windows.virtualbox_install_install")) + ui.detail(I18n.t( + "vagrant.hosts.windows.virtualbox_install_install_detail")) + script = File.expand_path("../../scripts/install_virtualbox.ps1", __FILE__) + result = Vagrant::Util::Powershell.execute(script, tf.path) + if result.exit_code != 0 + raise Vagrant::Errors::ProviderInstallFailed, + provider: "virtualbox", + stdout: result.stdout, + stderr: result.stderr + end + + ui.success(I18n.t("vagrant.hosts.windows.virtualbox_install_success")) + end + end + end + end +end diff --git a/plugins/hosts/windows/plugin.rb b/plugins/hosts/windows/plugin.rb index fe3b28923..d441f229f 100644 --- a/plugins/hosts/windows/plugin.rb +++ b/plugins/hosts/windows/plugin.rb @@ -11,6 +11,11 @@ module VagrantPlugins Host end + host_capability("windows", "provider_install_virtualbox") do + require_relative "cap/provider_install_virtualbox" + Cap::ProviderInstallVirtualBox + end + host_capability("windows", "nfs_installed") do require_relative "cap/nfs" Cap::NFS diff --git a/plugins/hosts/windows/scripts/install_virtualbox.ps1 b/plugins/hosts/windows/scripts/install_virtualbox.ps1 new file mode 100644 index 000000000..82a73d4b6 --- /dev/null +++ b/plugins/hosts/windows/scripts/install_virtualbox.ps1 @@ -0,0 +1,37 @@ +Param( + [Parameter(Mandatory=$True)] + [string]$path +) + +# Stop on first error +$ErrorActionPreference = "Stop" + +# Make the path complete +$path = Resolve-Path $path + +# Determine if this is a 64-bit or 32-bit CPU +$architecture="x86" +if ((Get-WmiObject -Class Win32_OperatingSystem).OSArchitecture -eq "64-bit") { + $architecture = "amd64" +} + +# Extract the contents of the installer +Start-Process -FilePath $path ` + -ArgumentList ('--extract','--silent','--path','.') ` + -Wait ` + -NoNewWindow + +# Find the installer +$matches = Get-ChildItem | Where-Object { $_.Name -match "VirtualBox-.*_$($architecture).msi" } +if ($matches.Count -ne 1) { + Write-Host "Multiple matches for VirtualBox MSI found: $($matches.Count)" + exit 1 +} +$installerPath = Resolve-Path $matches[0] + +# Run the installer +Start-Process -FilePath "$($env:systemroot)\System32\msiexec.exe" ` + -ArgumentList "/i `"$installerPath`" /qn /norestart /l*v `"$($pwd)\install.log`"" ` + -Verb RunAs ` + -Wait ` + -WorkingDirectory "$pwd" diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 908af4c4c..2bafd0f55 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1899,6 +1899,22 @@ en: arch: nfs_export: prepare: "Preparing to edit /etc/exports. Administrator privileges will be required..." + windows: + virtualbox_install_download: |- + Downloading VirtualBox %{version}... + virtualbox_install_detail: |- + This may not be the latest version of VirtualBox, but it is a version + that is known to work well. Over time, we'll update the version that + is installed. + virtualbox_install_install: |- + Installing VirtualBox. This will take a few minutes... + virtualbox_install_install_detail: |- + A couple pop-ups will occur during this installation process to + ask for admin privileges as well as to install Oracle drivers. + Please say yes to both. If you're uncomfortable with this, please + install VirtualBox manually. + virtualbox_install_success: |- + VirtualBox has successfully been installed! provisioners: chef: From ea66e22d2ed36501087eaa9b4bfe7bc4d8255421 Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Thu, 5 Nov 2015 18:55:34 +0200 Subject: [PATCH 152/484] Use UNC paths for shared folders on Windows host --- plugins/providers/virtualbox/driver/version_4_3.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index f895739d3..b77b2a4f9 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -610,6 +610,9 @@ module VagrantPlugins def share_folders(folders) folders.each do |folder| hostpath = folder[:hostpath] + if Vagrant::Util::Platform.windows? + hostpath = Vagrant::Util::Platform.windows_unc_path(hostpath) + end args = ["--name", folder[:name], "--hostpath", From 4a7aa83e58fb8269740e4783ea30a862af5c7124 Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Thu, 5 Nov 2015 18:56:24 +0200 Subject: [PATCH 153/484] Use UNC paths for shared folders on Windows host --- plugins/providers/virtualbox/driver/version_5_0.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index d13330c00..75a71c4f1 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -611,14 +611,9 @@ module VagrantPlugins def share_folders(folders) folders.each do |folder| hostpath = folder[:hostpath] - -=begin - # Removed for GH-5933 until further research. if Vagrant::Util::Platform.windows? hostpath = Vagrant::Util::Platform.windows_unc_path(hostpath) end -=end - args = ["--name", folder[:name], "--hostpath", From 69d9bc0fe85e65ec21ff6ee8d19b9b77da1ef7e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Nov 2015 11:50:10 -0800 Subject: [PATCH 154/484] Environment can_install_provider and install_provider --- lib/vagrant/environment.rb | 26 ++++++++++++ lib/vagrant/plugin/v2/provider.rb | 15 +++++++ test/unit/vagrant/environment_test.rb | 42 ++++++++++++++++++++ test/unit/vagrant/plugin/v2/provider_test.rb | 4 ++ 4 files changed, 87 insertions(+) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 015e4b95f..034d71163 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -383,6 +383,26 @@ module Vagrant raise Errors::NoDefaultProvider end + # Returns whether or not we know how to install the provider with + # the given name. + # + # @return [Boolean] + def can_install_provider?(name) + host.capability?(provider_install_key(name)) + end + + # Installs the provider with the given name. + # + # This will raise an exception if we don't know how to install the + # provider with the given name. You should guard this call with + # `can_install_provider?` for added safety. + # + # An exception will be raised if there are any failures installing + # the provider. + def install_provider(name) + host.capability(provider_install_key(name)) + end + # Returns the collection of boxes for the environment. # # @return [BoxCollection] @@ -883,6 +903,12 @@ module Vagrant nil end + # Returns the key used for the host capability for provider installs + # of the given name. + def provider_install_key(name) + "provider_install_#{name}".to_sym + end + # This upgrades a home directory that was in the v1.1 format to the # v1.5 format. It will raise exceptions if anything fails. def upgrade_home_path_v1_1 diff --git a/lib/vagrant/plugin/v2/provider.rb b/lib/vagrant/plugin/v2/provider.rb index 0ea7a1f13..3e96d86e1 100644 --- a/lib/vagrant/plugin/v2/provider.rb +++ b/lib/vagrant/plugin/v2/provider.rb @@ -24,6 +24,21 @@ module Vagrant true end + # This is called early, before a machine is instantiated, to check + # if this provider is installed. This should return true or false. + # + # If the provider is not installed and Vagrant determines it is + # able to install this provider, then it will do so. Installation + # is done by calling Environment.install_provider. + # + # If Environment.can_install_provider? returns false, then an error + # will be shown to the user. + def self.installed? + # By default return true for backwards compat so all providers + # continue to work. + true + end + # Initialize the provider to represent the given machine. # # @param [Vagrant::Machine] machine The machine that this provider diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index c5cf8370d..6a595fb57 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -25,6 +25,48 @@ describe Vagrant::Environment do let(:instance) { env.create_vagrant_env } subject { instance } + describe "#can_install_provider?" do + let(:plugin_hosts) { {} } + let(:plugin_host_caps) { {} } + + before do + m = Vagrant.plugin("2").manager + m.stub(hosts: plugin_hosts) + m.stub(host_capabilities: plugin_host_caps) + + # Detect the host + env.vagrantfile <<-VF + Vagrant.configure("2") do |config| + config.vagrant.host = nil + end + VF + + # Setup the foo host by default + plugin_hosts[:foo] = [detect_class(true), nil] + end + + it "should return whether it can install or not" do + plugin_host_caps[:foo] = { provider_install_foo: Class } + + expect(subject.can_install_provider?(:foo)).to be_true + expect(subject.can_install_provider?(:bar)).to be_false + end + end + + describe "#install_provider" do + let(:host) { double(:host) } + + before do + allow(subject).to receive(:host).and_return(host) + end + + it "should install the correct provider" do + expect(host).to receive(:capability).with(:provider_install_foo) + + subject.install_provider(:foo) + end + end + describe "#home_path" do it "is set to the home path given" do Dir.mktmpdir do |dir| diff --git a/test/unit/vagrant/plugin/v2/provider_test.rb b/test/unit/vagrant/plugin/v2/provider_test.rb index c27274db1..634b311c6 100644 --- a/test/unit/vagrant/plugin/v2/provider_test.rb +++ b/test/unit/vagrant/plugin/v2/provider_test.rb @@ -12,6 +12,10 @@ describe Vagrant::Plugin::V2::Provider do expect(described_class).to be_usable end + it "should be installed by default" do + expect(described_class).to be_installed + end + it "should return nil by default for actions" do expect(instance.action(:whatever)).to be_nil end From d863242538703dc97d4d59eda28ee613dd0e9e77 Mon Sep 17 00:00:00 2001 From: Clayton O'Neill Date: Thu, 5 Nov 2015 15:40:27 -0500 Subject: [PATCH 155/484] Add Errno::ENETUNREACH to SSH rescue This changes the ssh ready? method to treat ENETUNREACH the same way as EHOSTUNREACH errors. When attempting to SSH into a box, it tries up to 5 times to connect to the box, ignoring various errors. Later it will catch and gracefully handle most of those errors so that callers don't have to know the details. However, the Errno::ENETUNREACH error is not caught, which means that callers that expect a clean boolean return from ready? don't get that, and instead get an exception they probably aren't expecting. --- plugins/communicators/ssh/communicator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/communicators/ssh/communicator.rb b/plugins/communicators/ssh/communicator.rb index 8297329f9..035ef5f61 100644 --- a/plugins/communicators/ssh/communicator.rb +++ b/plugins/communicators/ssh/communicator.rb @@ -420,7 +420,7 @@ module VagrantPlugins rescue Errno::EHOSTDOWN # This is raised if we get an ICMP DestinationUnknown error. raise Vagrant::Errors::SSHHostDown - rescue Errno::EHOSTUNREACH + rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH # This is raised if we can't work out how to route traffic. raise Vagrant::Errors::SSHNoRoute rescue Net::SSH::Exception => e From df32f6ac5195e487e8d4c28d12a46025926be261 Mon Sep 17 00:00:00 2001 From: Luca Invernizzi Date: Thu, 5 Nov 2015 13:56:13 -0800 Subject: [PATCH 156/484] Update docker installer to work on custom kernels The current docker installer attempt to install the linux-image-extra-`uname -r` DEB package on Debian systems. This package may not exist, for example on custom kernels (e.g., Linode servers). If this happens, Vagrant halts the provisioning. However, this package is not really needed in newer Debian releases (such as Ubuntu 14.04). This small patch checks if the linux-image-extra-`uname -r` package exists, and it will install it if it does. In either case, it will continue provisioning. --- plugins/provisioners/docker/cap/debian/docker_install.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/provisioners/docker/cap/debian/docker_install.rb b/plugins/provisioners/docker/cap/debian/docker_install.rb index b598ee785..b9fbed673 100644 --- a/plugins/provisioners/docker/cap/debian/docker_install.rb +++ b/plugins/provisioners/docker/cap/debian/docker_install.rb @@ -11,7 +11,9 @@ module VagrantPlugins comm.sudo("apt-get update -y") # TODO: Perform check on the host machine if aufs is installed and using LXC if machine.provider_name != :lxc - comm.sudo("lsmod | grep aufs || modprobe aufs || apt-get install -y linux-image-extra-`uname -r`") + # Attempt to install linux-image-extra for this kernel, if it exists + package_name = "linux-image-extra-`uname -r`" + comm.sudo("lsmod | grep aufs || modprobe aufs || apt-cache show #{package_name} && apt-get install -y #{package_name} || true") end comm.sudo("apt-get install -y --force-yes -q curl") comm.sudo("curl -sSL https://get.docker.com/gpg | apt-key add -") From baea923e9cf3327336ad896f5024541339e3b599 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Nov 2015 13:58:15 -0800 Subject: [PATCH 157/484] commands/up: automatically install providers --- lib/vagrant/environment.rb | 2 + plugins/commands/up/command.rb | 85 +++++++++++++++++++----- plugins/providers/virtualbox/provider.rb | 9 +++ templates/locales/en.yml | 7 ++ 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 034d71163..88779a2e9 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -309,6 +309,7 @@ module Vagrant def default_provider(**opts) opts[:exclude] = Set.new(opts[:exclude]) if opts[:exclude] opts[:force_default] = true if !opts.key?(:force_default) + opts[:check_usable] = true if !opts.key?(:check_usable) default = ENV["VAGRANT_DEFAULT_PROVIDER"] default = nil if default == "" @@ -376,6 +377,7 @@ module Vagrant # Find the matching implementation ordered.each do |_, key, impl, _| + return key if !opts[:check_usable] return key if impl.usable?(false) end diff --git a/plugins/commands/up/command.rb b/plugins/commands/up/command.rb index 826bea0ae..b93e859cf 100644 --- a/plugins/commands/up/command.rb +++ b/plugins/commands/up/command.rb @@ -1,4 +1,5 @@ require 'optparse' +require 'set' require "vagrant" @@ -16,6 +17,7 @@ module VagrantPlugins def execute options = {} options[:destroy_on_error] = true + options[:install_provider] = true options[:parallel] = true options[:provision_ignore_sentinel] = false @@ -41,6 +43,11 @@ module VagrantPlugins "Back the machine with a specific provider") do |provider| options[:provider] = provider end + + o.on("--[no-]install-provider", + "If possible, install the provider if it isn't installed") do |p| + options[:install_provider] = p + end end # Parse the options @@ -53,24 +60,32 @@ module VagrantPlugins # Go over each VM and bring it up @logger.debug("'Up' each target VM...") - # Build up the batch job of what we'll do - machines = [] - @env.batch(options[:parallel]) do |batch| - names = argv - if names.empty? - autostart = false - @env.vagrantfile.machine_names_and_options.each do |n, o| - autostart = true if o.key?(:autostart) - o[:autostart] = true if !o.key?(:autostart) - names << n.to_s if o[:autostart] - end - - # If we have an autostart key but no names, it means that - # all machines are autostart: false and we don't start anything. - names = nil if autostart && names.empty? + # Get the names of the machines we want to bring up + names = argv + if names.empty? + autostart = false + @env.vagrantfile.machine_names_and_options.each do |n, o| + autostart = true if o.key?(:autostart) + o[:autostart] = true if !o.key?(:autostart) + names << n.to_s if o[:autostart] end - if names + # If we have an autostart key but no names, it means that + # all machines are autostart: false and we don't start anything. + names = nil if autostart && names.empty? + end + + # Build up the batch job of what we'll do + machines = [] + if names + # If we're installing providers, then do that. We don't + # parallelize this step because it is likely the same provider + # anyways. + if options[:install_provider] + install_providers(names) + end + + @env.batch(options[:parallel]) do |batch| with_target_vms(names, provider: options[:provider]) do |machine| @env.ui.info(I18n.t( "vagrant.commands.up.upping", @@ -106,6 +121,44 @@ module VagrantPlugins # Success, exit status 0 0 end + + protected + + def install_providers(names) + # First create a set of all the providers we need to check for. + # Most likely this will be a set of one. + providers = Set.new + names.each do |name| + providers.add(@env.default_provider(machine: name.to_sym, check_usable: false)) + end + + # Go through and determine if we can install the providers + providers.delete_if do |name| + !@env.can_install_provider?(name) + end + + # Install the providers if we have to + providers.each do |name| + # Find the provider. Ignore if we can't find it, this error + # will pop up later in the process. + parts = Vagrant.plugin("2").manager.providers[name] + next if !parts + + # If the provider is already installed, then our work here is done + cls = parts[0] + next if cls.installed? + + # Some human-friendly output + ui = Vagrant::UI::Prefixed.new(@env.ui, "") + ui.output(I18n.t( + "vagrant.installing_provider", + provider: name.to_s)) + ui.detail(I18n.t("vagrant.installing_provider_detail")) + + # Install the provider + @env.install_provider(name) + end + end end end end diff --git a/plugins/providers/virtualbox/provider.rb b/plugins/providers/virtualbox/provider.rb index 969fb4718..6bde344d2 100644 --- a/plugins/providers/virtualbox/provider.rb +++ b/plugins/providers/virtualbox/provider.rb @@ -5,6 +5,15 @@ module VagrantPlugins class Provider < Vagrant.plugin("2", :provider) attr_reader :driver + def self.installed? + Driver::Meta.new + true + rescue Vagrant::Errors::VirtualBoxInvalidVersion + return false + rescue Vagrant::Errors::VirtualBoxNotDetected + return false + end + def self.usable?(raise_error=false) # Instantiate the driver, which will determine the VirtualBox # version and all that, which checks for VirtualBox being present diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 2bafd0f55..3b7094310 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -154,6 +154,13 @@ en: Inserting generated public key within guest... inserting_remove_key: |- Removing insecure key from the guest if it's present... + installing_provider: |- + Provider '%{provider}' not found. We'll automatically install it now... + installing_provider_detail: |- + The installation process will start below. Human interaction may be + required at some points. If you're uncomfortable with automatically + installing this provider, you can safely Ctrl-C this process and install + it manually. list_commands: |- Below is a listing of all available Vagrant commands and a brief description of what they do. From c017340f699a199a7043817788cb9f7abecac37f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Nov 2015 13:59:08 -0800 Subject: [PATCH 158/484] website: update docs for --install-provider flag --- website/docs/source/v2/cli/up.html.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/docs/source/v2/cli/up.html.md b/website/docs/source/v2/cli/up.html.md index a0408a9a6..44d320669 100644 --- a/website/docs/source/v2/cli/up.html.md +++ b/website/docs/source/v2/cli/up.html.md @@ -20,6 +20,10 @@ on a day-to-day basis. unexpected error occurs. This will only happen on the first `vagrant up`. By default this is set. +* `--[no-]install-provider` - If the requested provider is not installed, + Vagrant will attempt to automatically install it if it can. By default this + is enabled. + * `--[no-]parallel` - Bring multiple machines up in parallel if the provider supports it. Please consult the provider documentation to see if this feature is supported. From 9bfdaf7e75d710aac42c65f68d9999c4f7c92798 Mon Sep 17 00:00:00 2001 From: Gilles Cornu Date: Tue, 10 Feb 2015 15:28:00 +0100 Subject: [PATCH 159/484] provisioners/ansible: introduce ansible_local With this change, the existing host-based Ansible provisioner is refactored to share a maximum of code with this new guest-based Ansible provisioner. At this stage of development, the existing unit tests are intentionally modified as little as possible, to keep safe the existing funtionalities. Other issues resolved by this changeset: - Display a warning when running from a Windows host [GH-5292] - Do not run `ansible-playbook` in verbose mode when the `verbose` option is set to an empty string. --- lib/vagrant/errors.rb | 8 - .../ansible/cap/guest/arch/ansible_install.rb | 19 ++ .../cap/guest/debian/ansible_install.rb | 28 ++ .../ansible/cap/guest/epel/ansible_install.rb | 39 +++ .../cap/guest/freebsd/ansible_install.rb | 18 ++ .../cap/guest/posix/ansible_installed.rb | 25 ++ .../ansible/cap/guest/suse/ansible_install.rb | 18 ++ .../cap/guest/ubuntu/ansible_install.rb | 22 ++ plugins/provisioners/ansible/config.rb | 128 -------- plugins/provisioners/ansible/config/base.rb | 105 ++++++ plugins/provisioners/ansible/config/guest.rb | 63 ++++ plugins/provisioners/ansible/config/host.rb | 61 ++++ plugins/provisioners/ansible/errors.rb | 27 ++ plugins/provisioners/ansible/helpers.rb | 37 +++ plugins/provisioners/ansible/plugin.rb | 68 +++- plugins/provisioners/ansible/provisioner.rb | 302 ------------------ .../provisioners/ansible/provisioner/base.rb | 151 +++++++++ .../provisioners/ansible/provisioner/guest.rb | 135 ++++++++ .../provisioners/ansible/provisioner/host.rb | 198 ++++++++++++ templates/locales/en.yml | 62 +++- .../provisioners/ansible/config_test.rb | 45 +-- .../provisioners/ansible/provisioner_test.rb | 114 ++++--- .../provisioners/support/shared/config.rb | 6 + website/docs/source/layouts/layout.erb | 1 + .../source/v2/provisioning/ansible.html.md | 215 ++----------- .../v2/provisioning/ansible_common.html.md | 86 +++++ .../v2/provisioning/ansible_intro.html.md | 208 ++++++++++++ .../v2/provisioning/ansible_local.html.md | 147 +++++++++ 28 files changed, 1627 insertions(+), 709 deletions(-) create mode 100644 plugins/provisioners/ansible/cap/guest/arch/ansible_install.rb create mode 100644 plugins/provisioners/ansible/cap/guest/debian/ansible_install.rb create mode 100644 plugins/provisioners/ansible/cap/guest/epel/ansible_install.rb create mode 100644 plugins/provisioners/ansible/cap/guest/freebsd/ansible_install.rb create mode 100644 plugins/provisioners/ansible/cap/guest/posix/ansible_installed.rb create mode 100644 plugins/provisioners/ansible/cap/guest/suse/ansible_install.rb create mode 100644 plugins/provisioners/ansible/cap/guest/ubuntu/ansible_install.rb delete mode 100644 plugins/provisioners/ansible/config.rb create mode 100644 plugins/provisioners/ansible/config/base.rb create mode 100644 plugins/provisioners/ansible/config/guest.rb create mode 100644 plugins/provisioners/ansible/config/host.rb create mode 100644 plugins/provisioners/ansible/errors.rb create mode 100644 plugins/provisioners/ansible/helpers.rb delete mode 100644 plugins/provisioners/ansible/provisioner.rb create mode 100644 plugins/provisioners/ansible/provisioner/base.rb create mode 100644 plugins/provisioners/ansible/provisioner/guest.rb create mode 100644 plugins/provisioners/ansible/provisioner/host.rb create mode 100644 website/docs/source/v2/provisioning/ansible_common.html.md create mode 100644 website/docs/source/v2/provisioning/ansible_intro.html.md create mode 100644 website/docs/source/v2/provisioning/ansible_local.html.md diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index c76f3cbe9..ea3811cbe 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -108,14 +108,6 @@ module Vagrant error_key(:active_machine_with_different_provider) end - class AnsibleFailed < VagrantError - error_key(:ansible_failed) - end - - class AnsiblePlaybookAppNotFound < VagrantError - error_key(:ansible_playbook_app_not_found) - end - class BatchMultiError < VagrantError error_key(:batch_multi_error) end diff --git a/plugins/provisioners/ansible/cap/guest/arch/ansible_install.rb b/plugins/provisioners/ansible/cap/guest/arch/ansible_install.rb new file mode 100644 index 000000000..62e1e2141 --- /dev/null +++ b/plugins/provisioners/ansible/cap/guest/arch/ansible_install.rb @@ -0,0 +1,19 @@ + +module VagrantPlugins + module Ansible + module Cap + module Guest + module Arch + module AnsibleInstall + + def self.ansible_install(machine) + machine.communicate.sudo("pacman -Syy --noconfirm") + machine.communicate.sudo("pacman -S --noconfirm ansible") + end + + end + end + end + end + end +end diff --git a/plugins/provisioners/ansible/cap/guest/debian/ansible_install.rb b/plugins/provisioners/ansible/cap/guest/debian/ansible_install.rb new file mode 100644 index 000000000..05308d03d --- /dev/null +++ b/plugins/provisioners/ansible/cap/guest/debian/ansible_install.rb @@ -0,0 +1,28 @@ + +module VagrantPlugins + module Ansible + module Cap + module Guest + module Debian + module AnsibleInstall + + def self.ansible_install(machine) + +install_backports_if_wheezy_release = < /etc/apt/sources.list.d/wheezy-backports.list +fi +INLINE_CRIPT + + machine.communicate.sudo(install_backports_if_wheezy_release) + machine.communicate.sudo("apt-get update -y -qq") + machine.communicate.sudo("apt-get install -y -qq ansible") + end + + end + end + end + end + end +end diff --git a/plugins/provisioners/ansible/cap/guest/epel/ansible_install.rb b/plugins/provisioners/ansible/cap/guest/epel/ansible_install.rb new file mode 100644 index 000000000..6ae10b091 --- /dev/null +++ b/plugins/provisioners/ansible/cap/guest/epel/ansible_install.rb @@ -0,0 +1,39 @@ + +module VagrantPlugins + module Ansible + module Cap + module Guest + module EPEL # Extra Packages for Enterprise Linux (for RedHat-family distributions) + module AnsibleInstall + + # This should work on recent Fedora releases, and any other members of the + # RedHat family which supports YUM and http://fedoraproject.org/wiki/EPEL + def self.ansible_install(machine) + + configure_epel = </etc/yum.repos.d/epel-bootstrap.repo +[epel] +name=Bootstrap EPEL +mirrorlist=http://mirrors.fedoraproject.org/mirrorlist?repo=epel-\$releasever&arch=\$basearch +failovermethod=priority +enabled=0 +gpgcheck=0 +EOM + +yum --assumeyes --quiet --enablerepo=epel install epel-release +rm -f /etc/yum.repos.d/epel-bootstrap.repo +INLINE_CRIPT + + ansible_is_installable = machine.communicate.execute("yum info ansible", :error_check => false) + if ansible_is_installable != 0 + machine.communicate.sudo(configure_epel) + end + machine.communicate.sudo("yum --assumeyes --quiet --enablerepo=epel install ansible") + end + + end + end + end + end + end +end diff --git a/plugins/provisioners/ansible/cap/guest/freebsd/ansible_install.rb b/plugins/provisioners/ansible/cap/guest/freebsd/ansible_install.rb new file mode 100644 index 000000000..4cf7b1807 --- /dev/null +++ b/plugins/provisioners/ansible/cap/guest/freebsd/ansible_install.rb @@ -0,0 +1,18 @@ + +module VagrantPlugins + module Ansible + module Cap + module Guest + module FreeBSD + module AnsibleInstall + + def self.ansible_install(machine) + machine.communicate.sudo("yes | pkg install ansible") + end + + end + end + end + end + end +end diff --git a/plugins/provisioners/ansible/cap/guest/posix/ansible_installed.rb b/plugins/provisioners/ansible/cap/guest/posix/ansible_installed.rb new file mode 100644 index 000000000..16abae9bf --- /dev/null +++ b/plugins/provisioners/ansible/cap/guest/posix/ansible_installed.rb @@ -0,0 +1,25 @@ +module VagrantPlugins + module Ansible + module Cap + module Guest + module POSIX + module AnsibleInstalled + + # Check if Ansible is installed (at the given version). + # @return [true, false] + def self.ansible_installed(machine, version) + command = 'test -x "$(command -v ansible)"' + + if !version.empty? + command << "&& ansible --version | grep 'ansible #{version}'" + end + + machine.communicate.test(command, sudo: false) + end + + end + end + end + end + end +end diff --git a/plugins/provisioners/ansible/cap/guest/suse/ansible_install.rb b/plugins/provisioners/ansible/cap/guest/suse/ansible_install.rb new file mode 100644 index 000000000..64f3cd41c --- /dev/null +++ b/plugins/provisioners/ansible/cap/guest/suse/ansible_install.rb @@ -0,0 +1,18 @@ + +module VagrantPlugins + module Ansible + module Cap + module Guest + module SUSE + module AnsibleInstall + + def self.ansible_install(machine) + machine.communicate.sudo("zypper --non-interactive --quiet install ansible") + end + + end + end + end + end + end +end diff --git a/plugins/provisioners/ansible/cap/guest/ubuntu/ansible_install.rb b/plugins/provisioners/ansible/cap/guest/ubuntu/ansible_install.rb new file mode 100644 index 000000000..92182fe06 --- /dev/null +++ b/plugins/provisioners/ansible/cap/guest/ubuntu/ansible_install.rb @@ -0,0 +1,22 @@ + +module VagrantPlugins + module Ansible + module Cap + module Guest + module Ubuntu + module AnsibleInstall + + def self.ansible_install(machine) + machine.communicate.sudo("apt-get update -y -qq") + machine.communicate.sudo("apt-get install -y -qq software-properties-common python-software-properties") + machine.communicate.sudo("add-apt-repository ppa:ansible/ansible -y") + machine.communicate.sudo("apt-get update -y -qq") + machine.communicate.sudo("apt-get install -y -qq ansible") + end + + end + end + end + end + end +end diff --git a/plugins/provisioners/ansible/config.rb b/plugins/provisioners/ansible/config.rb deleted file mode 100644 index 784cf48f7..000000000 --- a/plugins/provisioners/ansible/config.rb +++ /dev/null @@ -1,128 +0,0 @@ -module VagrantPlugins - module Ansible - class Config < Vagrant.plugin("2", :config) - attr_accessor :playbook - attr_accessor :extra_vars - attr_accessor :inventory_path - attr_accessor :ask_sudo_pass - attr_accessor :ask_vault_pass - attr_accessor :vault_password_file - attr_accessor :limit - attr_accessor :sudo - attr_accessor :sudo_user - attr_accessor :verbose - attr_accessor :tags - attr_accessor :skip_tags - attr_accessor :start_at_task - attr_accessor :groups - attr_accessor :host_key_checking - - # Joker attribute, used to pass unsupported arguments to ansible-playbook anyway - attr_accessor :raw_arguments - # Joker attribute, used to set additional SSH parameters for ansible-playbook anyway - attr_accessor :raw_ssh_args - - def initialize - @playbook = UNSET_VALUE - @extra_vars = UNSET_VALUE - @inventory_path = UNSET_VALUE - @ask_sudo_pass = UNSET_VALUE - @ask_vault_pass = UNSET_VALUE - @vault_password_file = UNSET_VALUE - @limit = UNSET_VALUE - @sudo = UNSET_VALUE - @sudo_user = UNSET_VALUE - @verbose = UNSET_VALUE - @tags = UNSET_VALUE - @skip_tags = UNSET_VALUE - @start_at_task = UNSET_VALUE - @groups = UNSET_VALUE - @host_key_checking = UNSET_VALUE - @raw_arguments = UNSET_VALUE - @raw_ssh_args = UNSET_VALUE - end - - def finalize! - @playbook = nil if @playbook == UNSET_VALUE - @extra_vars = nil if @extra_vars == UNSET_VALUE - @inventory_path = nil if @inventory_path == UNSET_VALUE - @ask_sudo_pass = false unless @ask_sudo_pass == true - @ask_vault_pass = false unless @ask_vault_pass == true - @vault_password_file = nil if @vault_password_file == UNSET_VALUE - @limit = nil if @limit == UNSET_VALUE - @sudo = false unless @sudo == true - @sudo_user = nil if @sudo_user == UNSET_VALUE - @verbose = nil if @verbose == UNSET_VALUE - @tags = nil if @tags == UNSET_VALUE - @skip_tags = nil if @skip_tags == UNSET_VALUE - @start_at_task = nil if @start_at_task == UNSET_VALUE - @groups = {} if @groups == UNSET_VALUE - @host_key_checking = false unless @host_key_checking == true - @raw_arguments = nil if @raw_arguments == UNSET_VALUE - @raw_ssh_args = nil if @raw_ssh_args == UNSET_VALUE - end - - def validate(machine) - errors = _detected_errors - - # Validate that a playbook path was provided - if !playbook - errors << I18n.t("vagrant.provisioners.ansible.no_playbook") - end - - # Validate the existence of said playbook on the host - if playbook - expanded_path = Pathname.new(playbook).expand_path(machine.env.root_path) - if !expanded_path.file? - errors << I18n.t("vagrant.provisioners.ansible.playbook_path_invalid", - path: expanded_path) - end - end - - # Validate that extra_vars is either a hash, or a path to an - # existing file - if extra_vars - extra_vars_is_valid = extra_vars.kind_of?(Hash) || extra_vars.kind_of?(String) - if extra_vars.kind_of?(String) - # Accept the usage of '@' prefix in Vagrantfile (e.g. '@vars.yml' - # and 'vars.yml' are both supported) - match_data = /^@?(.+)$/.match(extra_vars) - extra_vars_path = match_data[1].to_s - expanded_path = Pathname.new(extra_vars_path).expand_path(machine.env.root_path) - extra_vars_is_valid = expanded_path.exist? - if extra_vars_is_valid - @extra_vars = '@' + extra_vars_path - end - end - - if !extra_vars_is_valid - errors << I18n.t("vagrant.provisioners.ansible.extra_vars_invalid", - type: extra_vars.class.to_s, - value: extra_vars.to_s - ) - end - end - - # Validate the existence of the inventory_path, if specified - if inventory_path - expanded_path = Pathname.new(inventory_path).expand_path(machine.env.root_path) - if !expanded_path.exist? - errors << I18n.t("vagrant.provisioners.ansible.inventory_path_invalid", - path: expanded_path) - end - end - - # Validate the existence of the vault_password_file, if specified - if vault_password_file - expanded_path = Pathname.new(vault_password_file).expand_path(machine.env.root_path) - if !expanded_path.exist? - errors << I18n.t("vagrant.provisioners.ansible.vault_password_file_invalid", - path: expanded_path) - end - end - - { "ansible provisioner" => errors } - end - end - end -end diff --git a/plugins/provisioners/ansible/config/base.rb b/plugins/provisioners/ansible/config/base.rb new file mode 100644 index 000000000..e0b610a27 --- /dev/null +++ b/plugins/provisioners/ansible/config/base.rb @@ -0,0 +1,105 @@ +module VagrantPlugins + module Ansible + module Config + class Base < Vagrant.plugin("2", :config) + + attr_accessor :extra_vars + attr_accessor :groups + attr_accessor :inventory_path + attr_accessor :limit + attr_accessor :playbook + 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 + + def initialize + @extra_vars = UNSET_VALUE + @groups = UNSET_VALUE + @inventory_path = UNSET_VALUE + @limit = UNSET_VALUE + @playbook = UNSET_VALUE + @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 + end + + def finalize! + @extra_vars = nil if @extra_vars == UNSET_VALUE + @groups = {} if @groups == UNSET_VALUE + @inventory_path = nil if @inventory_path == UNSET_VALUE + @limit = nil if @limit == UNSET_VALUE + @playbook = nil if @playbook == UNSET_VALUE + @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 + end + + # Just like the normal configuration "validate" method except that + # it returns an array of errors that should be merged into some + # other error accumulator. + def validate(machine) + @errors = _detected_errors + + # Validate that a playbook path was provided + if !playbook + @errors << I18n.t("vagrant.provisioners.ansible.no_playbook") + end + + # Validate the existence of the playbook + if playbook + check_path_is_a_file(machine, playbook, "vagrant.provisioners.ansible.playbook_path_invalid") + end + + # Validate the existence of the inventory_path, if specified + if inventory_path + check_path_exists(machine, inventory_path, "vagrant.provisioners.ansible.inventory_path_invalid") + end + + # Validate the existence of the vault_password_file, if specified + if vault_password_file + check_path_is_a_file(machine, vault_password_file, "vagrant.provisioners.ansible.vault_password_file_invalid") + end + + # Validate that extra_vars is either a hash, or a path to an + # existing file + if extra_vars + extra_vars_is_valid = extra_vars.kind_of?(Hash) || extra_vars.kind_of?(String) + if extra_vars.kind_of?(String) + # Accept the usage of '@' prefix in Vagrantfile (e.g. '@vars.yml' + # and 'vars.yml' are both supported) + match_data = /^@?(.+)$/.match(extra_vars) + extra_vars_path = match_data[1].to_s + extra_vars_is_valid = check_path_is_a_file(machine, extra_vars_path) + if extra_vars_is_valid + @extra_vars = '@' + extra_vars_path + end + end + + if !extra_vars_is_valid + @errors << I18n.t( + "vagrant.provisioners.ansible.extra_vars_invalid", + type: extra_vars.class.to_s, + value: extra_vars.to_s) + end + end + + end + end + end + end +end diff --git a/plugins/provisioners/ansible/config/guest.rb b/plugins/provisioners/ansible/config/guest.rb new file mode 100644 index 000000000..a7f3bf44e --- /dev/null +++ b/plugins/provisioners/ansible/config/guest.rb @@ -0,0 +1,63 @@ +require_relative "base" + +module VagrantPlugins + module Ansible + module Config + class Guest < Base + + attr_accessor :provisioning_path + attr_accessor :tmp_path + attr_accessor :install + attr_accessor :version + + def initialize + super + + @install = UNSET_VALUE + @provisioning_path = UNSET_VALUE + @tmp_path = UNSET_VALUE + @version = UNSET_VALUE + end + + def finalize! + super + + @install = true if @install == 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) + super + + { "ansible local provisioner" => @errors } + end + + protected + + def check_path(machine, path, test_args, error_message_key = nil) + remote_path = Pathname.new(path).expand_path(@provisioning_path) + if machine.communicate.ready? && !machine.communicate.test("test #{test_args} #{remote_path}") + if error_message_key + @errors << I18n.t(error_message_key, path: remote_path, system: "guest") + end + return false + end + # when the machine is not ready for SSH communication, + # the check is "optimistically" by passed. + true + end + + def check_path_is_a_file(machine, path, error_message_key = nil) + check_path(machine, path, "-f", error_message_key) + end + + def check_path_exists(machine, path, error_message_key = nil) + check_path(machine, path, "-e", error_message_key) + end + + end + end + end +end diff --git a/plugins/provisioners/ansible/config/host.rb b/plugins/provisioners/ansible/config/host.rb new file mode 100644 index 000000000..fa5d79d19 --- /dev/null +++ b/plugins/provisioners/ansible/config/host.rb @@ -0,0 +1,61 @@ +require_relative "base" + +module VagrantPlugins + module Ansible + module Config + class Host < Base + + attr_accessor :ask_sudo_pass + attr_accessor :ask_vault_pass + attr_accessor :host_key_checking + attr_accessor :raw_ssh_args + + def initialize + super + + @ask_sudo_pass = false + @ask_vault_pass = false + @host_key_checking = false + @raw_ssh_args = UNSET_VALUE + end + + def finalize! + super + + @ask_sudo_pass = false if @ask_sudo_pass != true + @ask_vault_pass = false if @ask_vault_pass != true + @host_key_checking = false if @host_key_checking != true + @raw_ssh_args = nil if @raw_ssh_args == UNSET_VALUE + end + + def validate(machine) + super + + { "ansible remote provisioner" => @errors } + end + + protected + + def check_path(machine, path, path_test_method, error_message_key = nil) + expanded_path = Pathname.new(path).expand_path(machine.env.root_path) + if !expanded_path.public_send(path_test_method) + if error_message_key + @errors << I18n.t(error_message_key, path: expanded_path, system: "host") + end + return false + end + true + end + + def check_path_is_a_file(machine, path, error_message_key = nil) + check_path(machine, path, "file?", error_message_key) + end + + def check_path_exists(machine, path, error_message_key = nil) + check_path(machine, path, "exist?", error_message_key) + end + + end + end + end +end diff --git a/plugins/provisioners/ansible/errors.rb b/plugins/provisioners/ansible/errors.rb new file mode 100644 index 000000000..2a6f8dbbd --- /dev/null +++ b/plugins/provisioners/ansible/errors.rb @@ -0,0 +1,27 @@ +require "vagrant" + +module VagrantPlugins + module Ansible + module Errors + class AnsibleError < Vagrant::Errors::VagrantError + error_namespace("vagrant.provisioners.ansible.errors") + end + + class AnsiblePlaybookAppFailed < AnsibleError + error_key(:ansible_playbook_app_failed) + end + + class AnsiblePlaybookAppNotFoundOnHost < AnsibleError + error_key(:ansible_playbook_app_not_found_on_host) + end + + class AnsiblePlaybookAppNotFoundOnGuest < AnsibleError + error_key(:ansible_playbook_app_not_found_on_guest) + end + + class AnsibleVersionNotFoundOnGuest < AnsibleError + error_key(:ansible_version_not_found_on_guest) + end + end + end +end \ No newline at end of file diff --git a/plugins/provisioners/ansible/helpers.rb b/plugins/provisioners/ansible/helpers.rb new file mode 100644 index 000000000..1bdb57366 --- /dev/null +++ b/plugins/provisioners/ansible/helpers.rb @@ -0,0 +1,37 @@ +require "vagrant" + +module VagrantPlugins + module Ansible + class Helpers + def self.stringify_ansible_playbook_command(env, command) + shell_command = '' + env.each_pair do |k, v| + if k == 'ANSIBLE_SSH_ARGS' + shell_command += "#{k}='#{v}' " + else + shell_command += "#{k}=#{v} " + end + end + + shell_arg = [] + command.each do |arg| + if arg =~ /(--start-at-task|--limit)=(.+)/ + shell_arg << "#{$1}='#{$2}'" + else + shell_arg << arg + end + end + + shell_command += shell_arg.join(' ') + end + + def self.as_list_argument(v) + v.kind_of?(Array) ? v.join(',') : v + end + + def self.as_array(v) + v.kind_of?(Array) ? v : [v] + end + end + end +end \ No newline at end of file diff --git a/plugins/provisioners/ansible/plugin.rb b/plugins/provisioners/ansible/plugin.rb index 0cafe47fb..3be60f8ad 100644 --- a/plugins/provisioners/ansible/plugin.rb +++ b/plugins/provisioners/ansible/plugin.rb @@ -3,21 +3,73 @@ require "vagrant" module VagrantPlugins module Ansible class Plugin < Vagrant.plugin("2") + name "ansible" description <<-DESC - Provides support for provisioning your virtual machines with - Ansible playbooks. + Provides support for provisioning your virtual machines with Ansible + from the Vagrant host (`ansible`) or from the guests (`ansible_local`). DESC - config(:ansible, :provisioner) do - require File.expand_path("../config", __FILE__) - Config + config("ansible", :provisioner) do + require_relative "config/host" + Config::Host end - provisioner(:ansible) do - require File.expand_path("../provisioner", __FILE__) - Provisioner + config("ansible_local", :provisioner) do + require_relative "config/guest" + Config::Guest end + + provisioner("ansible") do + require_relative "provisioner/host" + Provisioner::Host + end + + provisioner("ansible_local") do + require_relative "provisioner/guest" + Provisioner::Guest + end + + guest_capability(:linux, :ansible_installed) do + require_relative "cap/guest/posix/ansible_installed" + Cap::Guest::POSIX::AnsibleInstalled + end + + guest_capability(:freebsd, :ansible_installed) do + require_relative "cap/guest/posix/ansible_installed" + Cap::Guest::POSIX::AnsibleInstalled + end + + guest_capability(:arch, :ansible_install) do + require_relative "cap/guest/arch/ansible_install" + Cap::Guest::Arch::AnsibleInstall + end + + guest_capability(:debian, :ansible_install) do + require_relative "cap/guest/debian/ansible_install" + Cap::Guest::Debian::AnsibleInstall + end + + guest_capability(:ubuntu, :ansible_install) do + require_relative "cap/guest/ubuntu/ansible_install" + Cap::Guest::Ubuntu::AnsibleInstall + end + + guest_capability(:redhat, :ansible_install) do + require_relative "cap/guest/epel/ansible_install" + Cap::Guest::EPEL::AnsibleInstall + end + + guest_capability(:suse, :ansible_install) do + require_relative "cap/guest/suse/ansible_install" + Cap::Guest::SUSE::AnsibleInstall + end + + guest_capability(:freebsd, :ansible_install) do + require_relative "cap/guest/freebsd/ansible_install" + Cap::Guest::FreeBSD::AnsibleInstall + end + end end end diff --git a/plugins/provisioners/ansible/provisioner.rb b/plugins/provisioners/ansible/provisioner.rb deleted file mode 100644 index 6dd1313e0..000000000 --- a/plugins/provisioners/ansible/provisioner.rb +++ /dev/null @@ -1,302 +0,0 @@ -require "vagrant/util/platform" -require "thread" - -module VagrantPlugins - module Ansible - class Provisioner < Vagrant.plugin("2", :provisioner) - - @@lock = Mutex.new - - def initialize(machine, config) - super - - @logger = Log4r::Logger.new("vagrant::provisioners::ansible") - end - - def provision - @ssh_info = @machine.ssh_info - - # - # Ansible provisioner options - # - - # By default, connect with Vagrant SSH username - options = %W[--user=#{@ssh_info[:username]}] - - # Connect with native OpenSSH client - # Other modes (e.g. paramiko) are not officially supported, - # but can be enabled via raw_arguments option. - options << "--connection=ssh" - - # Increase the SSH connection timeout, as the Ansible default value (10 seconds) - # is a bit demanding for some overloaded developer boxes. This is particularly - # helpful when additional virtual networks are configured, as their availability - # is not controlled during vagrant boot process. - options << "--timeout=30" - - # By default we limit by the current machine, but - # this can be overridden by the `limit` option. - if config.limit - options << "--limit=#{as_list_argument(config.limit)}" - else - options << "--limit=#{@machine.name}" - end - - options << "--inventory-file=#{self.setup_inventory_file}" - options << "--extra-vars=#{self.get_extra_vars_argument}" if config.extra_vars - options << "--sudo" if config.sudo - options << "--sudo-user=#{config.sudo_user}" if config.sudo_user - options << "#{self.get_verbosity_argument}" if config.verbose - options << "--ask-sudo-pass" if config.ask_sudo_pass - options << "--ask-vault-pass" if config.ask_vault_pass - options << "--vault-password-file=#{config.vault_password_file}" if config.vault_password_file - options << "--tags=#{as_list_argument(config.tags)}" if config.tags - options << "--skip-tags=#{as_list_argument(config.skip_tags)}" if config.skip_tags - options << "--start-at-task=#{config.start_at_task}" if config.start_at_task - - # Finally, add the raw configuration options, which has the highest precedence - # and can therefore potentially override any other options of this provisioner. - options.concat(self.as_array(config.raw_arguments)) if config.raw_arguments - - # - # Assemble the full ansible-playbook command - # - - command = (%w(ansible-playbook) << options << config.playbook).flatten - - env = { - # Ensure Ansible output isn't buffered so that we receive output - # on a task-by-task basis. - "PYTHONUNBUFFERED" => 1, - - # Some Ansible options must be passed as environment variables, - # as there is no equivalent command line arguments - "ANSIBLE_HOST_KEY_CHECKING" => "#{config.host_key_checking}", - } - - # When Ansible output is piped in Vagrant integration, its default colorization is - # automatically disabled and the only way to re-enable colors is to use ANSIBLE_FORCE_COLOR. - env["ANSIBLE_FORCE_COLOR"] = "true" if @machine.env.ui.color? - # Setting ANSIBLE_NOCOLOR is "unnecessary" at the moment, but this could change in the future - # (e.g. local provisioner [GH-2103], possible change in vagrant/ansible integration, etc.) - env["ANSIBLE_NOCOLOR"] = "true" if !@machine.env.ui.color? - - # ANSIBLE_SSH_ARGS is required for Multiple SSH keys, SSH forwarding and custom SSH settings - env["ANSIBLE_SSH_ARGS"] = ansible_ssh_args unless ansible_ssh_args.empty? - - show_ansible_playbook_command(env, command) if config.verbose - - # Write stdout and stderr data, since it's the regular Ansible output - command << { - env: env, - notify: [:stdout, :stderr], - workdir: @machine.env.root_path.to_s - } - - begin - result = Vagrant::Util::Subprocess.execute(*command) do |type, data| - if type == :stdout || type == :stderr - @machine.env.ui.info(data, new_line: false, prefix: false) - end - end - - raise Vagrant::Errors::AnsibleFailed if result.exit_code != 0 - rescue Vagrant::Util::Subprocess::LaunchError - raise Vagrant::Errors::AnsiblePlaybookAppNotFound - end - end - - protected - - # Auto-generate "safe" inventory file based on Vagrantfile, - # unless inventory_path is explicitly provided - def setup_inventory_file - return config.inventory_path if config.inventory_path - - # Managed machines - inventory_machines = {} - - generated_inventory_dir = @machine.env.local_data_path.join(File.join(%w(provisioners ansible inventory))) - FileUtils.mkdir_p(generated_inventory_dir) unless File.directory?(generated_inventory_dir) - generated_inventory_file = generated_inventory_dir.join('vagrant_ansible_inventory') - - inventory = "# Generated by Vagrant\n\n" - - @machine.env.active_machines.each do |am| - begin - m = @machine.env.machine(*am) - m_ssh_info = m.ssh_info - if !m_ssh_info.nil? - inventory += "#{m.name} ansible_ssh_host=#{m_ssh_info[:host]} ansible_ssh_port=#{m_ssh_info[:port]} ansible_ssh_private_key_file='#{m_ssh_info[:private_key_path][0]}'\n" - inventory_machines[m.name] = m - else - @logger.error("Auto-generated inventory: Impossible to get SSH information for machine '#{m.name} (#{m.provider_name})'. This machine should be recreated.") - # Let a note about this missing machine - inventory += "# MISSING: '#{m.name}' machine was probably removed without using Vagrant. This machine should be recreated.\n" - end - rescue Vagrant::Errors::MachineNotFound => e - @logger.info("Auto-generated inventory: Skip machine '#{am[0]} (#{am[1]})', which is not configured for this Vagrant environment.") - end - end - - # Write out groups information. - # All defined groups will be included, but only supported - # machines and defined child groups will be included. - # Group variables are intentionally skipped. - groups_of_groups = {} - defined_groups = [] - - config.groups.each_pair do |gname, gmembers| - # Require that gmembers be an array - # (easier to be tolerant and avoid error management of few value) - gmembers = [gmembers] if !gmembers.is_a?(Array) - - if gname.end_with?(":children") - groups_of_groups[gname] = gmembers - defined_groups << gname.sub(/:children$/, '') - elsif !gname.include?(':vars') - defined_groups << gname - inventory += "\n[#{gname}]\n" - gmembers.each do |gm| - inventory += "#{gm}\n" if inventory_machines.include?(gm.to_sym) - end - end - end - - defined_groups.uniq! - groups_of_groups.each_pair do |gname, gmembers| - inventory += "\n[#{gname}]\n" - gmembers.each do |gm| - inventory += "#{gm}\n" if defined_groups.include?(gm) - end - end - - @@lock.synchronize do - if ! File.exists?(generated_inventory_file) or - inventory != File.read(generated_inventory_file) - - generated_inventory_file.open('w') do |file| - file.write(inventory) - end - end - end - - return generated_inventory_dir.to_s - end - - def get_extra_vars_argument - if config.extra_vars.kind_of?(String) and config.extra_vars =~ /^@.+$/ - # A JSON or YAML file is referenced (requires Ansible 1.3+) - return config.extra_vars - else - # Expected to be a Hash after config validation. (extra_vars as - # JSON requires Ansible 1.2+, while YAML requires Ansible 1.3+) - return config.extra_vars.to_json - end - end - - def get_verbosity_argument - if config.verbose.to_s =~ /^v+$/ - # ansible-playbook accepts "silly" arguments like '-vvvvv' as '-vvvv' for now - return "-#{config.verbose}" - else - # safe default, in case input strays - return '-v' - end - end - - def ansible_ssh_args - @ansible_ssh_args ||= get_ansible_ssh_args - end - - # Use ANSIBLE_SSH_ARGS to pass some OpenSSH options that are not wrapped by - # an ad-hoc Ansible option. Last update corresponds to Ansible 1.8 - def get_ansible_ssh_args - ssh_options = [] - - # Use an SSH ProxyCommand when using the Docker provider with the intermediate host - if @machine.provider_name == :docker && machine.provider.host_vm? - docker_host_ssh_info = machine.provider.host_vm.ssh_info - - proxy_cmd = "ssh #{docker_host_ssh_info[:username]}@#{docker_host_ssh_info[:host]}" + - " -p #{docker_host_ssh_info[:port]} -i #{docker_host_ssh_info[:private_key_path][0]}" - - # Use same options than plugins/providers/docker/communicator.rb - # Note: this could be improved (DRY'ed) by sharing these settings. - proxy_cmd += " -o Compression=yes -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" - - proxy_cmd += " -o ForwardAgent=yes" if @ssh_info[:forward_agent] - - proxy_cmd += " exec nc %h %p 2>/dev/null" - - ssh_options << "-o ProxyCommand='#{ proxy_cmd }'" - end - - # Don't access user's known_hosts file, except when host_key_checking is enabled. - ssh_options << "-o UserKnownHostsFile=/dev/null" unless config.host_key_checking - - # Set IdentitiesOnly=yes to avoid authentication errors when the host has more than 5 ssh keys. - # Notes: - # - Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the IdentitiesOnly option. - # - this could be improved by sharing logic with lib/vagrant/util/ssh.rb - ssh_options << "-o IdentitiesOnly=yes" unless Vagrant::Util::Platform.solaris? - - # Multiple Private Keys - unless !config.inventory_path && @ssh_info[:private_key_path].size == 1 - @ssh_info[:private_key_path].each do |key| - ssh_options << "-o IdentityFile=#{key}" - end - end - - # SSH Forwarding - ssh_options << "-o ForwardAgent=yes" if @ssh_info[:forward_agent] - - # Unchecked SSH Parameters - ssh_options.concat(self.as_array(config.raw_ssh_args)) if config.raw_ssh_args - - # Re-enable ControlPersist Ansible defaults, - # which are lost when ANSIBLE_SSH_ARGS is defined. - unless ssh_options.empty? - ssh_options << "-o ControlMaster=auto" - ssh_options << "-o ControlPersist=60s" - # Intentionally keep ControlPath undefined to let ansible-playbook - # automatically sets this option to Ansible default value - end - - ssh_options.join(' ') - end - - def as_list_argument(v) - v.kind_of?(Array) ? v.join(',') : v - end - - def as_array(v) - v.kind_of?(Array) ? v : [v] - end - - def show_ansible_playbook_command(env, command) - shell_command = '' - env.each_pair do |k, v| - if k == 'ANSIBLE_SSH_ARGS' - shell_command += "#{k}='#{v}' " - else - shell_command += "#{k}=#{v} " - end - end - - shell_arg = [] - command.each do |arg| - if arg =~ /(--start-at-task|--limit)=(.+)/ - shell_arg << "#{$1}='#{$2}'" - else - shell_arg << arg - end - end - - shell_command += shell_arg.join(' ') - - @machine.env.ui.detail(shell_command) - end - end - end -end diff --git a/plugins/provisioners/ansible/provisioner/base.rb b/plugins/provisioners/ansible/provisioner/base.rb new file mode 100644 index 000000000..051473271 --- /dev/null +++ b/plugins/provisioners/ansible/provisioner/base.rb @@ -0,0 +1,151 @@ +require_relative "../errors" +require_relative "../helpers" + +module VagrantPlugins + module Ansible + module Provisioner + + # This class is a base class where the common functionality shared between + # both Ansible provisioners are stored. + # This is **not an actual provisioner**. + # Instead, {Host} (ansible) or {Guest} (ansible_local) should be used. + + class Base < Vagrant.plugin("2", :provisioner) + + protected + + def initialize(machine, config) + super + + @command_arguments = [] + @environment_variables = {} + @inventory_machines = {} + @inventory_path = nil + end + + def prepare_common_command_arguments + # By default we limit by the current machine, + # but this can be overridden by the `limit` option. + if config.limit + @command_arguments << "--limit=#{Helpers::as_list_argument(config.limit)}" + else + @command_arguments << "--limit=#{@machine.name}" + end + + @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 << "#{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 + @command_arguments << "--skip-tags=#{Helpers::as_list_argument(config.skip_tags)}" if config.skip_tags + @command_arguments << "--start-at-task=#{config.start_at_task}" if config.start_at_task + + # Finally, add the raw configuration options, which has the highest precedence + # and can therefore potentially override any other options of this provisioner. + @command_arguments.concat(Helpers::as_array(config.raw_arguments)) if config.raw_arguments + end + + def prepare_common_environment_variables + # Ensure Ansible output isn't buffered so that we receive output + # on a task-by-task basis. + @environment_variables["PYTHONUNBUFFERED"] = 1 + + # When Ansible output is piped in Vagrant integration, its default colorization is + # automatically disabled and the only way to re-enable colors is to use ANSIBLE_FORCE_COLOR. + @environment_variables["ANSIBLE_FORCE_COLOR"] = "true" if @machine.env.ui.color? + # Setting ANSIBLE_NOCOLOR is "unnecessary" at the moment, but this could change in the future + # (e.g. local provisioner [GH-2103], possible change in vagrant/ansible integration, etc.) + @environment_variables["ANSIBLE_NOCOLOR"] = "true" if !@machine.env.ui.color? + end + + # Auto-generate "safe" inventory file based on Vagrantfile, + # unless inventory_path is explicitly provided + def inventory_path + if config.inventory_path + config.inventory_path + else + @inventory_path ||= generate_inventory + end + end + + def generate_inventory + inventory = "# Generated by Vagrant\n\n" + + # This "abstract" step must fill the @inventory_machines list + # and return the list of supported host(s) + inventory += generate_inventory_machines + + inventory += generate_inventory_groups + + # This "abstract" step must create the inventory file and + # return its location path + # TODO: explain possible race conditions, etc. + @inventory_path = ship_generated_inventory(inventory) + end + + # Write out groups information. + # All defined groups will be included, but only supported + # machines and defined child groups will be included. + # Group variables are intentionally skipped. + def generate_inventory_groups + groups_of_groups = {} + defined_groups = [] + inventory_groups = "" + + config.groups.each_pair do |gname, gmembers| + # Require that gmembers be an array + # (easier to be tolerant and avoid error management of few value) + gmembers = [gmembers] if !gmembers.is_a?(Array) + + if gname.end_with?(":children") + groups_of_groups[gname] = gmembers + defined_groups << gname.sub(/:children$/, '') + elsif !gname.include?(':vars') + defined_groups << gname + inventory_groups += "\n[#{gname}]\n" + gmembers.each do |gm| + inventory_groups += "#{gm}\n" if @inventory_machines.include?(gm.to_sym) + end + end + end + + defined_groups.uniq! + groups_of_groups.each_pair do |gname, gmembers| + inventory_groups += "\n[#{gname}]\n" + gmembers.each do |gm| + inventory_groups += "#{gm}\n" if defined_groups.include?(gm) + end + end + + return inventory_groups + end + + def extra_vars_argument + if config.extra_vars.kind_of?(String) and config.extra_vars =~ /^@.+$/ + # A JSON or YAML file is referenced. + config.extra_vars + else + # Expected to be a Hash after config validation. + config.extra_vars.to_json + end + end + + def verbosity_is_enabled? + config.verbose && !config.verbose.to_s.empty? + end + + def verbosity_argument + if config.verbose.to_s =~ /^-?(v+)$/ + "-#{$+}" + else + # safe default, in case input strays + '-v' + end + end + + end + end + end +end diff --git a/plugins/provisioners/ansible/provisioner/guest.rb b/plugins/provisioners/ansible/provisioner/guest.rb new file mode 100644 index 000000000..3ac35bea1 --- /dev/null +++ b/plugins/provisioners/ansible/provisioner/guest.rb @@ -0,0 +1,135 @@ +require 'tempfile' + +require_relative "base" + +module VagrantPlugins + module Ansible + module Provisioner + class Guest < Base + + def initialize(machine, config) + super + @logger = Log4r::Logger.new("vagrant::provisioners::ansible_guest") + end + + def provision + check_and_install_ansible + prepare_common_command_arguments + prepare_common_environment_variables + execute_ansible_playbook_on_guest + end + + protected + + # + # This handles verifying the Ansible installation, installing it if it was + # requested, and so on. This method will raise exceptions if things are wrong. + # + # 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. + # + def check_and_install_ansible + @logger.info("Checking for Ansible installation...") + + # If the guest cannot check if Ansible is installed, + # print a warning and try to continue without any installation attempt... + if !@machine.guest.capability?(:ansible_installed) + @machine.ui.warn(I18n.t("vagrant.provisioners.ansible.cannot_detect")) + return + end + + # Try to install Ansible (if needed and requested) + if config.install && + (config.version == :latest || + !@machine.guest.capability(:ansible_installed, config.version)) + @machine.ui.detail(I18n.t("vagrant.provisioners.ansible.installing")) + @machine.guest.capability(:ansible_install) + end + + # Check for the existence of ansible-playbook binary on the guest, + @machine.communicate.execute( + "ansible-playbook --help", + :error_class => Ansible::Errors::AnsibleError, + :error_key => :ansible_playbook_app_not_found_on_guest) + + # Check if requested ansible version is available + if (!config.version.empty? && + config.version != :latest && + !@machine.guest.capability(:ansible_installed, config.version)) + raise Ansible::Errors::AnsibleVersionNotFoundOnGuest, required_version: config.version.to_s + end + end + + def execute_ansible_playbook_on_guest + command = (%w(ansible-playbook) << @command_arguments << config.playbook).flatten + remote_command = "cd #{config.provisioning_path} && #{Helpers::stringify_ansible_playbook_command(@environment_variables, command)}" + + # TODO: generic HOOK ??? + # Show the ansible command in use + if verbosity_is_enabled? + @machine.env.ui.detail(remote_command) + end + + result = execute_on_guest(remote_command) + raise Ansible::Errors::AnsiblePlaybookAppFailed if result != 0 + end + + def execute_on_guest(command) + @machine.communicate.execute(command, :error_check => false) do |type, data| + if [:stderr, :stdout].include?(type) + @machine.env.ui.info(data, :new_line => false, :prefix => false) + end + end + end + + def ship_generated_inventory(inventory_content) + inventory_basedir = File.join(config.tmp_path, "inventory") + inventory_path = File.join(inventory_basedir, "vagrant_ansible_local_inventory") + + temp_inventory = Tempfile.new("vagrant_ansible_local_inventory_#{@machine.name}") + temp_inventory.write(inventory_content) + temp_inventory.close + + create_and_chown_remote_folder(inventory_basedir) + @machine.communicate.tap do |comm| + comm.sudo("rm -f #{inventory_path}", error_check: false) + comm.upload(temp_inventory.path, inventory_path) + end + + return inventory_path + end + + def generate_inventory_machines + machines = "" + + # TODO: Instead, why not loop over active_machines and skip missing guests, like in Host? + machine.env.machine_names.each do |machine_name| + begin + @inventory_machines[machine_name] = machine_name + if @machine.name == machine_name + machines += "#{machine_name} ansible_connection=local\n" + else + machines += "#{machine_name}\n" + end + end + end + + return machines + end + + def create_and_chown_remote_folder(path) + @machine.communicate.tap do |comm| + comm.sudo("mkdir -p #{path}") + comm.sudo("chown -h #{@machine.ssh_info[:username]} #{path}") + end + end + + end + end + end +end diff --git a/plugins/provisioners/ansible/provisioner/host.rb b/plugins/provisioners/ansible/provisioner/host.rb new file mode 100644 index 000000000..c72fa8728 --- /dev/null +++ b/plugins/provisioners/ansible/provisioner/host.rb @@ -0,0 +1,198 @@ +require "thread" + +require_relative "base" + +module VagrantPlugins + module Ansible + module Provisioner + class Host < Base + + @@lock = Mutex.new + + def initialize(machine, config) + super + @logger = Log4r::Logger.new("vagrant::provisioners::ansible_host") + end + + def provision + # At this stage, the SSH access is guaranteed to be ready + @ssh_info = @machine.ssh_info + + warn_for_unsupported_platform + prepare_command_arguments + prepare_environment_variables + execute_ansible_playbook_from_host + end + + protected + + 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")) + end + end + + def prepare_command_arguments + # By default, connect with Vagrant SSH username + @command_arguments << "--user=#{@ssh_info[:username]}" + + # Connect with native OpenSSH client + # Other modes (e.g. paramiko) are not officially supported, + # but can be enabled via raw_arguments option. + @command_arguments << "--connection=ssh" + + # Increase the SSH connection timeout, as the Ansible default value (10 seconds) + # is a bit demanding for some overloaded developer boxes. This is particularly + # helpful when additional virtual networks are configured, as their availability + # is not controlled during vagrant boot process. + @command_arguments << "--timeout=30" + + @command_arguments << "--ask-sudo-pass" if config.ask_sudo_pass + @command_arguments << "--ask-vault-pass" if config.ask_vault_pass + + prepare_common_command_arguments + end + + + def prepare_environment_variables + prepare_common_environment_variables + + # Some Ansible options must be passed as environment variables, + # as there is no equivalent command line arguments + @environment_variables["ANSIBLE_HOST_KEY_CHECKING"] = "#{config.host_key_checking}" + + # ANSIBLE_SSH_ARGS is required for Multiple SSH keys, SSH forwarding and custom SSH settings + @environment_variables["ANSIBLE_SSH_ARGS"] = ansible_ssh_args unless ansible_ssh_args.empty? + end + + def execute_ansible_playbook_from_host + # Assemble the full ansible-playbook command + command = (%w(ansible-playbook) << @command_arguments << config.playbook).flatten + + # TODO: generic HOOK ??? + # Show the ansible command in use + if verbosity_is_enabled? + @machine.env.ui.detail(Helpers::stringify_ansible_playbook_command(@environment_variables, command)) + end + + # Write stdout and stderr data, since it's the regular Ansible output + command << { + env: @environment_variables, + notify: [:stdout, :stderr], + workdir: @machine.env.root_path.to_s + } + + begin + result = Vagrant::Util::Subprocess.execute(*command) do |type, data| + if type == :stdout || type == :stderr + @machine.env.ui.info(data, new_line: false, prefix: false) + end + end + raise Ansible::Errors::AnsiblePlaybookAppFailed if result.exit_code != 0 + rescue Vagrant::Errors::CommandUnavailable + raise Ansible::Errors::AnsiblePlaybookAppNotFoundOnHost + end + end + + def ship_generated_inventory(inventory_content) + inventory_path = Pathname.new(File.join(@machine.env.local_data_path.join, %w(provisioners ansible inventory))) + FileUtils.mkdir_p(inventory_path) unless File.directory?(inventory_path) + + inventory_file = Pathname.new(File.join(inventory_path, 'vagrant_ansible_inventory')) + @@lock.synchronize do + if !File.exists?(inventory_file) or inventory_content != File.read(inventory_file) + inventory_file.open('w') do |file| + file.write(inventory_content) + end + end + end + + return inventory_path + end + + def generate_inventory_machines + machines = "" + + @machine.env.active_machines.each do |am| + begin + m = @machine.env.machine(*am) + m_ssh_info = m.ssh_info + if !m_ssh_info.nil? + machines += "#{m.name} ansible_ssh_host=#{m_ssh_info[:host]} ansible_ssh_port=#{m_ssh_info[:port]} ansible_ssh_private_key_file='#{m_ssh_info[:private_key_path][0]}'\n" + @inventory_machines[m.name] = m + else + @logger.error("Auto-generated inventory: Impossible to get SSH information for machine '#{m.name} (#{m.provider_name})'. This machine should be recreated.") + # Let a note about this missing machine + machines += "# MISSING: '#{m.name}' machine was probably removed without using Vagrant. This machine should be recreated.\n" + end + rescue Vagrant::Errors::MachineNotFound => e + @logger.info("Auto-generated inventory: Skip machine '#{am[0]} (#{am[1]})', which is not configured for this Vagrant environment.") + end + end + + return machines + end + + def ansible_ssh_args + @ansible_ssh_args ||= prepare_ansible_ssh_args + end + + def prepare_ansible_ssh_args + ssh_options = [] + + # Use an SSH ProxyCommand when using the Docker provider with the intermediate host + if @machine.provider_name == :docker && machine.provider.host_vm? + docker_host_ssh_info = machine.provider.host_vm.ssh_info + + proxy_cmd = "ssh #{docker_host_ssh_info[:username]}@#{docker_host_ssh_info[:host]}" + + " -p #{docker_host_ssh_info[:port]} -i #{docker_host_ssh_info[:private_key_path][0]}" + + # Use same options than plugins/providers/docker/communicator.rb + # Note: this could be improved (DRY'ed) by sharing these settings. + proxy_cmd += " -o Compression=yes -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" + + proxy_cmd += " -o ForwardAgent=yes" if @ssh_info[:forward_agent] + + proxy_cmd += " exec nc %h %p 2>/dev/null" + + ssh_options << "-o ProxyCommand='#{ proxy_cmd }'" + end + + # Don't access user's known_hosts file, except when host_key_checking is enabled. + ssh_options << "-o UserKnownHostsFile=/dev/null" unless config.host_key_checking + + # Set IdentitiesOnly=yes to avoid authentication errors when the host has more than 5 ssh keys. + # Notes: + # - Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the IdentitiesOnly option. + # - this could be improved by sharing logic with lib/vagrant/util/ssh.rb + ssh_options << "-o IdentitiesOnly=yes" unless Vagrant::Util::Platform.solaris? + + # Multiple Private Keys + unless !config.inventory_path && @ssh_info[:private_key_path].size == 1 + @ssh_info[:private_key_path].each do |key| + ssh_options << "-o IdentityFile=#{key}" + end + end + + # SSH Forwarding + ssh_options << "-o ForwardAgent=yes" if @ssh_info[:forward_agent] + + # Unchecked SSH Parameters + ssh_options.concat(Helpers::as_array(config.raw_ssh_args)) if config.raw_ssh_args + + # Re-enable ControlPersist Ansible defaults, + # which are lost when ANSIBLE_SSH_ARGS is defined. + unless ssh_options.empty? + ssh_options << "-o ControlMaster=auto" + ssh_options << "-o ControlPersist=60s" + # Intentionally keep ControlPath undefined to let ansible-playbook + # automatically sets this option to Ansible default value + end + + ssh_options.join(' ') + end + + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 3b7094310..dcd7fef14 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -371,17 +371,6 @@ en: Machine name: %{name} Active provider: %{active_provider} Requested provider: %{requested_provider} - ansible_failed: |- - Ansible failed to complete successfully. Any error output should be - visible above. Please fix these errors and try again. - ansible_playbook_app_not_found: |- - The "ansible-playbook" program could not be found! Please verify - that "ansible-playbook" is available on the PATH of your host - system, and try again. - - If you haven't installed Ansible yet, please install Ansible - on your system. Vagrant can't do this for you in a safe, automated - way. Please see ansible.cc for more info. batch_multi_error: |- An error occurred while executing multiple actions in parallel. Any errors that occurred are shown below. @@ -2050,11 +2039,52 @@ en: upload_path_not_set: "`upload_path` must be set for the shell provisioner." ansible: - no_playbook: "`playbook` must be set for the Ansible provisioner." - playbook_path_invalid: "`playbook` for the Ansible provisioner does not exist on the host system: %{path}" - inventory_path_invalid: "`inventory_path` for the Ansible provisioner does not exist on the host system: %{path}" - vault_password_file_invalid: "`vault_password_file` for the Ansible provisioner does not exist on the host system: %{path}" - extra_vars_invalid: "`extra_vars` for the Ansible provisioner must be a hash or a path to an existing file. Received: %{value} (as %{type})" + errors: + ansible_playbook_app_failed: |- + Ansible failed to complete successfully. Any error output should be + visible above. Please fix these errors and try again. + ansible_playbook_app_not_found_on_guest: |- + The "ansible-playbook" program 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 + http://docs.vagrantup.com/v2/provisioning/ansible_local.html + for more information. + 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 http://docs.vagrantup.com/v2/provisioning/ansible_local.html + for more information. + ansible_playbook_app_not_found_on_host: |- + The "ansible-playbook" program could not be found! Please verify + that Ansible is correctly installed on your host system. + + If you haven't installed Ansible yet, please install Ansible + on your host system. Vagrant can't do this for you in a safe and + automated way. + Please check http://docs.ansible.com for more information. + extra_vars_invalid: |- + `extra_vars` for the Ansible provisioner must be a hash or a path to an existing file. Received: %{value} (as %{type}) + inventory_path_invalid: |- + `inventory_path` for the Ansible provisioner does not exist on the %{system}: %{path} + no_playbook: |- + `playbook` must be set for the Ansible provisioner. + playbook_path_invalid: |- + `playbook` for the Ansible provisioner does not exist on the %{system}: %{path} + vault_password_file_invalid: |- + `vault_password_file` for the Ansible provisioner does not exist on the %{system}: %{path} + cannot_detect: |- + Vagrant does not support detecting whether Ansible is installed + for the guest OS running in the machine. Vagrant will assume it is + installed and attempt to continue. + installing: |- + Installing Ansible... + windows_not_supported_for_control_machine: |- + Windows is not officially supported for the Ansible Control Machine. + Please check http://docs.ansible.com/intro_installation.html#control-machine-requirements docker: not_running: "Docker is not running on the guest VM." diff --git a/test/unit/plugins/provisioners/ansible/config_test.rb b/test/unit/plugins/provisioners/ansible/config_test.rb index ab1b6dca0..c4d76bcf3 100644 --- a/test/unit/plugins/provisioners/ansible/config_test.rb +++ b/test/unit/plugins/provisioners/ansible/config_test.rb @@ -1,9 +1,9 @@ require_relative "../../../base" require_relative "../support/shared/config" -require Vagrant.source_root.join("plugins/provisioners/ansible/config") +require Vagrant.source_root.join("plugins/provisioners/ansible/config/host") -describe VagrantPlugins::Ansible::Config do +describe VagrantPlugins::Ansible::Config::Host do include_context "unit" subject { described_class.new } @@ -15,6 +15,7 @@ describe VagrantPlugins::Ansible::Config do it "supports a list of options" do config_options = subject.public_methods(false).find_all { |i| i.to_s.end_with?('=') } config_options.map! { |i| i.to_s.sub('=', '') } + supported_options = %w( ask_sudo_pass ask_vault_pass extra_vars @@ -33,7 +34,7 @@ describe VagrantPlugins::Ansible::Config do vault_password_file verbose ) - expect(config_options.sort).to eql(supported_options) + expect(get_provisioner_option_names(described_class)).to eql(supported_options) end it "assigns default values to unset options" do @@ -47,7 +48,7 @@ describe VagrantPlugins::Ansible::Config do expect(subject.limit).to be_nil expect(subject.sudo).to be_false expect(subject.sudo_user).to be_nil - expect(subject.verbose).to be_nil + expect(subject.verbose).to be_false expect(subject.tags).to be_nil expect(subject.skip_tags).to be_nil expect(subject.start_at_task).to be_nil @@ -79,7 +80,7 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([]) + expect(result["ansible remote provisioner"]).to eql([]) end it "returns an error if the playbook option is undefined" do @@ -87,7 +88,7 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([ + expect(result["ansible remote provisioner"]).to eql([ I18n.t("vagrant.provisioners.ansible.no_playbook") ]) end @@ -97,9 +98,9 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([ + expect(result["ansible remote provisioner"]).to eql([ I18n.t("vagrant.provisioners.ansible.playbook_path_invalid", - path: non_existing_file) + path: non_existing_file, system: "host") ]) end @@ -108,7 +109,7 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([]) + expect(result["ansible remote provisioner"]).to eql([]) end it "passes if the extra_vars option is a hash" do @@ -116,7 +117,7 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([]) + expect(result["ansible remote provisioner"]).to eql([]) end it "returns an error if the extra_vars option refers to a file that does not exist" do @@ -124,7 +125,7 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([ + expect(result["ansible remote provisioner"]).to eql([ I18n.t("vagrant.provisioners.ansible.extra_vars_invalid", type: subject.extra_vars.class.to_s, value: subject.extra_vars.to_s) @@ -136,7 +137,7 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([ + expect(result["ansible remote provisioner"]).to eql([ I18n.t("vagrant.provisioners.ansible.extra_vars_invalid", type: subject.extra_vars.class.to_s, value: subject.extra_vars.to_s) @@ -148,7 +149,7 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([]) + expect(result["ansible remote provisioner"]).to eql([]) end it "returns an error if inventory_path is specified, but does not exist" do @@ -156,9 +157,9 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([ + expect(result["ansible remote provisioner"]).to eql([ I18n.t("vagrant.provisioners.ansible.inventory_path_invalid", - path: non_existing_file) + path: non_existing_file, system: "host") ]) end @@ -167,9 +168,9 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to eql([ + expect(result["ansible remote provisioner"]).to eql([ I18n.t("vagrant.provisioners.ansible.vault_password_file_invalid", - path: non_existing_file) + path: non_existing_file, system: "host") ]) end @@ -180,16 +181,16 @@ describe VagrantPlugins::Ansible::Config do subject.finalize! result = subject.validate(machine) - expect(result["ansible provisioner"]).to include( + expect(result["ansible remote provisioner"]).to include( I18n.t("vagrant.provisioners.ansible.playbook_path_invalid", - path: non_existing_file)) - expect(result["ansible provisioner"]).to include( + path: non_existing_file, system: "host")) + expect(result["ansible remote provisioner"]).to include( I18n.t("vagrant.provisioners.ansible.extra_vars_invalid", type: subject.extra_vars.class.to_s, value: subject.extra_vars.to_s)) - expect(result["ansible provisioner"]).to include( + expect(result["ansible remote provisioner"]).to include( I18n.t("vagrant.provisioners.ansible.inventory_path_invalid", - path: non_existing_file)) + path: non_existing_file, system: "host")) end end diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index 24863adc1..29e744a1f 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -1,7 +1,7 @@ require_relative "../../../base" -require Vagrant.source_root.join("plugins/provisioners/ansible/config") -require Vagrant.source_root.join("plugins/provisioners/ansible/provisioner") +require Vagrant.source_root.join("plugins/provisioners/ansible/config/host") +require Vagrant.source_root.join("plugins/provisioners/ansible/provisioner/host") # # Helper Functions @@ -15,7 +15,7 @@ def find_last_argument_after(ref_index, ansible_playbook_args, arg_pattern) return false end -describe VagrantPlugins::Ansible::Provisioner do +describe VagrantPlugins::Ansible::Provisioner::Host do include_context "unit" subject { described_class.new(machine, config) } @@ -37,7 +37,7 @@ VF end let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } - let(:config) { VagrantPlugins::Ansible::Config.new } + let(:config) { VagrantPlugins::Ansible::Config::Host.new } let(:ssh_info) {{ private_key_path: ['/path/to/my/key'], username: 'testuser', @@ -202,7 +202,7 @@ VF config.finalize! Vagrant::Util::Subprocess.stub(execute: Vagrant::Util::Subprocess::Result.new(1, "", "")) - expect {subject.provision}.to raise_error(Vagrant::Errors::AnsibleFailed) + expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsiblePlaybookAppFailed) end end @@ -215,16 +215,13 @@ VF inventory_content = File.read(generated_inventory_file) expect(inventory_content).to_not match(/^\s*\[^\\+\]\s*$/) - # Note: - # The expectation below is a workaround to a possible misuse (or bug) in RSpec/Ruby stack. - # If 'args' variable is not required by in this block, the "Vagrant::Util::Subprocess).to receive" - # surprisingly expects to receive "no args". - # This problem can be "solved" by using args the "unnecessary" (but harmless) expectation below: - expect(args.length).to be > 0 + # Ending this block with a negative expectation (to_not / not_to) + # would lead to a failure of the above expectation. + true } end - it "does not show the ansible-playbook command" do + it "doesn't show the ansible-playbook command" do expect(machine.env.ui).not_to receive(:detail).with { |full_command| expect(full_command).to include("ansible-playbook") } @@ -463,19 +460,69 @@ VF end end - describe "with verbose option" do - before do - config.verbose = 'v' + context "with verbose option defined" do + %w(vv vvvv).each do |verbose_option| + + describe "with a value of '#{verbose_option}'" do + before do + config.verbose = verbose_option + end + + it_should_set_arguments_and_environment_variables 7 + it_should_set_optional_arguments({ "verbose" => "-#{verbose_option}" }) + + it "shows the ansible-playbook command and set verbosity to '-#{verbose_option}' level" do + expect(machine.env.ui).to receive(:detail).with { |full_command| + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --user=testuser --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} -#{verbose_option} playbook.yml") + } + end + end + + describe "with a value of '-#{verbose_option}'" do + before do + config.verbose = "-#{verbose_option}" + end + + it_should_set_arguments_and_environment_variables 7 + it_should_set_optional_arguments({ "verbose" => "-#{verbose_option}" }) + + it "shows the ansible-playbook command and set verbosity to '-#{verbose_option}' level" do + expect(machine.env.ui).to receive(:detail).with { |full_command| + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --user=testuser --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} -#{verbose_option} playbook.yml") + } + end + end end - it_should_set_arguments_and_environment_variables 7 - it_should_set_optional_arguments({ "verbose" => "-v" }) + describe "with an invalid string" do + before do + config.verbose = "wrong" + end - it "shows the ansible-playbook command" do - expect(machine.env.ui).to receive(:detail).with { |full_command| - expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_FORCE_COLOR=true ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --user=testuser --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} -v playbook.yml") - } + it_should_set_arguments_and_environment_variables 7 + it_should_set_optional_arguments({ "verbose" => "-v" }) + + it "shows the ansible-playbook command and set verbosity to '-v' level" do + expect(machine.env.ui).to receive(:detail).with { |full_command| + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --user=testuser --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} -v playbook.yml") + } + end end + + describe "with an empty string" do + before do + config.verbose = "" + end + + it_should_set_arguments_and_environment_variables + + it "doesn't show the ansible-playbook command" do + expect(machine.env.ui).not_to receive(:detail).with { |full_command| + expect(full_command).to include("ansible-playbook") + } + end + end + end describe "without colorized output" do @@ -492,9 +539,8 @@ VF end end - # Note: - # The Vagrant Ansible provisioner does not validate the coherency of argument combinations, - # and let ansible-playbook complain. + # The Vagrant Ansible provisioner does not validate the coherency of + # argument combinations, and let ansible-playbook complain. describe "with a maximum of options" do before do # vagrant general options @@ -547,7 +593,7 @@ VF it "shows the ansible-playbook command, with additional quotes when required" do expect(machine.env.ui).to receive(:detail).with { |full_command| - expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_FORCE_COLOR=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 --user=testuser --connection=ssh --timeout=30 --limit='machine*:&vagrant:!that_one' --inventory-file=#{generated_inventory_dir} --extra-vars=@#{File.expand_path(__FILE__)} --sudo --sudo-user=deployer -vvv --ask-sudo-pass --ask-vault-pass --vault-password-file=#{File.expand_path(__FILE__)} --tags=db,www --skip-tags=foo,bar --start-at-task='an awesome task' --why-not --su-user=foot --ask-su-pass --limit='all' --private-key=./myself.key playbook.yml") + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true 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 --user=testuser --connection=ssh --timeout=30 --ask-sudo-pass --ask-vault-pass --limit='machine*:&vagrant:!that_one' --inventory-file=#{generated_inventory_dir} --extra-vars=@#{File.expand_path(__FILE__)} --sudo --sudo-user=deployer -vvv --vault-password-file=#{File.expand_path(__FILE__)} --tags=db,www --skip-tags=foo,bar --start-at-task='an awesome task' --why-not --su-user=foot --ask-su-pass --limit='all' --private-key=./myself.key playbook.yml") } end end @@ -598,12 +644,9 @@ VF cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to_not include("-o IdentitiesOnly=yes") - # Note: - # The expectation below is a workaround to a possible misuse (or bug) in RSpec/Ruby stack. - # If 'args' variable is not required by in this block, the "Vagrant::Util::Subprocess).to receive" - # surprisingly expects to receive "no args". - # This problem can be "solved" by using args the "unnecessary" (but harmless) expectation below: - expect(true).to be_true + # Ending this block with a negative expectation (to_not / not_to) + # would lead to a failure of the above expectation. + true } end @@ -615,12 +658,9 @@ VF cmd_opts = args.last expect(cmd_opts[:env]).to_not include('ANSIBLE_SSH_ARGS') - # Note: - # The expectation below is a workaround to a possible misuse (or bug) in RSpec/Ruby stack. - # If 'args' variable is not required by in this block, the "Vagrant::Util::Subprocess).to receive" - # surprisingly expects to receive "no args". - # This problem can be "solved" by using args the "unnecessary" (but harmless) expectation below: - expect(true).to be_true + # Ending this block with a negative expectation (to_not / not_to) + # would lead to a failure of the above expectation. + true } end end diff --git a/test/unit/plugins/provisioners/support/shared/config.rb b/test/unit/plugins/provisioners/support/shared/config.rb index 052a10b4a..db7aaaeaa 100644 --- a/test/unit/plugins/provisioners/support/shared/config.rb +++ b/test/unit/plugins/provisioners/support/shared/config.rb @@ -1,3 +1,9 @@ +def get_provisioner_option_names(provisioner_class) + config_options = provisioner_class.instance_methods(true).find_all { |i| i.to_s.end_with?('=') } + config_options.map! { |i| i.to_s.sub('=', '') } + (config_options - ["!", "=", "=="]).sort +end + shared_examples_for 'any VagrantConfigProvisioner strict boolean attribute' do |attr_name, attr_default_value| [true, false].each do |bool| diff --git a/website/docs/source/layouts/layout.erb b/website/docs/source/layouts/layout.erb index 259577d79..bd7b056f4 100644 --- a/website/docs/source/layouts/layout.erb +++ b/website/docs/source/layouts/layout.erb @@ -160,6 +160,7 @@ >File >Shell >Ansible + >Ansible Local >CFEngine >Chef Solo >Chef Zero diff --git a/website/docs/source/v2/provisioning/ansible.html.md b/website/docs/source/v2/provisioning/ansible.html.md index 381cf5bd1..ca7bdf87c 100644 --- a/website/docs/source/v2/provisioning/ansible.html.md +++ b/website/docs/source/v2/provisioning/ansible.html.md @@ -5,16 +5,9 @@ sidebar_current: "provisioning-ansible" # Ansible Provisioner -**Provisioner name: `"ansible"`** +**Provisioner name: `ansible`** -The ansible provisioner allows you to provision the guest using -[Ansible](http://ansible.com) playbooks by executing `ansible-playbook` from the Vagrant host. - -Ansible playbooks are [YAML](http://en.wikipedia.org/wiki/YAML) documents that -comprise the set of steps to be orchestrated on one or more machines. This documentation -page will not go into how to use Ansible or how to write Ansible playbooks, since Ansible -is a complete deployment and configuration management system that is beyond the scope of -a single page of documentation. +The Ansible provisioner allows you to provision the guest using [Ansible](http://ansible.com) playbooks by executing **`ansible-playbook` from the Vagrant host**.

    @@ -27,199 +20,54 @@ a single page of documentation. ## Setup Requirements -* [Install Ansible](http://docs.ansible.com/intro_installation.html#installing-the-control-machine) on your Vagrant host. -* Your Vagrant host should ideally provide a recent version of OpenSSH that [supports ControlPersist](http://docs.ansible.com/faq.html#how-do-i-get-ansible-to-reuse-connections-enable-kerberized-ssh-or-have-ansible-pay-attention-to-my-local-ssh-config-file) + - **[Install Ansible](http://docs.ansible.com/intro_installation.html#installing-the-control-machine) on your Vagrant host**. -## Inventory File + - Your Vagrant host should ideally provide a recent version of OpenSSH that [supports ControlPersist](http://docs.ansible.com/faq.html#how-do-i-get-ansible-to-reuse-connections-enable-kerberized-ssh-or-have-ansible-pay-attention-to-my-local-ssh-config-file). -When using Ansible, it needs to know on which machines a given playbook should run. It does -this by way of an [inventory](http://docs.ansible.com/intro_inventory.html) file which lists those machines. -In the context of Vagrant, there are two ways to approach working with inventory files. +If installing Ansible directly on the Vagrant host is not an option in your development environment, you might be looking for the Ansible Local provisioner alternative. -### Auto-Generated Inventory +## Usage -The first and simplest option is to not provide one to Vagrant at all. Vagrant will generate an -inventory file encompassing all of the virtual machines it manages, and use it for provisioning -machines. The generated inventory file is stored as part of your local Vagrant environment in `.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory`. +This page only documents the specific parts of the `ansible` (remote) provisioner. General Ansible concepts like Playbook or Inventory are shortly explained in the [introduction to Ansible and Vagrant](/v2/provisioning/ansible_intro.html). -**Groups of Hosts** +### Simplest Configuration -The `ansible.groups` option can be used to pass a hash of group names and group members to be included in the generated inventory file. - -With this configuration example: - -``` -ansible.groups = { - "group1" => ["machine1"], - "group2" => ["machine2"], - "all_groups:children" => ["group1", "group2"] -} -``` - -Vagrant would generate an inventory file that might look like: - -``` -# Generated by Vagrant - -machine1 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2200 ansible_ssh_private_key_file=/home/.../.vagrant/machines/machine1/virtualbox/private_key -machine2 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2201 ansible_ssh_private_key_file=/home/.../.vagrant/machines/machine2/virtualbox/private_key - -[group1] -machine1 - -[group2] -machine2 - -[all_groups:children] -group1 -group2 -``` - -**Notes** - - * The generation of group variables blocks (e.g. `[group1:vars]`) are intentionally not supported, as it is [not recommended to store group variables in the main inventory file](http://docs.ansible.com/intro_inventory.html#splitting-out-host-and-group-specific-data). A good practice is to store these group (or host) variables in `YAML` files stored in `group_vars/` or `host_vars/` directories in the playbook (or inventory) directory. - * Unmanaged machines and undefined groups are not added to the inventory, to avoid useless Ansible errors (e.g. *unreachable host* or *undefined child group*) - * Prior to Vagrant 1.7.3, the `ansible_ssh_private_key_file` variable was not set in generated inventory, but passed as command line argument to `ansible-playbook` command. - -For example, `machine3`, `group3` and `group1:vars` in the example below would not be added to the generated inventory file: - -``` -ansible.groups = { - "group1" => ["machine1"], - "group2" => ["machine2", "machine3"], - "all_groups:children" => ["group1", "group2", "group3"], - "group1:vars" => { "variable1" => 9, "variable2" => "example" } -} -``` - -### Static Inventory - -The second option is for situations where you'd like to have more control over the inventory management. -With the `ansible.inventory_path` option, you can reference a specific inventory resource (e.g. a static inventory file, a [dynamic inventory script](http://docs.ansible.com/intro_dynamic_inventory.html) or even [multiple inventories stored in the same directory](http://docs.ansible.com/intro_dynamic_inventory.html#using-multiple-inventory-sources)). Vagrant will then use this inventory information instead of generating it. - -A very simple inventory file for use with Vagrant might look like: - -``` -default ansible_ssh_host=192.168.111.222 -``` - -Where the above IP address is one set in your Vagrantfile: - -``` -config.vm.network :private_network, ip: "192.168.111.222" -``` - -**Notes:** - - * The machine names in `Vagrantfile` and `ansible.inventory_path` files should correspond, unless you use `ansible.limit` option to reference the correct machines. - * The SSH host addresses (and ports) must obviously be specified twice, in `Vagrantfile` and `ansible.inventory_path` files. - -## Playbook - -The second component of a successful Ansible provisioner setup is the Ansible playbook -which contains the steps that should be run on the guest. Ansible's -[playbook documentation](http://docs.ansible.com/playbooks.html) goes into great -detail on how to author playbooks, and there are a number of -[best practices](http://docs.ansible.com/playbooks_best_practices.html) that can be applied to use -Ansible's powerful features effectively. A playbook that installs and starts (or restarts -if it was updated) the NTP daemon via YUM looks like: - -``` ---- -- hosts: all - tasks: - - name: ensure ntpd is at the latest version - yum: pkg=ntp state=latest - notify: - - restart ntpd - handlers: - - name: restart ntpd - service: name=ntpd state=restarted -``` - -You can of course target other operating systems that don't have YUM by changing the -playbook tasks. Ansible ships with a number of [modules](http://docs.ansible.com/modules.html) -that make running otherwise tedious tasks dead simple. - -## Running Ansible - -To run Ansible against your Vagrant guest, the basic Vagrantfile configuration looks like: +To run Ansible against your Vagrant guest, the basic `Vagrantfile` configuration looks like: ```ruby -Vagrant.configure("2") do |config| +Vagrant.configure(2) do |config| + + # + # Run Ansible from the Vagrant Host + # config.vm.provision "ansible" do |ansible| ansible.playbook = "playbook.yml" end + end ``` -Since an Ansible playbook can include many files, you may also collect the related files in -a directory structure like this: +## Options -``` -$ tree -. -|-- Vagrantfile -|-- provisioning -| |-- group_vars -| |-- all -| |-- playbook.yml -``` +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](/v2/provisioning/ansible_common.html). -In such an arrangement, the `ansible.playbook` path should be adjusted accordingly: +- `ask_sudo_pass` (boolean) - require Ansible to [prompt for a sudo password](http://docs.ansible.com/intro_getting_started.html#remote-connection-information). -```ruby -Vagrant.configure("2") do |config| - config.vm.provision "ansible" do |ansible| - ansible.playbook = "provisioning/playbook.yml" - end -end -``` + The default value is `false`. -Vagrant will try to run the `playbook.yml` playbook against all machines defined in your Vagrantfile. +- `ask_vault_pass` (boolean) - require Ansible to [prompt for a vault password](http://docs.ansible.com/playbooks_vault.html#vault). -**Backward Compatibility Note**: + The default value is `false`. -Up to Vagrant 1.4, the Ansible provisioner could potentially connect (multiple times) to all hosts from the inventory file. -This behaviour is still possible by setting `ansible.limit = 'all'` (see more details below). +- `host_key_checking` (boolean) - require Ansible to [enable SSH host key checking](http://docs.ansible.com/intro_getting_started.html#host-key-checking). -## Additional Options + The default value is `false`. -The Ansible provisioner also includes a number of additional options that can be set, -all of which get passed to the `ansible-playbook` command that ships with Ansible. +- `raw_ssh_args` (array of strings) - require Ansible to apply a list of OpenSSH client options. -* `ansible.extra_vars` can be used to pass additional variables (with highest priority) to the playbook. This parameter can be a path to a JSON or YAML file, or a hash. For example: + Example: `['-o ControlMaster=no']`. - ``` - ansible.extra_vars = { - ntp_server: "pool.ntp.org", - nginx: { - port: 8008, - workers: 4 - } - } - ``` - These variables take the highest precedence over any other variables. -* `ansible.sudo` can be set to `true` to cause Ansible to perform commands using sudo. -* `ansible.sudo_user` can be set to a string containing a username on the guest who should be used -by the sudo command. -* `ansible.ask_sudo_pass` can be set to `true` to require Ansible to prompt for a sudo password. -* `ansible.ask_vault_pass` can be set to `true` to require Ansible to prompt for a vault password. -* `ansible.vault_password_file` can be set to a string containing the path of a file containing the password used by Ansible Vault. -* `ansible.limit` can be set to a string or an array of machines or groups from the inventory file to further control which hosts are affected. Note that: - * As of Vagrant 1.5, the machine name (taken from Vagrantfile) is set as **default limit** to ensure that `vagrant provision` steps only affect the expected machine. Setting `ansible.limit` will override this default. - * Setting `ansible.limit = 'all'` can be used to make Ansible connect to all machines from the inventory file. -* `ansible.verbose` can be set to increase Ansible's verbosity to obtain detailed logging: - * `'v'`, verbose mode - * `'vv'` - * `'vvv'`, more - * `'vvvv'`, connection debugging -* `ansible.tags` can be set to a string or an array of tags. Only plays, roles and tasks tagged with these values will be executed. -* `ansible.skip_tags` can be set to a string or an array of tags. Only plays, roles and tasks that *do not match* these values will be executed. -* `ansible.start_at_task` can be set to a string corresponding to the task name where the playbook provision will start. -* `ansible.raw_arguments` can be set to an array of strings corresponding to a list of `ansible-playbook` arguments (e.g. `['--check', '-M /my/modules']`). It is an *unsafe wildcard* that can be used to apply Ansible options that are not (yet) supported by this Vagrant provisioner. As of Vagrant 1.7, `raw_arguments` has the highest priority and its values can potentially override or break other Vagrant settings. -* `ansible.raw_ssh_args` can be set to an array of strings corresponding to a list of OpenSSH client parameters (e.g. `['-o ControlMaster=no']`). It is an *unsafe wildcard* that can be used to pass additional SSH settings to Ansible via `ANSIBLE_SSH_ARGS` environment variable. -* `ansible.host_key_checking` can be set to `true` which will enable host key checking. As of Vagrant 1.5, the default value is `false` and as of Vagrant 1.7 the user known host file (e.g. `~/.ssh/known_hosts`) is no longer read nor modified. In other words: by default, the Ansible provisioner behaves the same as Vagrant native commands (e.g `vagrant ssh`). + It is an *unsafe wildcard* that can be used to pass additional SSH settings to Ansible via `ANSIBLE_SSH_ARGS` environment variable, overriding any other SSH arguments (e.g. defined in an [`ansible.cfg` configuration file](http://docs.ansible.com/intro_configuration.html#ssh-args)). ## Tips and Tricks @@ -265,15 +113,7 @@ end If you apply this parallel provisioning pattern with a static Ansible inventory, you'll 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](/v2/vagrantfile/ssh_settings.html)). -### Provide a local `ansible.cfg` file - -Certain settings in Ansible are (only) adjustable via a [configuration file](http://docs.ansible.com/intro_configuration.html), and you might want to ship such a file in your Vagrant project. - -As `ansible-playbook` command looks for local `ansible.cfg` configuration file in its *current directory* (but not in the directory that contains the main playbook), you have to store this file adjacent to your Vagrantfile. - -Note that it is also possible to reference an Ansible configuration file via `ANSIBLE_CONFIG` environment variable, if you want to be flexible about the location of this file. - -### Why does the Ansible provisioner connect as the wrong user? +### Troubleshooting SSH Connection Errors It is good to know that the following Ansible settings always override the `config.ssh.username` option defined in [Vagrant SSH Settings](/v2/vagrantfile/ssh_settings.html): @@ -304,7 +144,6 @@ The Ansible provisioner is implemented with native OpenSSH support in mind, and If you really need to use this connection mode, it is though possible to enable paramiko as illustrated in the following configuration examples: - With auto-generated inventory: ``` diff --git a/website/docs/source/v2/provisioning/ansible_common.html.md b/website/docs/source/v2/provisioning/ansible_common.html.md new file mode 100644 index 000000000..3b6d7fbc1 --- /dev/null +++ b/website/docs/source/v2/provisioning/ansible_common.html.md @@ -0,0 +1,86 @@ +--- +page_title: "Common Ansible Options - Provisioning" +sidebar_current: "provisioning-ansible-common" +--- + +# Shared Ansible Options + +The following options are available to both Ansible provisioners: + + - [`ansible`](/v2/provisioning/ansible.html) + - [`ansible_local`](/v2/provisioning/ansible_local.html) + +These options get passed to the `ansible-playbook` command that ships with Ansible, either via command line arguments or environment variables, depending on Ansible own capabilities. + +Some of these options are for advanced usage only and should not be used unless you understand their purpose. + +- `extra_vars` (string or hash) - Pass additional variables (with highest priority) to the playbook. + + This parameter can be a path to a JSON or YAML file, or a hash. + + Example: + + ```ruby + ansible.extra_vars = { + ntp_server: "pool.ntp.org", + nginx: { + port: 8008, + workers: 4 + } + } + ``` + These variables take the highest precedence over any other variables. + +- `groups` (hash) - Set of inventory groups to be included in the [auto-generated inventory file](/v2/provisioning/ansible_intro.html). + + Example: + + ```ruby + ansible.groups = { + "web" => ["vm1", "vm2"], + "db" => ["vm3"] + } + ``` + + Notes: + + - Alphanumeric patterns are not supported (e.g. `db-[a:f]`, `vm[01:10]`). + - This option has no effect when the `inventory_path` option is defined. + +- `inventory_path` (string) - The path to an Ansible inventory resource (e.g. a [static inventory file](http://docs.ansible.com/intro_inventory.html), a [dynamic inventory script](http://docs.ansible.com/intro_dynamic_inventory.html) or even [multiple inventories stored in the same directory](http://docs.ansible.com/intro_dynamic_inventory.html#using-multiple-inventory-sources)). + + By default, this option is disabled and Vagrant generates an inventory based on the `Vagrantfile` information. + +- `limit` (string or array of strings) - Set of machines or groups from the inventory file to further control which hosts [are affected](http://docs.ansible.com/glossary.html#limit-groups). + + The default value is set to the machine name (taken from `Vagrantfile`) to ensure that `vagrant provision` command only affect the expected machine. + + Setting `limit = "all"` can be used to make Ansible connect to all machines from the inventory file. + +- `raw_arguments` (array of strings) - a list of additional `ansible-playbook` arguments. + + It is an *unsafe wildcard* that can be used to apply Ansible options that are not (yet) supported by this Vagrant provisioner. As of Vagrant 1.7, `raw_arguments` has the highest priority and its values can potentially override or break other Vagrant settings. + + Example: `['--check', '-M /my/modules']`). + +- `skip_tags` (string or array of strings) - Only plays, roles and tasks that [*do not match* these values will be executed](http://docs.ansible.com/playbooks_tags.html). + +- `start_at_task` (string) - The task name where the [playbook execution will start](http://docs.ansible.com/playbooks_startnstep.html#start-at-task). + +- `sudo` (boolean) - Cause Ansible to perform all the playbook tasks [using sudo](http://docs.ansible.com/glossary.html#sudo). + + The default value is `false`. + +- `sudo_user` (string) - set the default username who should be used by the sudo command. + +- `tags` (string or array of strings) - Only plays, roles and tasks [tagged with these values will be executed](http://docs.ansible.com/playbooks_tags.html) . + +- `verbose` (boolean or string) - Set Ansible's verbosity to obtain detailed logging + + Default value is `false` (minimal verbosity). + + 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. + +- `vault_password_file` (string) - The path of a file containing the password used by [Ansible Vault](http://docs.ansible.com/playbooks_vault.html#vault). diff --git a/website/docs/source/v2/provisioning/ansible_intro.html.md b/website/docs/source/v2/provisioning/ansible_intro.html.md new file mode 100644 index 000000000..0b4d4d38f --- /dev/null +++ b/website/docs/source/v2/provisioning/ansible_intro.html.md @@ -0,0 +1,208 @@ +--- +page_title: "Ansible - Short Introduction" +sidebar_current: "provisioning-ansible-intro" +--- + +# Ansible and Vagrant + +The information below is applicable to both Ansible provisioners: + + - [`ansible`](/v2/provisioning/ansible.html), where Ansible is executed on the **Vagrant host** + + - [`ansible_local`](/v2/provisioning/ansible_local.html), where Ansible is executed on the **Vagrant guest** + +The list of common options for these two provisioners is documented in a [separate documentation page](/v2/provisioning/ansible_common.html). + +This documentation page will not go into how to use Ansible or how to write Ansible playbooks, since Ansible is a complete deployment and configuration management system that is beyond the scope of Vagrant documentation. + +To learn more about Ansible, please consult the [Ansible Documentation Site](http://docs.ansible.com/). + +## The Playbook File + +The first component of a successful Ansible provisioner setup is the Ansible playbook which contains the steps that should be run on the guest. Ansible's +[playbook documentation](http://docs.ansible.com/playbooks.html) goes into great detail on how to author playbooks, and there are a number of [best practices](http://docs.ansible.com/playbooks_best_practices.html) that can be applied to use Ansible's powerful features effectively. + +A playbook that installs and starts (or restarts) the NTP daemon via YUM looks like: + +``` +--- +- hosts: all + tasks: + - name: ensure ntpd is at the latest version + yum: pkg=ntp state=latest + notify: + - restart ntpd + handlers: + - name: restart ntpd + service: name=ntpd state=restarted +``` + +You can of course target other operating systems that don't have YUM by changing the playbook tasks. Ansible ships with a number of [modules](http://docs.ansible.com/modules.html) that make running otherwise tedious tasks dead simple. + +### Running Ansible + +The `playbook` option is strictly required by both Ansible provisioners ([`ansible`](/v2/provisioning/ansible.html) and [`ansible_local`](/v2/provisioning/ansible_local.html)), as illustrated in this basic Vagrantfile` configuration: + +```ruby +Vagrant.configure(2) do |config| + + # Use :ansible or :ansible_local to + # select the provisioner of your choice + config.vm.provision :ansible do |ansible| + ansible.playbook = "playbook.yml" + end +end +``` + +Since an Ansible playbook can include many files, you may also collect the related files in a [directory structure](http://docs.ansible.com/playbooks_best_practices.html#directory-layout) like this: + +``` +. +|-- Vagrantfile +|-- provisioning +| |-- group_vars +| |-- all +| |-- roles +| |-- bar +| |-- foo +| |-- playbook.yml +``` + +In such an arrangement, the `ansible.playbook` path should be adjusted accordingly: + +```ruby +Vagrant.configure(2) do |config| + config.vm.provision "ansible" do |ansible| + ansible.playbook = "provisioning/playbook.yml" + end +end +``` + +## The Inventory File + +When using Ansible, it needs to know on which machines a given playbook should run. It does this by way of an [inventory](http://docs.ansible.com/intro_inventory.html) file which lists those machines. In the context of Vagrant, there are two ways to approach working with inventory files. + +### Auto-Generated Inventory + +The first and simplest option is to not provide one to Vagrant at all. Vagrant will generate an inventory file encompassing all of the virtual machines it manages, and use it for provisioning machines. + +**Example with the [`ansible`](/v2/provisioning/ansible.html) provisioner:** + +``` +# Generated by Vagrant + +default ansible_ssh_host=127.0.0.1 ansible_ssh_port=2200 ansible_ssh_private_key_file='/home/.../.vagrant/machines/default/virtualbox/private_key' +``` + +Note that the generated inventory file is stored as part of your local Vagrant environment in +`.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory`. + +**Example with the [`ansible_local`](/v2/provisioning/ansible_local.html) provisioner:** + +``` +# Generated by Vagrant + +default ansible_connection=local +``` + +Note that the generated inventory file is uploaded to the guest VM in a subdirectory of [`tmp_path`](/v2/provisioning/ansible_local.html), e.g. `/tmp/vagrant-ansible/inventory/vagrant_ansible_local_inventory`. + +**How to generate Inventory Groups:** + +The [`groups`](/v2/provisioning/ansible_common.html) option can be used to pass a hash of group names and group members to be included in the generated inventory file. + +With this configuration example: + +``` +Vagrant.configure(2) do |config| + + config.vm.box = "ubuntu/trusty64" + + config.vm.define "machine1" + config.vm.define "machine2" + + config.vm.provision "ansible" do |ansible| + ansible.playbook = "playbook.yml" + ansible.groups = { + "group1" => ["machine1"], + "group2" => ["machine2"], + "all_groups:children" => ["group1", "group2"] + } + end +end +``` + +Vagrant would generate an inventory file that might look like: + +``` +# Generated by Vagrant + +machine1 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2200 ansible_ssh_private_key_file='/home/.../.vagrant/machines/machine1/virtualbox/private_key' +machine2 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2222 ansible_ssh_private_key_file='/home/.../.vagrant/machines/machine2/virtualbox/private_key' + +[group1] +machine1 + +[group2] +machine2 + +[all_groups:children] +group1 +group2 +``` + +**Notes:** + + - Prior to Vagrant 1.7.3, the `ansible_ssh_private_key_file` variable was not set in generated inventory, but passed as command line argument to `ansible-playbook` command. + - The generation of group variables blocks (e.g. `[group1:vars]`) are intentionally not supported, as it is [not recommended to store group variables in the main inventory file](http://docs.ansible.com/intro_inventory.html#splitting-out-host-and-group-specific-data). A good practice is to store these group (or host) variables in `YAML` files stored in `group_vars/` or `host_vars/` directories in the playbook (or inventory) directory. + - Unmanaged machines and undefined groups are not added to the inventory, to avoid useless Ansible errors (e.g. *unreachable host* or *undefined child group*) + +For example, `machine3`, `group3` and `group1:vars` in the example below would not be added to the generated inventory file: + +``` +ansible.groups = { + "group1" => ["machine1"], + "group2" => ["machine2", "machine3"], + "all_groups:children" => ["group1", "group2", "group3"], + "group1:vars" => { "variable1" => 9, "variable2" => "example" } +} +``` + +### Static Inventory + +The second option is for situations where you'd like to have more control over the inventory management. + +With the `inventory_path` option, you can reference a specific inventory resource (e.g. a static inventory file, a [dynamic inventory script](http://docs.ansible.com/intro_dynamic_inventory.html) or even [multiple inventories stored in the same directory](http://docs.ansible.com/intro_dynamic_inventory.html#using-multiple-inventory-sources)). Vagrant will then use this inventory information instead of generating it. + +A very simple inventory file for use with Vagrant might look like: + +``` +default ansible_ssh_host=192.168.111.222 +``` + +Where the above IP address is one set in your Vagrantfile: + +``` +config.vm.network :private_network, ip: "192.168.111.222" +``` + +**Notes:** + + - The machine names in `Vagrantfile` and `ansible.inventory_path` files should correspond, unless you use `ansible.limit` option to reference the correct machines. + - The SSH host addresses (and ports) must obviously be specified twice, in `Vagrantfile` and `ansible.inventory_path` files. + - Sharing hostnames across Vagrant host and guests might be a good idea (e.g. with some Ansible configuration task, or with a plugin like [`vagrant-hostmanager`](https://github.com/smdahlen/vagrant-hostmanager)). + +### The Ansible Configuration File + +Certain settings in Ansible are (only) adjustable via a [configuration file](http://docs.ansible.com/intro_configuration.html), and you might want to ship such a file in your Vagrant project. + +When shipping an Ansible configuration file it is good to know that: + + - it is possible to reference an Ansible configuration file via `ANSIBLE_CONFIG` environment variable, if you want to be flexible about the location of this file. + - `ansible-playbook` **never** looks for `ansible.cfg` in the directory that contains the main playbook file. + - As of Ansible 1.5, the lookup order is the following: + + - `ANSIBLE_CONFIG` an environment variable + - `ansible.cfg` in the runtime current directory + - `.ansible.cfg` in the user home directory + - `/etc/ansible/ansible.cfg` diff --git a/website/docs/source/v2/provisioning/ansible_local.html.md b/website/docs/source/v2/provisioning/ansible_local.html.md new file mode 100644 index 000000000..1c68ddbef --- /dev/null +++ b/website/docs/source/v2/provisioning/ansible_local.html.md @@ -0,0 +1,147 @@ +--- +page_title: "Ansible Local - Provisioning" +sidebar_current: "provisioning-ansible-local" +--- + +# Ansible Local Provisioner + +**Provisioner name: `ansible_local`** + +The 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're not familiar with Ansible and Vagrant already, + I recommend starting with the shell + provisioner. However, if you're comfortable with Vagrant already, Vagrant + is a great way to learn Ansible. +

    +
    + +## Setup Requirements + +The main advantage of the Ansible Local provisioner in comparison to the [Ansible (remote) provisioner](/v2/provisioning/ansible.html) is that it does not require any additional software on your Vagrant host. + +On the other hand, [Ansible must obviously be installed](http://docs.ansible.com/intro_installation.html#installing-the-control-machine) on your guest machine(s). + +**Note:** By default, Vagrant will *try* to automatically install Ansible if it is not yet present on the guest machine (see the `install` option below for more details). + +## Usage + +This page only documents the specific parts of the `ansible_local` provisioner. General Ansible concepts like Playbook or Inventory are shortly explained in the [introduction to Ansible and Vagrant](/v2/provisioning/ansible_intro.html). + +The Ansible Local provisioner requires that all the Ansible Playbook files are available on the guest machine, at the location referred by the `provisioning_path` option. Usually these files are initially present on the host machine (as part of your Vagrant projet), and it is quite easy to share them with a Vagrant [Synced Folder](/v2/synced-folders/index.html). + +### Simplest Configuration + +To run Ansible from your Vagrant guest, the basic `Vagrantfile` configuration looks like: + +```ruby +Vagrant.configure(2) do |config| + + # + # Run Ansible from the Vagrant VM + # + config.vm.provision "ansible_local" do |ansible| + ansible.playbook = "playbook.yml" + end + +end +``` + +**Requirements:** + + - The `playbook.yml` file is stored in your Vagrant's project home directory. + + - The [default shared directory](/v2/synced-folders/basic_usage.html) is enabled (`.` → `/vagrant`). + +## Options + +This section lists the specific options for the Ansible Local provisioner. In addition to the options listed below, this provisioner supports the [common options for both Ansible provisioners](/v2/provisioning/ansible_common.html). + +- `install` (boolean) - Try to automatically install Ansible on the guest system. + + This option is enabled by default. + + Vagrant will to 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. + + **Attention:** There is no guarantee that this automated installation will replace a custom Ansible setup, that might be already present on the Vagrant box. + +- `provisioning_path` (string) - An absolute path on the guest machine where the Ansible files are stored. The `ansible-playbook` command is executed from this directory. + + The default value is `/vagrant`. + +- `tmp_path` (string) - An absolute path on the guest machine where temporary files are stored by the Ansible Local provisioner. + + 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. + + **Attention:** It is currently not possible to use this option to specify which version of Ansible must be automatically installed. With the `install` option enabled, the latest version packaged for the target operating system will always be installed. + +## Tips and Tricks + +### Ansible Parallel Execution from a Guest + +With the following configuration pattern, you can install and execute Ansible only on a single guest machine (the `"controller"`) to provision all your machines. + +```ruby +Vagrant.configure(2) do |config| + + config.vm.box = "ubuntu/trusty64" + + config.vm.define "node1" do |machine| + machine.vm.network "private_network", ip: "172.17.177.21" + end + + config.vm.define "node2" do |machine| + machine.vm.network "private_network", ip: "172.17.177.22" + end + + config.vm.define 'controller' do |machine| + machine.vm.network "private_network", ip: "172.17.177.11" + + machine.vm.provision :ansible_local do |ansible| + ansible.playbook = "example.yml" + ansible.verbose = true + ansible.install = true + ansible.limit = "all" # or only "nodes" group, etc. + ansible.inventory_path = "inventory" + end + end + +end +``` + +You need to create a static `inventory` file that corresponds to your `Vagrantfile` machine definitions: + +``` +controller ansible_connection=local +node1 ansible_ssh_host=172.17.177.21 ansible_ssh_private_key_file=/vagrant/.vagrant/machines/node1/virtualbox/private_key +node2 ansible_ssh_host=172.17.177.22 ansible_ssh_private_key_file=/vagrant/.vagrant/machines/node2/virtualbox/private_key + +[nodes] +node[1:2] +``` + +And finally, you also have to create an [`ansible.cfg` file](http://docs.ansible.com/intro_configuration.html#openssh-specific-settings) to fully disable SSH host key checking. More SSH configurations can be added to the `ssh_args` parameter (e.g. agent forwarding, etc.) + +``` +[defaults] +host_key_checking = no + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes +``` From 3e1e66a76b5978608502143d4f1a19ab5de274c9 Mon Sep 17 00:00:00 2001 From: Gilles Cornu Date: Sun, 8 Nov 2015 14:11:09 +0100 Subject: [PATCH 160/484] Update CHANGELOG.md Close #2103 Close #5292 --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 602956e8a..e4a74d90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ FEATURES: and restore point-in-time snapshots. - **IPv6 Private Networks**: Private networking now supports IPv6. This only works with VirtualBox and VMware at this point. [GH-6342] + - New provisioner: `ansible_local` to execute Ansible from the guest + machine. [GH-2103] BREAKING CHANGES: @@ -15,21 +17,23 @@ BREAKING CHANGES: (i.e. `ansible_ssh_user` setting) to always correspond to the vagrant ssh username. This change is enabled by default, but we expect this to affect only a tiny number of people as it corresponds to the common usage. - If you however use different remote usernames in your Ansible plays, tasks, + If you however use multiple remote usernames in your Ansible plays, tasks, or custom inventories, you can simply set the option `force_remote_user` to false to make Vagrant behave the same as before. - IMPROVEMENTS: - provisioners/ansible: add new `force_remote_user` option to control whether `ansible_ssh_user` parameter should be applied or not [GH-6348] + - provisioners/ansible: show a warning when running from a Windows Host [GH-5292] BUG FIXES: - communicator/winrm: respect `boot_timeout` setting [GH-6229] - provisioners/ansible: use quotes for the `ansible_ssh_private_key_file` value in the generated inventory [GH-6209] + - provisioners/ansible: don't show the `ansible-playbook` command when verbose + option is an empty string ## 1.7.4 (July 17, 2015) From a21d5be705cf5e061f332bebdcd38f33fb3bdd97 Mon Sep 17 00:00:00 2001 From: Duncan Mac-Vicar P Date: Sun, 8 Nov 2015 16:09:04 +0100 Subject: [PATCH 161/484] SUSE-flavored systems uses STARTMODE and not ONBOOT As described in /etc/sysconfig/network/ifcfg.template Static template was already using the right one, but the dhcp configuration seems to be copied from a Fedora/Redhat template. This fixes the issue that the interface does not come up after reboot. --- templates/guests/suse/network_dhcp.erb | 6 +++--- test/unit/templates/guests/suse/network_dhcp_test.rb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/guests/suse/network_dhcp.erb b/templates/guests/suse/network_dhcp.erb index 8bbaa62e4..3504b265e 100644 --- a/templates/guests/suse/network_dhcp.erb +++ b/templates/guests/suse/network_dhcp.erb @@ -1,6 +1,6 @@ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. -BOOTPROTO=dhcp -ONBOOT=yes -DEVICE=eth<%= options[:interface] %> +BOOTPROTO='dhcp' +STARTMODE='auto' +DEVICE='eth<%= options[:interface] %>' #VAGRANT-END diff --git a/test/unit/templates/guests/suse/network_dhcp_test.rb b/test/unit/templates/guests/suse/network_dhcp_test.rb index 82595db47..1ecb4dc1f 100644 --- a/test/unit/templates/guests/suse/network_dhcp_test.rb +++ b/test/unit/templates/guests/suse/network_dhcp_test.rb @@ -12,9 +12,9 @@ describe "templates/guests/suse/network_dhcp" do expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. - BOOTPROTO=dhcp - ONBOOT=yes - DEVICE=ethen0 + BOOTPROTO='dhcp' + STARTMODE='auto' + DEVICE='ethen0' #VAGRANT-END EOH end From 3f4a372d576fd18d5dc2d9836f19f695fdb3b6c9 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Sun, 13 Apr 2014 12:24:22 +0200 Subject: [PATCH 162/484] Remove all available versions of a box This patch introduces a new parameter --all for the remove command of the box plugin. Setting this parameter will remove all available versions of a specific box. Example usage: ``` $ vagrant box list ubuntu/trusty64 (virtualbox, 20150427.0.0) ubuntu/trusty64 (virtualbox, 20150430.0.0) ubuntu/trusty64 (virtualbox, 20150506.0.0) ``` ``` $ vagrant box remove ubuntu/trusty64 You requested to remove the box 'ubuntu/trusty64' with provider 'virtualbox'. This box has multiple versions. You must explicitly specify which version you want to remove with the `--box-version` flag. The available versions for this box are: * 20150427.0.0 * 20150430.0.0 * 20150506.0.0 ``` With the --all parameter it is possible to remove all versions at once. ``` $ vagrant box remove --all ubuntu/trusty64 Removing box 'ubuntu/trusty64' (v20150506.0.0) with provider 'virtualbox'... Removing box 'ubuntu/trusty64' (v20150430.0.0) with provider 'virtualbox'... Removing box 'ubuntu/trusty64' (v20150427.0.0) with provider 'virtualbox'... ``` --- lib/vagrant/action/builtin/box_remove.rb | 78 +++++++++++++----------- plugins/commands/box/command/remove.rb | 5 ++ website/docs/source/v2/cli/box.html.md | 5 +- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/lib/vagrant/action/builtin/box_remove.rb b/lib/vagrant/action/builtin/box_remove.rb index 1d0cea7fd..5d18ed704 100644 --- a/lib/vagrant/action/builtin/box_remove.rb +++ b/lib/vagrant/action/builtin/box_remove.rb @@ -15,6 +15,7 @@ module Vagrant box_provider = env[:box_provider] box_provider = box_provider.to_sym if box_provider box_version = env[:box_version] + box_remove_all_versions = env[:box_remove_all_versions] boxes = {} env[:box_collection].all.each do |n, v, p| @@ -53,7 +54,7 @@ module Vagrant if all_versions.length == 1 # There is only one version, just use that. box_version = all_versions.first - else + elsif not box_remove_all_versions # There are multiple versions, we can't choose. raise Errors::BoxRemoveMultiVersion, name: box_name, @@ -68,48 +69,53 @@ module Vagrant versions: all_versions.sort.map { |k| " * #{k}" }.join("\n") end - box = env[:box_collection].find( - box_name, box_provider, box_version) + versions_to_remove = [box_version] + versions_to_remove = all_versions if box_remove_all_versions - # Verify that this box is not in use by an active machine, - # otherwise warn the user. - users = box.in_use?(env[:machine_index]) || [] - users = users.find_all { |u| u.valid?(env[:home_path]) } - if !users.empty? - # Build up the output to show the user. - users = users.map do |entry| - "#{entry.name} (ID: #{entry.id})" - end.join("\n") + versions_to_remove.sort.each do |version_to_remove| + box = env[:box_collection].find( + box_name, box_provider, box_version) - force_key = :force_confirm_box_remove - message = I18n.t( - "vagrant.commands.box.remove_in_use_query", - name: box.name, - provider: box.provider, - version: box.version, - users: users) + " " + # Verify that this box is not in use by an active machine, + # otherwise warn the user. + users = box.in_use?(env[:machine_index]) || [] + users = users.find_all { |u| u.valid?(env[:home_path]) } + if !users.empty? + # Build up the output to show the user. + users = users.map do |entry| + "#{entry.name} (ID: #{entry.id})" + end.join("\n") - # Ask the user if we should do this - stack = Builder.new.tap do |b| - b.use Confirm, message, force_key + force_key = :force_confirm_box_remove + message = I18n.t( + "vagrant.commands.box.remove_in_use_query", + name: box.name, + provider: box.provider, + version: box.version, + users: users) + " " + + # Ask the user if we should do this + stack = Builder.new.tap do |b| + b.use Confirm, message, force_key + end + + result = env[:action_runner].run(stack, env) + if !result[:result] + # They said "no", so continue with the next box + next + end end - result = env[:action_runner].run(stack, env) - if !result[:result] - # They said "no", so just return - return @app.call(env) - end + env[:ui].info(I18n.t("vagrant.commands.box.removing", + name: box.name, + provider: box.provider, + version: box.version)) + box.destroy! + + # Passes on the removed box to the rest of the middleware chain + env[:box_removed] = box end - env[:ui].info(I18n.t("vagrant.commands.box.removing", - name: box.name, - provider: box.provider, - version: box.version)) - box.destroy! - - # Passes on the removed box to the rest of the middleware chain - env[:box_removed] = box - @app.call(env) end end diff --git a/plugins/commands/box/command/remove.rb b/plugins/commands/box/command/remove.rb index 013ca2921..4bedd96c0 100644 --- a/plugins/commands/box/command/remove.rb +++ b/plugins/commands/box/command/remove.rb @@ -27,6 +27,10 @@ module VagrantPlugins "The specific version of the box to remove") do |v| options[:version] = v end + + o.on("--all", "Remove all available versions of the box") do |a| + options[:all] = a + end end # Parse the options @@ -50,6 +54,7 @@ module VagrantPlugins box_provider: options[:provider], box_version: options[:version], force_confirm_box_remove: options[:force], + box_remove_all_versions: options[:all], }) # Success, exit status 0 diff --git a/website/docs/source/v2/cli/box.html.md b/website/docs/source/v2/cli/box.html.md index e919fc9ad..24cacbe30 100644 --- a/website/docs/source/v2/cli/box.html.md +++ b/website/docs/source/v2/cli/box.html.md @@ -128,13 +128,16 @@ This command removes a box from Vagrant that matches the given name. If a box has multiple providers, the exact provider must be specified with the `--provider` flag. If a box has multiple versions, you can select -what versions to delete with the `--box-version` flag. +what versions to delete with the `--box-version` flag or remove all versions +with the `--all` flag. ## Options * `--box-version VALUE` - Version of version constraints of the boxes to remove. See documentation on this flag for `box add` for more details. +* `--all` - Remove all available versions of a box. + * `--force` - Forces removing the box even if an active Vagrant environment is using it. From a115928d983c2919efb8ea53984dc85bc8954d26 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 9 Nov 2015 09:36:27 +0000 Subject: [PATCH 163/484] Update output message to include --all --- templates/locales/en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 3b7094310..16c8b4e5d 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -556,8 +556,8 @@ en: You requested to remove the box '%{name}' with provider '%{provider}'. This box has multiple versions. You must explicitly specify which version you want to remove with - the `--box-version` flag. The available versions for this - box are: + the `--box-version` flag or specify the `--all` flag to remove all + versions. The available versions for this box are: %{versions} box_remove_not_found: |- From cfa0658cef4e1d42f46c9b3eb477392b61d8d051 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 9 Nov 2015 09:40:49 +0000 Subject: [PATCH 164/484] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f78eac8bc..c6d56fb98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ FEATURES: IMPROVEMENTS: + - core: allow removal of all box versions with `--all` flag [GH-3462] + BUG FIXES: - communicator/winrm: respect `boot_timeout` setting [GH-6229] From 75cc6ef8d3da4d8829dcdc58cf85511039b40f5e Mon Sep 17 00:00:00 2001 From: Gilles Cornu Date: Thu, 12 Nov 2015 09:09:58 +0100 Subject: [PATCH 165/484] provisioners/ansible_local: fix a str-to-sym bug Without this change `ansible.version = "latest"` is not considered as equivalent to `ansible.version = :latest`. --- plugins/provisioners/ansible/provisioner/guest.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/provisioners/ansible/provisioner/guest.rb b/plugins/provisioners/ansible/provisioner/guest.rb index 3ac35bea1..285df8ac1 100644 --- a/plugins/provisioners/ansible/provisioner/guest.rb +++ b/plugins/provisioners/ansible/provisioner/guest.rb @@ -45,7 +45,7 @@ module VagrantPlugins # Try to install Ansible (if needed and requested) if config.install && - (config.version == :latest || + (config.version.to_s.to_sym == :latest || !@machine.guest.capability(:ansible_installed, config.version)) @machine.ui.detail(I18n.t("vagrant.provisioners.ansible.installing")) @machine.guest.capability(:ansible_install) @@ -59,7 +59,7 @@ module VagrantPlugins # Check if requested ansible version is available if (!config.version.empty? && - config.version != :latest && + config.version.to_s.to_sym != :latest && !@machine.guest.capability(:ansible_installed, config.version)) raise Ansible::Errors::AnsibleVersionNotFoundOnGuest, required_version: config.version.to_s end From 80021ceafb10253c15d581ba2b2a1a98b2fb0da0 Mon Sep 17 00:00:00 2001 From: Olivier Meurice Date: Thu, 12 Nov 2015 14:33:04 +0100 Subject: [PATCH 166/484] Add network configuration plugin for Slackware Linux --- .../guests/slackware/cap/change_host_name.rb | 19 ++++++++++ .../slackware/cap/configure_networks.rb | 36 +++++++++++++++++++ plugins/guests/slackware/guest.rb | 11 ++++++ plugins/guests/slackware/plugin.rb | 25 +++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 plugins/guests/slackware/cap/change_host_name.rb create mode 100644 plugins/guests/slackware/cap/configure_networks.rb create mode 100644 plugins/guests/slackware/guest.rb create mode 100644 plugins/guests/slackware/plugin.rb diff --git a/plugins/guests/slackware/cap/change_host_name.rb b/plugins/guests/slackware/cap/change_host_name.rb new file mode 100644 index 000000000..07976725f --- /dev/null +++ b/plugins/guests/slackware/cap/change_host_name.rb @@ -0,0 +1,19 @@ +module VagrantPlugins + module GuestSlackware + module Cap + class ChangeHostName + def self.change_host_name(machine, name) + machine.communicate.tap do |comm| + # Only do this if the hostname is not already set + if !comm.test("sudo hostname | grep '#{name}'") + comm.sudo("chmod o+w /etc/hostname") + comm.sudo("echo #{name} > /etc/hostname") + comm.sudo("chmod o-w /etc/hostname") + comm.sudo("hostname -F /etc/hostname") + end + end + end + end + end + end +end diff --git a/plugins/guests/slackware/cap/configure_networks.rb b/plugins/guests/slackware/cap/configure_networks.rb new file mode 100644 index 000000000..dc84080e0 --- /dev/null +++ b/plugins/guests/slackware/cap/configure_networks.rb @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +require "tempfile" + +require "vagrant/util/template_renderer" + +module VagrantPlugins + module GuestSlackware + module Cap + class ConfigureNetworks + include Vagrant::Util + + def self.configure_networks(machine, networks) + interfaces = Array.new + machine.communicate.sudo("ip -o -0 addr | grep -v LOOPBACK | awk '{print $2}' | sed 's/://'") do |_, result| + interfaces = result.split("\n") + end + + networks.each do |network| + network[:device] = interfaces[network[:interface]] + + entry = TemplateRenderer.render("guests/slackware/network_#{network[:type]}", options: network) + + temp = Tempfile.new("vagrant") + temp.binmode + temp.write(entry) + temp.close + + machine.communicate.upload(temp.path, "/tmp/vagrant_network") + machine.communicate.sudo("mv /tmp/vagrant_network /etc/rc.d/rc.inet1.conf") + machine.communicate.sudo("/etc/rc.d/rc.inet1") + end + end + end + end + end +end diff --git a/plugins/guests/slackware/guest.rb b/plugins/guests/slackware/guest.rb new file mode 100644 index 000000000..1442f26fc --- /dev/null +++ b/plugins/guests/slackware/guest.rb @@ -0,0 +1,11 @@ +require "vagrant" + +module VagrantPlugins + module GuestSlackware + class Guest < Vagrant.plugin("2", :guest) + def detect?(machine) + machine.communicate.test("cat /etc/slackware-version") + end + end + end +end diff --git a/plugins/guests/slackware/plugin.rb b/plugins/guests/slackware/plugin.rb new file mode 100644 index 000000000..c81db6aa4 --- /dev/null +++ b/plugins/guests/slackware/plugin.rb @@ -0,0 +1,25 @@ +require "vagrant" + +module VagrantPlugins + module GuestSlackware + class Plugin < Vagrant.plugin("2") + name "Slackware guest" + description "Slackware guest support." + + guest("slackware", "linux") do + require File.expand_path("../guest", __FILE__) + Guest + end + + guest_capability("slackware", "change_host_name") do + require_relative "cap/change_host_name" + Cap::ChangeHostName + end + + guest_capability("slackware", "configure_networks") do + require_relative "cap/configure_networks" + Cap::ConfigureNetworks + end + end + end +end From fd4c5f02d3c5216b3c9997d6f8726b821bee931b Mon Sep 17 00:00:00 2001 From: Olivier Meurice Date: Thu, 12 Nov 2015 14:33:37 +0100 Subject: [PATCH 167/484] Add network configuration plugin template files for Slackware Linux --- templates/guests/slackware/network_dhcp.erb | 23 +++++++++++++++++ templates/guests/slackware/network_static.erb | 25 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 templates/guests/slackware/network_dhcp.erb create mode 100644 templates/guests/slackware/network_static.erb diff --git a/templates/guests/slackware/network_dhcp.erb b/templates/guests/slackware/network_dhcp.erb new file mode 100644 index 000000000..53f48f8c0 --- /dev/null +++ b/templates/guests/slackware/network_dhcp.erb @@ -0,0 +1,23 @@ +IPADDR[0]="" +NETMASK[0]="" +USE_DHCP[0]="yes" +DHCP_HOSTNAME[0]="" + +IPADDR[1]="" +NETMASK[1]="" +USE_DHCP[1]="yes" +DHCP_HOSTNAME[1]="" + +IPADDR[2]="" +NETMASK[2]="" +USE_DHCP[2]="" +DHCP_HOSTNAME[2]="" + +IPADDR[3]="" +NETMASK[3]="" +USE_DHCP[3]="" +DHCP_HOSTNAME[3]="" + +GATEWAY="" + +DEBUG_ETH_UP="no" diff --git a/templates/guests/slackware/network_static.erb b/templates/guests/slackware/network_static.erb new file mode 100644 index 000000000..0745e8b1a --- /dev/null +++ b/templates/guests/slackware/network_static.erb @@ -0,0 +1,25 @@ +IPADDR[0]="" +NETMASK[0]="" +USE_DHCP[0]="yes" +DHCP_HOSTNAME[0]="" + +IPADDR[1]="<%= options[:ip] %>" +NETMASK[1]="" +USE_DHCP[1]="" +DHCP_HOSTNAME[1]="" + +IPADDR[2]="" +NETMASK[2]="" +USE_DHCP[2]="" +DHCP_HOSTNAME[2]="" + +IPADDR[3]="" +NETMASK[3]="" +USE_DHCP[3]="" +DHCP_HOSTNAME[3]="" + +<% if options[:gateway] %> + GATEWAY="<%= options[:gateway] %>" +<% end %> + +DEBUG_ETH_UP="no" From 99f060cd42d79818d1fcfc878cdc4f751e5a12b0 Mon Sep 17 00:00:00 2001 From: captainill Date: Fri, 13 Nov 2015 21:00:42 -0800 Subject: [PATCH 168/484] less shared files --- website/www/source/javascripts/Sidebar.js | 50 +++ website/www/source/javascripts/lib/Base.js | 145 ++++++++ website/www/source/layouts/_mobile_nav.erb | 22 ++ website/www/source/layouts/layout.erb | 10 +- website/www/source/stylesheets/_mixins.less | 19 + .../www/source/stylesheets/_mobile-nav.less | 23 ++ .../hashicorp-shared/_hashicorp-header.less | 326 ++++++++++++++++++ .../_hashicorp-mobile-nav.less | 293 ++++++++++++++++ .../hashicorp-shared/_hashicorp-utility.less | 70 ++++ .../hashicorp-shared/_project-utility.less | 28 ++ website/www/source/stylesheets/vagrantup.less | 6 + website/www/source/svg/_svg-by-hashicorp.erb | 18 + website/www/source/svg/_svg-download.erb | 4 + website/www/source/svg/_svg-github.erb | 9 + .../www/source/svg/_svg-hashicorp-logo.erb | 7 + 15 files changed, 1028 insertions(+), 2 deletions(-) create mode 100644 website/www/source/javascripts/Sidebar.js create mode 100644 website/www/source/javascripts/lib/Base.js create mode 100644 website/www/source/layouts/_mobile_nav.erb create mode 100644 website/www/source/stylesheets/_mobile-nav.less create mode 100755 website/www/source/stylesheets/hashicorp-shared/_hashicorp-header.less create mode 100644 website/www/source/stylesheets/hashicorp-shared/_hashicorp-mobile-nav.less create mode 100755 website/www/source/stylesheets/hashicorp-shared/_hashicorp-utility.less create mode 100755 website/www/source/stylesheets/hashicorp-shared/_project-utility.less create mode 100644 website/www/source/svg/_svg-by-hashicorp.erb create mode 100644 website/www/source/svg/_svg-download.erb create mode 100644 website/www/source/svg/_svg-github.erb create mode 100644 website/www/source/svg/_svg-hashicorp-logo.erb diff --git a/website/www/source/javascripts/Sidebar.js b/website/www/source/javascripts/Sidebar.js new file mode 100644 index 000000000..b36e508c4 --- /dev/null +++ b/website/www/source/javascripts/Sidebar.js @@ -0,0 +1,50 @@ +(function(){ + + Sidebar = Base.extend({ + + $body: null, + $overlay: null, + $sidebar: null, + $sidebarHeader: null, + $sidebarImg: null, + $toggleButton: null, + + constructor: function(){ + this.$body = $('body'); + this.$overlay = $('.sidebar-overlay'); + this.$sidebar = $('#sidebar'); + this.$sidebarHeader = $('#sidebar .sidebar-header'); + this.$toggleButton = $('.navbar-toggle'); + this.sidebarImg = this.$sidebarHeader.css('background-image'); + + this.addEventListeners(); + }, + + addEventListeners: function(){ + var _this = this; + + _this.$toggleButton.on('click', function() { + _this.$sidebar.toggleClass('open'); + if ((_this.$sidebar.hasClass('sidebar-fixed-left') || _this.$sidebar.hasClass('sidebar-fixed-right')) && _this.$sidebar.hasClass('open')) { + _this.$overlay.addClass('active'); + _this.$body.css('overflow', 'hidden'); + } else { + _this.$overlay.removeClass('active'); + _this.$body.css('overflow', 'auto'); + } + + return false; + }); + + _this.$overlay.on('click', function() { + $(this).removeClass('active'); + _this.$body.css('overflow', 'auto'); + _this.$sidebar.removeClass('open'); + }); + } + + }); + + window.Sidebar = Sidebar; + +})(); diff --git a/website/www/source/javascripts/lib/Base.js b/website/www/source/javascripts/lib/Base.js new file mode 100644 index 000000000..504e2beea --- /dev/null +++ b/website/www/source/javascripts/lib/Base.js @@ -0,0 +1,145 @@ +/* + Based on Base.js 1.1a (c) 2006-2010, Dean Edwards + Updated to pass JSHint and converted into a module by Kenneth Powers + License: http://www.opensource.org/licenses/mit-license.php +*/ +/*global define:true module:true*/ +/*jshint eqeqeq:true*/ +(function (name, global, definition) { + if (typeof module !== 'undefined') { + module.exports = definition(); + } else if (typeof define !== 'undefined' && typeof define.amd === 'object') { + define(definition); + } else { + global[name] = definition(); + } +})('Base', this, function () { + // Base Object + var Base = function () {}; + + // Implementation + Base.extend = function (_instance, _static) { // subclass + var extend = Base.prototype.extend; + // build the prototype + Base._prototyping = true; + var proto = new this(); + extend.call(proto, _instance); + proto.base = function () { + // call this method from any other method to invoke that method's ancestor + }; + delete Base._prototyping; + // create the wrapper for the constructor function + //var constructor = proto.constructor.valueOf(); //-dean + var constructor = proto.constructor; + var klass = proto.constructor = function () { + if (!Base._prototyping) { + if (this._constructing || this.constructor === klass) { // instantiation + this._constructing = true; + constructor.apply(this, arguments); + delete this._constructing; + } else if (arguments[0] !== null) { // casting + return (arguments[0].extend || extend).call(arguments[0], proto); + } + } + }; + // build the class interface + klass.ancestor = this; + klass.extend = this.extend; + klass.forEach = this.forEach; + klass.implement = this.implement; + klass.prototype = proto; + klass.toString = this.toString; + klass.valueOf = function (type) { + return (type === 'object') ? klass : constructor.valueOf(); + }; + extend.call(klass, _static); + // class initialization + if (typeof klass.init === 'function') klass.init(); + return klass; + }; + + Base.prototype = { + extend: function (source, value) { + if (arguments.length > 1) { // extending with a name/value pair + var ancestor = this[source]; + if (ancestor && (typeof value === 'function') && // overriding a method? + // the valueOf() comparison is to avoid circular references + (!ancestor.valueOf || ancestor.valueOf() !== value.valueOf()) && /\bbase\b/.test(value)) { + // get the underlying method + var method = value.valueOf(); + // override + value = function () { + var previous = this.base || Base.prototype.base; + this.base = ancestor; + var returnValue = method.apply(this, arguments); + this.base = previous; + return returnValue; + }; + // point to the underlying method + value.valueOf = function (type) { + return (type === 'object') ? value : method; + }; + value.toString = Base.toString; + } + this[source] = value; + } else if (source) { // extending with an object literal + var extend = Base.prototype.extend; + // if this object has a customized extend method then use it + if (!Base._prototyping && typeof this !== 'function') { + extend = this.extend || extend; + } + var proto = { + toSource: null + }; + // do the "toString" and other methods manually + var hidden = ['constructor', 'toString', 'valueOf']; + // if we are prototyping then include the constructor + for (var i = Base._prototyping ? 0 : 1; i < hidden.length; i++) { + var h = hidden[i]; + if (source[h] !== proto[h]) + extend.call(this, h, source[h]); + } + // copy each of the source object's properties to this object + for (var key in source) { + if (!proto[key]) extend.call(this, key, source[key]); + } + } + return this; + } + }; + + // initialize + Base = Base.extend({ + constructor: function () { + this.extend(arguments[0]); + } + }, { + ancestor: Object, + version: '1.1', + forEach: function (object, block, context) { + for (var key in object) { + if (this.prototype[key] === undefined) { + block.call(context, object[key], key, object); + } + } + }, + implement: function () { + for (var i = 0; i < arguments.length; i++) { + if (typeof arguments[i] === 'function') { + // if it's a function, call it + arguments[i](this.prototype); + } else { + // add the interface using the extend method + this.prototype.extend(arguments[i]); + } + } + return this; + }, + toString: function () { + return String(this.valueOf()); + } + }); + + // Return Base implementation + return Base; +}); diff --git a/website/www/source/layouts/_mobile_nav.erb b/website/www/source/layouts/_mobile_nav.erb new file mode 100644 index 000000000..142b6880f --- /dev/null +++ b/website/www/source/layouts/_mobile_nav.erb @@ -0,0 +1,22 @@ + + + + + diff --git a/website/www/source/layouts/layout.erb b/website/www/source/layouts/layout.erb index 4e6736743..78d525fe5 100644 --- a/website/www/source/layouts/layout.erb +++ b/website/www/source/layouts/layout.erb @@ -47,6 +47,8 @@
+ <%= partial "layouts/mobile_nav" %> + <%= yield %> @@ -88,10 +90,15 @@ + <%= javascript_include_tag "lib/Base" %> + <%= javascript_include_tag "Sidebar" %> + @@ -129,4 +136,3 @@ - diff --git a/website/www/source/stylesheets/_mixins.less b/website/www/source/stylesheets/_mixins.less index e47f055da..fadf9bb3d 100644 --- a/website/www/source/stylesheets/_mixins.less +++ b/website/www/source/stylesheets/_mixins.less @@ -63,6 +63,25 @@ padding: @baseline 0; } +.transition(@transition) { + -webkit-transition: @transition; + -o-transition: @transition; + transition: @transition; +} + +.translate3d (@x, @y: 0, @z: 0) { + -webkit-transform: translate3d(@x, @y, @z); + -moz-transform: translate3d(@x, @y, @z); + -ms-transform: translate3d(@x, @y, @z); + -o-transform: translate3d(@x, @y, @z); +} + +.clearfix{ + zoom:1; + &:before, &:after{ content:""; display:table; } + &:after{ clear: both; } +} + .inner-bg-large { background-image: #c1b4d5; /* Old browsers */ background-image: url(/images/sidebar_background_inner.png), -moz-linear-gradient(45deg, #c1b4d5 0%, #98d3f8 100%); /* FF3.6+ */ diff --git a/website/www/source/stylesheets/_mobile-nav.less b/website/www/source/stylesheets/_mobile-nav.less new file mode 100644 index 000000000..debcb14f0 --- /dev/null +++ b/website/www/source/stylesheets/_mobile-nav.less @@ -0,0 +1,23 @@ +// +// Sidebar +// - Project Specific +// - Make sidebar edits here +// -------------------------------------------------- + +.sidebar { + .sidebar-nav { + // Links + //---------------- + li { + a { + color: $purple; + + svg{ + path{ + fill: $purple; + } + } + } + } + } +} diff --git a/website/www/source/stylesheets/hashicorp-shared/_hashicorp-header.less b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-header.less new file mode 100755 index 000000000..abb94012c --- /dev/null +++ b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-header.less @@ -0,0 +1,326 @@ +// +// Hashicorp nav +// -------------------------------------------------- + +#header{ + position: relative; + margin-bottom: 0; +} + +.navigation { + color: black; + text-rendering: optimizeLegibility; + transition: all 1s ease; + + &.white{ + .navbar-brand { + .logo { + color: white; + } + } + + .main-links, + .external-links { + li > a { + &:hover{ + opacity: 1; + } + } + } + } + + .navbar-toggle{ + height: @header-height; + margin: 0; + border-radius: 0; + .icon-bar{ + border: 1px solid @black; + border-radius: 0; + } + } + + .external-links { + &.white{ + svg path{ + fill: @white; + } + } + + li { + position: relative; + + svg path{ + .transition( all 300ms ease-in ); + } + + &:hover{ + svg path{ + .transition( all 300ms ease-in ); + } + } + + &.download{ + margin-right: 10px; + } + + > a { + padding-left: 12px !important; + svg{ + position: absolute; + left: -12px; + top: 50%; + margin-top: -7px; + width: 14px; + height: 14px; + } + } + } + } + + .main-links{ + margin-right: @nav-margin-right * 2; + } + + .main-links, + .external-links { + &.white{ + li > a { + color: white; + } + } + li > a { + .hashi-a-style(); + margin: 0 10px; + padding-top: 1px; + line-height: @header-height; + .project-a-style(); + } + } + + .nav > li > a:hover, .nav > li > a:focus { + background-color: transparent; + .transition( all 300ms ease-in ); + } +} + +.navbar-brand { + display: block; + height: @header-height; + padding: 0; + margin: 0 10px 0 0; + + .logo{ + display: inline-block; + height: @header-height; + vertical-align:top; + padding: 0; + line-height: @header-height; + padding-left: @project-logo-width + @project-logo-pad-left; + background-position: 0 center; + .transition(all 300ms ease-in); + + &:hover{ + .transition(all 300ms ease-in); + text-decoration: none; + } + } +} + +.navbar-toggle{ + &.white{ + .icon-bar{ + border: 1px solid white; + } + } +} + +.by-hashicorp{ + display: inline-block; + vertical-align:top; + height: @header-height; + margin-left: 3px; + padding-top: 2px; + color: black; + line-height: @header-height; + font-family: @header-font-family; + font-weight: 600; + font-size: 0; + text-decoration: none; + + &.white{ + color: white; + font-weight: 300; + svg{ + path, + polygon{ + fill: white; + } + line{ + stroke: white; + } + } + } + + &:focus, + &:hover{ + text-decoration: none; + } + + .svg-wrap{ + font-size: 13px; + } + + svg{ + &.svg-by{ + width: @by-hashicorp-width; + height: @by-hashicorp-height; + margin-bottom: -4px; + margin-left: 4px; + } + + &.svg-logo{ + width: 16px; + height: 16px; + margin-bottom: -3px; + margin-left: 4px; + } + + path, + polygon{ + fill: black; + .transition(all 300ms ease-in); + + &:hover{ + .transition(all 300ms ease-in); + } + } + line{ + stroke: black; + .transition(all 300ms ease-in); + + &:hover{ + .transition(all 300ms ease-in); + } + } + } +} + +.hashicorp-project{ + display: inline-block; + height: 30px; + line-height: 30px; + text-decoration: none; + font-size: 14px; + color: @black; + font-weight: 600; + + &.white{ + color: white; + svg{ + path, + polygon{ + fill: white; + } + line{ + stroke: white; + } + } + } + + &:focus{ + text-decoration: none; + } + + &:hover{ + text-decoration: none; + svg{ + &.svg-by{ + line{ + stroke: @purple; + } + } + } + } + + span{ + margin-right: 4px; + font-family: @header-font-family; + font-weight: 500; + } + + span, + svg{ + display: inline-block; + } + + svg{ + &.svg-by{ + width: @by-hashicorp-width; + height: @by-hashicorp-height; + margin-bottom: -4px; + margin-left: -3px; + } + + &.svg-logo{ + width: 30px; + height: 30px; + margin-bottom: -10px; + margin-left: -1px; + } + + path, + line{ + fill: @black; + .transition(all 300ms ease-in); + + &:hover{ + .transition(all 300ms ease-in); + } + } + } +} + +@media (max-width: 992px) { + .navigation { + > .container{ + width: 100%; + } + } +} + +@media (max-width: 768px) { + .navigation { + .main-links{ + margin-right: 0; + } + } +} + +@media (max-width: 414px) { + #header { + .navbar-toggle{ + padding-top: 10px; + height: @header-mobile-height; + } + + .navbar-brand { + height: @header-mobile-height; + + .logo{ + height: @header-mobile-height; + line-height: @header-mobile-height; + } + .by-hashicorp{ + height: @header-mobile-height; + line-height: @header-mobile-height; + padding-top: 0; + } + } + .main-links, + .external-links { + li > a { + line-height: @header-mobile-height; + } + } + } +} diff --git a/website/www/source/stylesheets/hashicorp-shared/_hashicorp-mobile-nav.less b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-mobile-nav.less new file mode 100644 index 000000000..39be1c2ee --- /dev/null +++ b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-mobile-nav.less @@ -0,0 +1,293 @@ +// +// Hashicorp Sidebar +// - Shared throughout projects +// - Edits should not be made here +// -------------------------------------------------- + +// Base variables +// -------------------------------------------------- +@screen-tablet: 768px; + +@gray-darker: #212121; // #212121 - text +@gray-secondary: #757575; // #757575 - secondary text, icons +@gray: #bdbdbd; // #bdbdbd - hint text +@gray-light: #e0e0e0; // #e0e0e0 - divider +@gray-lighter: #f5f5f5; // #f5f5f5 - background +@link-color: @gray-darker; +@link-bg: transparent; +@link-hover-color: @gray-lighter; +@link-hover-bg: @gray-lighter; +@link-active-color: @gray-darker; +@link-active-bg: @gray-light; +@link-disabled-color: @gray-light; +@link-disabled-bg: transparent; + +/* -- Sidebar style ------------------------------- */ + +// Sidebar variables +// -------------------------------------------------- +@zindex-sidebar-fixed: 1035; + +@sidebar-desktop-width: 280px; +@sidebar-width: 240px; + +@sidebar-padding: 16px; +@sidebar-divider: @sidebar-padding/2; + +@sidebar-icon-width: 40px; +@sidebar-icon-height: 20px; + +.sidebar-nav-base { + text-align: center; + + &:last-child{ + border-bottom: none; + } + + li > a { + background-color: @link-bg; + } + li:hover > a { + background-color: @link-hover-bg; + } + li:focus > a, li > a:focus { + background-color: @link-bg; + } + + > .open > a { + &, + &:hover, + &:focus { + background-color: @link-hover-bg; + } + } + + > .active > a { + &, + &:hover, + &:focus { + background-color: @link-active-bg; + } + } + > .disabled > a { + &, + &:hover, + &:focus { + background-color: @link-disabled-bg; + } + } + + // Dropdown menu items + > .dropdown { + // Remove background color from open dropdown + > .dropdown-menu { + background-color: @link-hover-bg; + + > li > a { + &:focus { + background-color: @link-hover-bg; + } + &:hover { + background-color: @link-hover-bg; + } + } + + > .active > a { + &, + &:hover, + &:focus { + color: @link-active-color; + background-color: @link-active-bg; + } + } + } + } +} + +// +// Sidebar +// -------------------------------------------------- + +// Sidebar Elements +// +// Basic style of sidebar elements +.sidebar { + position: relative; + display: block; + min-height: 100%; + overflow-y: auto; + overflow-x: hidden; + border: none; + .transition(all 0.5s cubic-bezier(0.55, 0, 0.1, 1)); + .clearfix(); + background-color: @white; + + ul{ + padding-left: 0; + list-style-type: none; + } + + .sidebar-divider, .divider { + width: 80%; + height: 1px; + margin: 8px auto; + background-color: lighten(@gray, 20%); + } + + // Sidebar heading + //---------------- + .sidebar-header { + position: relative; + margin-bottom: @sidebar-padding; + .transition(all .2s ease-in-out); + } + + .sidebar-image { + padding-top: 24px; + img { + display: block; + margin: 0 auto; + } + } + + + // Sidebar icons + //---------------- + .sidebar-icon { + display: inline-block; + height: @sidebar-icon-height; + margin-right: @sidebar-divider; + text-align: left; + font-size: @sidebar-icon-height; + vertical-align: middle; + + &:before, &:after { + vertical-align: middle; + } + } + + .sidebar-nav { + margin: 0; + padding: 0; + + .sidebar-nav-base(); + + // Links + //---------------- + li { + position: relative; + list-style-type: none; + text-align: center; + + a { + position: relative; + cursor: pointer; + user-select: none; + .hashi-a-style-core(); + + svg{ + top: 2px; + width: 14px; + height: 14px; + margin-bottom: -2px; + margin-right: 4px; + } + } + } + } +} + +// Sidebar toggling +// +// Hide sidebar +.sidebar { + width: 0; + .translate3d(-@sidebar-desktop-width, 0, 0); + + &.open { + min-width: @sidebar-desktop-width; + width: @sidebar-desktop-width; + .translate3d(0, 0, 0); + } +} + +// Sidebar positions: fix the left/right sidebars +.sidebar-fixed-left, +.sidebar-fixed-right, +.sidebar-stacked { + position: fixed; + top: 0; + bottom: 0; + z-index: @zindex-sidebar-fixed; +} +.sidebar-stacked { + left: 0; +} +.sidebar-fixed-left { + left: 0; + box-shadow: 2px 0px 25px rgba(0,0,0,0.15); + -webkit-box-shadow: 2px 0px 25px rgba(0,0,0,0.15); +} +.sidebar-fixed-right { + right: 0; + box-shadow: 0px 2px 25px rgba(0,0,0,0.15); + -webkit-box-shadow: 0px 2px 25px rgba(0,0,0,0.15); + + .translate3d(@sidebar-desktop-width, 0, 0); + &.open { + .translate3d(0, 0, 0); + } + .icon-material-sidebar-arrow:before { + content: "\e614"; // icon-material-arrow-forward + } +} + +// Sidebar size +// +// Change size of sidebar and sidebar elements on small screens +@media (max-width: @screen-tablet) { + .sidebar.open { + min-width: @sidebar-width; + width: @sidebar-width; + } + + .sidebar .sidebar-header { + //height: @sidebar-width * 9/16; // 16:9 header dimension + } + + .sidebar .sidebar-image { + /* img { + width: @sidebar-width/4 - @sidebar-padding; + height: @sidebar-width/4 - @sidebar-padding; + } */ + } +} + +.sidebar-overlay { + visibility: hidden; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + background: @white; + z-index: @zindex-sidebar-fixed - 1; + + -webkit-transition: visibility 0 linear .4s,opacity .4s cubic-bezier(.4,0,.2,1); + -moz-transition: visibility 0 linear .4s,opacity .4s cubic-bezier(.4,0,.2,1); + transition: visibility 0 linear .4s,opacity .4s cubic-bezier(.4,0,.2,1); + -webkit-transform: translateZ(0); + -moz-transform: translateZ(0); + -ms-transform: translateZ(0); + -o-transform: translateZ(0); + transform: translateZ(0); +} + +.sidebar-overlay.active { + opacity: 0.3; + visibility: visible; + -webkit-transition-delay: 0; + -moz-transition-delay: 0; + transition-delay: 0; +} diff --git a/website/www/source/stylesheets/hashicorp-shared/_hashicorp-utility.less b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-utility.less new file mode 100755 index 000000000..f94100b1b --- /dev/null +++ b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-utility.less @@ -0,0 +1,70 @@ +// +// Hashicorp Nav (header/footer) Utiliy Vars and Mixins +// +// Notes: +// - Include this in Application.scss before header and feature-footer +// - Open Sans Google (Semibold - 600) font needs to be included if not already +// -------------------------------------------------- + +// Variables +@font-family-open-sans: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +@header-font-family: @font-family-open-sans; +@header-font-weight: 600; // semi-bold + +@header-height: 74px; +@header-mobile-height: 60px; +@by-hashicorp-width: 74px; +@by-hashicorp-height: 16px; +@nav-margin-right: 12px; + +// Mixins +.hashi-a-style-core{ + font-family: @header-font-family; + font-weight: @header-font-weight; + font-size: 14px; + //letter-spacing: 0.0625em; +} + +.hashi-a-style{ + margin: 0 15px; + padding: 0; + line-height: 22px; + .hashi-a-style-core(); + .transition( all 0.3s ease ); + + &:hover{ + .transition( all 0.3s ease ); + background-color: transparent; + } +} + +// +// ------------------------- +.anti-alias() { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +.open-light() { + font-family: @font-family-open-sans; + font-weight: 300; +} + +.open() { + font-family: @font-family-open-sans; + font-weight: 400; +} + +.open-sb() { + font-family: @font-family-open-sans; + font-weight: 600; +} + +.open-bold() { + font-family: @font-family-open-sans; + font-weight: 700; +} + +.bez-1-transition{ + .transition( all 300ms ease-in-out ); +} diff --git a/website/www/source/stylesheets/hashicorp-shared/_project-utility.less b/website/www/source/stylesheets/hashicorp-shared/_project-utility.less new file mode 100755 index 000000000..f27a6baaa --- /dev/null +++ b/website/www/source/stylesheets/hashicorp-shared/_project-utility.less @@ -0,0 +1,28 @@ +// +// Mixins Specific to project +// - make edits to mixins here +// -------------------------------------------------- + +// Variables +@project-logo-width: 40px; +@project-logo-height: 40px; +@project-logo-pad-left: 0px; + +// Mixins +.project-a-style{ + font-weight: 300; + opacity: .75; + + &:hover{ + color: $white; + opacity: 1; + } +} + +.project-footer-a-style{ + line-height: 30px; + + &:hover{ + opacity: .5; + } +} diff --git a/website/www/source/stylesheets/vagrantup.less b/website/www/source/stylesheets/vagrantup.less index 09d06a9cd..be37466e9 100644 --- a/website/www/source/stylesheets/vagrantup.less +++ b/website/www/source/stylesheets/vagrantup.less @@ -8,6 +8,12 @@ v a g r a n t u p @import '_type'; @import '_mixins'; @import '_base'; + +@import 'hashicorp-shared/_hashicorp-utility'; +@import 'hashicorp-shared/_project-utility'; +@import 'hashicorp-shared/_hashicorp-header'; +@import 'hashicorp-shared/_hashicorp-mobile-nav'; + @import '_nav'; @import '_components'; @import '_modules'; diff --git a/website/www/source/svg/_svg-by-hashicorp.erb b/website/www/source/svg/_svg-by-hashicorp.erb new file mode 100644 index 000000000..d89929590 --- /dev/null +++ b/website/www/source/svg/_svg-by-hashicorp.erb @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/website/www/source/svg/_svg-download.erb b/website/www/source/svg/_svg-download.erb new file mode 100644 index 000000000..6d8441fea --- /dev/null +++ b/website/www/source/svg/_svg-download.erb @@ -0,0 +1,4 @@ + + + diff --git a/website/www/source/svg/_svg-github.erb b/website/www/source/svg/_svg-github.erb new file mode 100644 index 000000000..f0264d5aa --- /dev/null +++ b/website/www/source/svg/_svg-github.erb @@ -0,0 +1,9 @@ + + + + diff --git a/website/www/source/svg/_svg-hashicorp-logo.erb b/website/www/source/svg/_svg-hashicorp-logo.erb new file mode 100644 index 000000000..60663e140 --- /dev/null +++ b/website/www/source/svg/_svg-hashicorp-logo.erb @@ -0,0 +1,7 @@ + From 0701d2dee6868ee3915377bb9f4dfaeb286a6e42 Mon Sep 17 00:00:00 2001 From: captainill Date: Sat, 14 Nov 2015 00:18:37 -0800 Subject: [PATCH 169/484] header left logo --- website/www/Gemfile | 2 +- website/www/Gemfile.lock | 9 +- website/www/source/images/logo-header.png | Bin 0 -> 5010 bytes website/www/source/images/logo-header@2x.png | Bin 0 -> 8863 bytes website/www/source/javascripts/vagrantup.js | 4 +- website/www/source/layouts/layout.erb | 45 ++++--- .../{ => layouts}/svg/_svg-by-hashicorp.erb | 0 .../{ => layouts}/svg/_svg-download.erb | 0 .../source/{ => layouts}/svg/_svg-github.erb | 0 .../{ => layouts}/svg/_svg-hashicorp-logo.erb | 0 website/www/source/stylesheets/_header.less | 126 ++++++++++++++++++ website/www/source/stylesheets/_nav.less | 59 ++++++-- .../hashicorp-shared/_hashicorp-header.less | 13 +- .../hashicorp-shared/_hashicorp-utility.less | 21 ++- .../hashicorp-shared/_project-utility.less | 6 +- website/www/source/stylesheets/vagrantup.less | 3 +- 16 files changed, 249 insertions(+), 39 deletions(-) create mode 100644 website/www/source/images/logo-header.png create mode 100644 website/www/source/images/logo-header@2x.png rename website/www/source/{ => layouts}/svg/_svg-by-hashicorp.erb (100%) rename website/www/source/{ => layouts}/svg/_svg-download.erb (100%) rename website/www/source/{ => layouts}/svg/_svg-github.erb (100%) rename website/www/source/{ => layouts}/svg/_svg-hashicorp-logo.erb (100%) create mode 100644 website/www/source/stylesheets/_header.less diff --git a/website/www/Gemfile b/website/www/Gemfile index 074cfa13c..1d1b3a338 100644 --- a/website/www/Gemfile +++ b/website/www/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' ruby "2.2.2" gem "builder", "~> 3.2.2" -gem "less", "~> 2.2.2" +gem "less", "~> 2.6.0" gem "middleman", "~> 3.1.5" gem "middleman-blog", "~> 3.3.0" gem "middleman-minify-html", "~> 3.1.1" diff --git a/website/www/Gemfile.lock b/website/www/Gemfile.lock index 946a40701..9f7cbd874 100644 --- a/website/www/Gemfile.lock +++ b/website/www/Gemfile.lock @@ -34,8 +34,8 @@ GEM hike (1.2.3) i18n (0.6.11) kramdown (1.9.0) - less (2.2.2) - commonjs (~> 0.2.6) + less (2.6.0) + commonjs (~> 0.2.7) libv8 (3.16.14.13) listen (1.3.1) rb-fsevent (>= 0.9.3) @@ -123,7 +123,7 @@ PLATFORMS DEPENDENCIES builder (~> 3.2.2) highline (~> 1.6.15) - less (~> 2.2.2) + less (~> 2.6.0) middleman (~> 3.1.5) middleman-blog (~> 3.3.0) middleman-minify-html (~> 3.1.1) @@ -133,3 +133,6 @@ DEPENDENCIES redcarpet (~> 3.0.0) therubyracer (~> 0.12.0) thin (~> 1.5.0) + +BUNDLED WITH + 1.10.6 diff --git a/website/www/source/images/logo-header.png b/website/www/source/images/logo-header.png new file mode 100644 index 0000000000000000000000000000000000000000..a058de16f12580fbc7e20901f6569695851d18b3 GIT binary patch literal 5010 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 z 0) $('nav').addClass("drop-shadow"); - if (top === 0) $('nav').removeClass("drop-shadow"); + if (top > 0) $('#header').addClass("drop-shadow"); + if (top === 0) $('header').removeClass("drop-shadow"); }); }); diff --git a/website/www/source/layouts/layout.erb b/website/www/source/layouts/layout.erb index 78d525fe5..5085bca2a 100644 --- a/website/www/source/layouts/layout.erb +++ b/website/www/source/layouts/layout.erb @@ -20,7 +20,7 @@ <%= javascript_include_tag "vagrantup" %> - + @@ -32,20 +32,35 @@
- - - + + <%= partial "layouts/mobile_nav" %> diff --git a/website/www/source/svg/_svg-by-hashicorp.erb b/website/www/source/layouts/svg/_svg-by-hashicorp.erb similarity index 100% rename from website/www/source/svg/_svg-by-hashicorp.erb rename to website/www/source/layouts/svg/_svg-by-hashicorp.erb diff --git a/website/www/source/svg/_svg-download.erb b/website/www/source/layouts/svg/_svg-download.erb similarity index 100% rename from website/www/source/svg/_svg-download.erb rename to website/www/source/layouts/svg/_svg-download.erb diff --git a/website/www/source/svg/_svg-github.erb b/website/www/source/layouts/svg/_svg-github.erb similarity index 100% rename from website/www/source/svg/_svg-github.erb rename to website/www/source/layouts/svg/_svg-github.erb diff --git a/website/www/source/svg/_svg-hashicorp-logo.erb b/website/www/source/layouts/svg/_svg-hashicorp-logo.erb similarity index 100% rename from website/www/source/svg/_svg-hashicorp-logo.erb rename to website/www/source/layouts/svg/_svg-hashicorp-logo.erb diff --git a/website/www/source/stylesheets/_header.less b/website/www/source/stylesheets/_header.less new file mode 100644 index 000000000..b83d9e6a6 --- /dev/null +++ b/website/www/source/stylesheets/_header.less @@ -0,0 +1,126 @@ +// +// Header +// - Project Specific +// - edits should be made here +// -------------------------------------------------- + +#header { + width: 100%; + // font-size: 15px; + text-transform: uppercase; + height: @header-height; + position: fixed; + top: 0; + left: 0; + background-color: @white; + z-index: 9999999999; + + &.docs { + background: @gray-background; + } + + .navbar-brand { + float: left; + .logo{ + padding-left: 36px; + font-size: 0; + line-height: 77px; + width: @project-logo-width; + padding-left: 0; + .img-retina('/images/logo-header.png', @project-logo-width, @project-logo-height, no-repeat); + background-position: 0 center; + + &:hover{ + opacity: .6; + } + } + + .by-hashicorp{ + color: @project-link-color; + + svg{ + path, + polygon{ + fill: @project-link-color; + } + line{ + stroke: @project-link-color; + } + } + + &:hover{ + color: black; + svg{ + path, + polygon{ + fill: black; + } + line{ + stroke: black; + } + } + } + + .svg-wrap{ + font-weight: 400; + } + } + } + + .buttons{ + margin-top: 2px; //baseline everything + + .navigation-links{ + float: right; + } + } + + .main-links, + .external-links { + li > a { + .project-a-style(); + } + } + + .main-links { + li > a { + color: white; + + &:hover{ + color: @project-link-color; + } + } + } +} + +@media (max-width: 768px) { + #header { + .navbar-brand { + + } + } +} + +@media (max-width: 414px) { + #header { + .navbar-brand { + .logo{ + padding-left: 37px; + font-size: 18px; + .img-retina('/images/logo-header.png', @project-logo-width * .75, @project-logo-height * .75, no-repeat); + //background-position: 0 45%; + } + } + } +} + + +@media (max-width: 320px) { + #header { + .navbar-brand { + .logo{ + font-size: 0 !important; //hide terraform text + } + } + } +} diff --git a/website/www/source/stylesheets/_nav.less b/website/www/source/stylesheets/_nav.less index 9c2d5c1bd..05d570861 100644 --- a/website/www/source/stylesheets/_nav.less +++ b/website/www/source/stylesheets/_nav.less @@ -17,14 +17,57 @@ nav { background: @gray-background; } - .vagrant-logo { - display: block; - text-indent: -999999px; - background: url(/images/logo_vagrant.png) no-repeat 0 0; - height: 70px; - width: 275px; - float: left; - margin: 10px 20px; + .logo { + font-size: 0; + .img-retina('/images/logo-header.png', @project-logo-width, @project-logo-height, no-repeat); + background-position: 0, center; + height: @project-logo-height; + width: @project-logo-width; + margin-left: 15px; + &:hover{ + opacity: .6; + } + } + + .by-hashicorp{ + &:hover{ + svg{ + line{ + opacity: .4; + } + } + } + } + + .by-hashicorp{ + color: @project-link-color; + + &:hover{ + color: black; + svg{ + path, + polygon{ + fill: black; + } + line{ + stroke: black; + } + } + } + + .svg-wrap{ + font-weight: 400; + } + + svg{ + path, + polygon{ + fill: @project-link-color; + } + line{ + stroke: @project-link-color; + } + } } .vagrant-docs-logo { diff --git a/website/www/source/stylesheets/hashicorp-shared/_hashicorp-header.less b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-header.less index abb94012c..53f561ab6 100755 --- a/website/www/source/stylesheets/hashicorp-shared/_hashicorp-header.less +++ b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-header.less @@ -2,11 +2,6 @@ // Hashicorp nav // -------------------------------------------------- -#header{ - position: relative; - margin-bottom: 0; -} - .navigation { color: black; text-rendering: optimizeLegibility; @@ -115,7 +110,6 @@ vertical-align:top; padding: 0; line-height: @header-height; - padding-left: @project-logo-width + @project-logo-pad-left; background-position: 0 center; .transition(all 300ms ease-in); @@ -145,7 +139,9 @@ font-family: @header-font-family; font-weight: 600; font-size: 0; + letter-spacing: 0; text-decoration: none; + text-transform: none; &.white{ color: white; @@ -166,8 +162,13 @@ text-decoration: none; } + &:hover{ + .transition(all 300ms ease-in); + } + .svg-wrap{ font-size: 13px; + .transition(all 300ms ease-in); } svg{ diff --git a/website/www/source/stylesheets/hashicorp-shared/_hashicorp-utility.less b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-utility.less index f94100b1b..8ecb75ee6 100755 --- a/website/www/source/stylesheets/hashicorp-shared/_hashicorp-utility.less +++ b/website/www/source/stylesheets/hashicorp-shared/_hashicorp-utility.less @@ -11,7 +11,7 @@ @header-font-family: @font-family-open-sans; @header-font-weight: 600; // semi-bold -@header-height: 74px; +@header-height: 80px; @header-mobile-height: 60px; @by-hashicorp-width: 74px; @by-hashicorp-height: 16px; @@ -38,6 +38,25 @@ } } +.img-retina(@image, @width, @height, @repeat: no-repeat) { + @filename : ~`/(.*)\.(jpg|jpeg|png|gif)/.exec(@{image})[1]`; + @extension : ~`/(.*)\.(jpg|jpeg|png|gif)/.exec(@{image})[2]`; + background-image: ~`"url(@{filename}.@{extension})"`; + background-repeat: @repeat; + + @media + only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and ( min--moz-device-pixel-ratio: 2), + only screen and ( -o-min-device-pixel-ratio: 2/1), + only screen and ( min-device-pixel-ratio: 2), + only screen and ( min-resolution: 192dpi), + only screen and ( min-resolution: 2dppx) { + /* on retina, use image that's scaled by 2 */ + background-image: ~`"url(@{filename}@2x.@{extension})"`; + background-size: @width @height; + } +} + // // ------------------------- .anti-alias() { diff --git a/website/www/source/stylesheets/hashicorp-shared/_project-utility.less b/website/www/source/stylesheets/hashicorp-shared/_project-utility.less index f27a6baaa..03d7eca54 100755 --- a/website/www/source/stylesheets/hashicorp-shared/_project-utility.less +++ b/website/www/source/stylesheets/hashicorp-shared/_project-utility.less @@ -4,14 +4,16 @@ // -------------------------------------------------- // Variables -@project-logo-width: 40px; -@project-logo-height: 40px; +@project-logo-width: 169px; +@project-logo-height: 46px; @project-logo-pad-left: 0px; +@project-link-color: #8d9ba8; // Mixins .project-a-style{ font-weight: 300; opacity: .75; + color: @project-link-color; &:hover{ color: $white; diff --git a/website/www/source/stylesheets/vagrantup.less b/website/www/source/stylesheets/vagrantup.less index be37466e9..de14b3c71 100644 --- a/website/www/source/stylesheets/vagrantup.less +++ b/website/www/source/stylesheets/vagrantup.less @@ -14,7 +14,8 @@ v a g r a n t u p @import 'hashicorp-shared/_hashicorp-header'; @import 'hashicorp-shared/_hashicorp-mobile-nav'; -@import '_nav'; +// @import '_nav'; +@import '_header'; @import '_components'; @import '_modules'; @import '_sidebar'; From 38616ca7bca8c91892143af7af1192e744673368 Mon Sep 17 00:00:00 2001 From: captainill Date: Sat, 14 Nov 2015 00:59:33 -0800 Subject: [PATCH 170/484] add navbar toggle --- website/www/source/layouts/layout.erb | 9 ++++----- .../hashicorp-shared/_hashicorp-header.less | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/website/www/source/layouts/layout.erb b/website/www/source/layouts/layout.erb index 5085bca2a..983504e8c 100644 --- a/website/www/source/layouts/layout.erb +++ b/website/www/source/layouts/layout.erb @@ -33,7 +33,7 @@
-