From f4fc2c742a05310df98f89cf042d3a8dacb772bd Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Tue, 13 May 2025 17:02:32 -0700 Subject: [PATCH] Update cloud-init behavior and iso generation This includes a couple modifications to the cloud-init behavior. First, the cloud-init wait action will now write a sentinel file after successfully waiting for cloud-init. This results in subsequent boots of the machine to skip executing the cloud-init wait command as cloud-init is only executed on the initial boot. Second, the cloud-init setup action will check for the sentinel file written by the cloud-init wait action, and if detected it will skip the cloud-init setup. When creating the ISO for cloud-init, a second sentinel file will be used to log the path of the generated ISO file. If the file exists, the ISO generation process will be skipped. --- .../action/builtin/cloud_init_setup.rb | 71 +++++-- lib/vagrant/action/builtin/cloud_init_wait.rb | 38 +++- lib/vagrant/action/builtin/disk.rb | 5 +- lib/vagrant/util/file_checksum.rb | 119 ++++++------ lib/vagrant/util/mime.rb | 2 +- plugins/kernel_v2/config/vm.rb | 3 + plugins/providers/virtualbox/action.rb | 13 +- .../action/builtin/cloud_init_setup_test.rb | 176 +++++++++++++++++- .../action/builtin/cloud_init_wait_test.rb | 63 ++++++- test/unit/vagrant/action/builtin/disk_test.rb | 5 +- .../docs/vagrantfile/machine_settings.mdx | 6 + 11 files changed, 397 insertions(+), 104 deletions(-) diff --git a/lib/vagrant/action/builtin/cloud_init_setup.rb b/lib/vagrant/action/builtin/cloud_init_setup.rb index 6cf512c01..02aa058c6 100644 --- a/lib/vagrant/action/builtin/cloud_init_setup.rb +++ b/lib/vagrant/action/builtin/cloud_init_setup.rb @@ -16,16 +16,38 @@ module Vagrant end def call(env) - machine = env[:machine] + catch(:complete) do + machine = env[:machine] - user_data_configs = machine.config.vm.cloud_init_configs - .select { |c| c.type == :user_data } + # The sentinel file in this check is written by the cloud init + # wait action and is only written after cloud init has completed. + @logger.info("Checking cloud-init sentinel file...") + sentinel_path = machine.data_dir.join("action_cloud_init") + if sentinel_path.file? + contents = sentinel_path.read.chomp + if machine.id.to_s == contents + if machine.config.vm.cloud_init_first_boot_only + @logger.info("Sentinel found for cloud-init, skipping") + throw :complete + else + @logger.info("Sentinel found for cloud-init but is configuration enabled") + end + else + @logger.debug("Found stale sentinel file, removing... (#{machine.id} != #{contents})") + end + sentinel_path.unlink + end - if !user_data_configs.empty? - user_data = setup_user_data(machine, env, user_data_configs) - meta_data = { "instance-id" => "i-#{machine.id.split('-').join}" } + user_data_configs = machine.config.vm.cloud_init_configs.select { |c| + c.type == :user_data + } - write_cfg_iso(machine, env, user_data, meta_data) + if !user_data_configs.empty? + user_data = setup_user_data(machine, env, user_data_configs) + meta_data = { "instance-id" => "i-#{machine.id.split('-').join}" } + + write_cfg_iso(machine, env, user_data, meta_data) + end end # Continue On @@ -88,24 +110,43 @@ module Vagrant # @param [Vagrant::Util::Mime::Multipart] user_data # @param [Hash] meta_data def write_cfg_iso(machine, env, user_data, meta_data) - iso_path = nil + raise Errors::CreateIsoHostCapNotFound if !env[:env].host.capability?(:create_iso) + + iso_path = catch(:iso_path) do + # This iso sentinel file is used to store the path of the + # generated iso file and its checksum. If the file does + # not exist, or the actual checksum of the file does not + # match that stored in the sentinel file, it is ignored + # and the iso is generated. This is used to prevent multiple + # iso file from being created over time. + iso_sentinel = env[:machine].data_dir.join("action_cloud_init_iso") + if iso_sentinel.file? + checksum, path = iso_sentinel.read.chomp.split(":", 2) + if File.exist?(path) && Vagrant::Util::FileChecksum.new(path, :sha256).checksum == checksum + throw :iso_path, Pathname.new(path) + end + iso_sentinel.unlink + end - if env[:env].host.capability?(:create_iso) begin source_dir = Pathname.new(Dir.mktmpdir(TEMP_PREFIX)) File.open("#{source_dir}/user-data", 'w') { |file| file.write(user_data.to_s) } - File.open("#{source_dir}/meta-data", 'w') { |file| file.write(meta_data.to_yaml) } - iso_path = env[:env].host.capability(:create_iso, - source_dir, volume_id: "cidata") - attach_disk_config(machine, env, iso_path.to_path) + env[:env].host.capability( + :create_iso, + source_dir, + volume_id: "cidata" + ).tap { |path| + checksum = Vagrant::Util::FileChecksum.new(path.to_path, :sha256).checksum + iso_sentinel.write("#{checksum}:#{path.to_path}") + } ensure FileUtils.remove_entry(source_dir) end - else - raise Errors::CreateIsoHostCapNotFound end + + attach_disk_config(machine, env, iso_path.to_path) end # Adds a new :dvd disk config with the given iso_path to be attached diff --git a/lib/vagrant/action/builtin/cloud_init_wait.rb b/lib/vagrant/action/builtin/cloud_init_wait.rb index 032af5057..ed6bbad98 100644 --- a/lib/vagrant/action/builtin/cloud_init_wait.rb +++ b/lib/vagrant/action/builtin/cloud_init_wait.rb @@ -14,19 +14,37 @@ module Vagrant end def call(env) - machine = env[:machine] - cloud_init_wait_cmd = "cloud-init status --wait" - if !machine.config.vm.cloud_init_configs.empty? - if machine.communicate.test("command -v cloud-init") - env[:ui].output(I18n.t("vagrant.cloud_init_waiting")) - result = machine.communicate.sudo(cloud_init_wait_cmd, error_check: false) - if result != 0 - raise Vagrant::Errors::CloudInitCommandFailed, cmd: cloud_init_wait_cmd, guest_name: machine.name + catch(:complete) do + machine = env[:machine] + sentinel_path = machine.data_dir.join("action_cloud_init") + + @logger.info("Checking cloud-init sentinel file...") + if sentinel_path.file? + contents = sentinel_path.read.chomp + if machine.id.to_s == contents + @logger.info("Sentinel found for cloud-init, skipping") + throw :complete end - else - raise Vagrant::Errors::CloudInitNotFound, guest_name: machine.name + @logger.debug("Found stale sentinel file, removing... (#{machine.id} != #{contents})") + sentinel_path.unlink end + + cloud_init_wait_cmd = "cloud-init status --wait" + if !machine.config.vm.cloud_init_configs.empty? + if machine.communicate.test("command -v cloud-init") + env[:ui].output(I18n.t("vagrant.cloud_init_waiting")) + result = machine.communicate.sudo(cloud_init_wait_cmd, error_check: false) + if result != 0 + raise Vagrant::Errors::CloudInitCommandFailed, cmd: cloud_init_wait_cmd, guest_name: machine.name + end + else + raise Vagrant::Errors::CloudInitNotFound, guest_name: machine.name + end + end + # Write sentinel path + sentinel_path.write(machine.id.to_s) end + @app.call(env) end end diff --git a/lib/vagrant/action/builtin/disk.rb b/lib/vagrant/action/builtin/disk.rb index 6b6f2420e..87645d528 100644 --- a/lib/vagrant/action/builtin/disk.rb +++ b/lib/vagrant/action/builtin/disk.rb @@ -27,7 +27,10 @@ module Vagrant end end - write_disk_metadata(machine, configured_disks) unless configured_disks.empty? + # Always write the disk metadata even if the configured + # disks is empty. This ensure that old entries are not + # orphaned in the metadata file. + write_disk_metadata(machine, configured_disks) # Continue On @app.call(env) diff --git a/lib/vagrant/util/file_checksum.rb b/lib/vagrant/util/file_checksum.rb index 83409eb50..2ab229335 100644 --- a/lib/vagrant/util/file_checksum.rb +++ b/lib/vagrant/util/file_checksum.rb @@ -13,65 +13,74 @@ class DigestClass def hexdigest; end end -class FileChecksum - BUFFER_SIZE = 1024 * 8 +module Vagrant + module Util + class FileChecksum + BUFFER_SIZE = 1024 * 8 - # Supported file checksum - CHECKSUM_MAP = { - :md5 => Digest::MD5, - :sha1 => Digest::SHA1, - :sha256 => Digest::SHA256, - :sha384 => Digest::SHA384, - :sha512 => Digest::SHA512 - }.freeze + # Supported file checksum + CHECKSUM_MAP = { + :md5 => Digest::MD5, + :sha1 => Digest::SHA1, + :sha256 => Digest::SHA256, + :sha384 => Digest::SHA384, + :sha512 => Digest::SHA512 + }.freeze - # Initializes an object to calculate the checksum of a file. The given - # ``digest_klass`` should implement the ``DigestClass`` interface. Note - # that the built-in Ruby digest classes duck type this properly: - # Digest::MD5, Digest::SHA1, etc. - def initialize(path, digest_klass) - if digest_klass.is_a?(Class) - @digest_klass = digest_klass - else - @digest_klass = load_digest(digest_klass) - end - - @path = path - end - - # This calculates the checksum of the file and returns it as a - # string. - # - # @return [String] - def checksum - digest = @digest_klass.new - buf = '' - - File.open(@path, "rb") do |f| - while !f.eof - begin - f.readpartial(BUFFER_SIZE, buf) - digest.update(buf) - rescue EOFError - # Although we check for EOF earlier, this seems to happen - # sometimes anyways [GH-2716]. - break + # Initializes an object to calculate the checksum of a file. The given + # ``digest_klass`` should implement the ``DigestClass`` interface. Note + # that the built-in Ruby digest classes duck type this properly: + # Digest::MD5, Digest::SHA1, etc. + def initialize(path, digest_klass) + if digest_klass.is_a?(Class) + @digest_klass = digest_klass + else + @digest_klass = load_digest(digest_klass) end + + @path = path + end + + # This calculates the checksum of the file and returns it as a + # string. + # + # @return [String] + def checksum + digest = @digest_klass.new + buf = '' + + File.open(@path, "rb") do |f| + while !f.eof + begin + f.readpartial(BUFFER_SIZE, buf) + digest.update(buf) + rescue EOFError + # Although we check for EOF earlier, this seems to happen + # sometimes anyways [GH-2716]. + break + end + end + end + + digest.hexdigest + end + + private + + def load_digest(type) + digest = CHECKSUM_MAP[type.to_s.downcase.to_sym] + if digest.nil? + raise Vagrant::Errors::BoxChecksumInvalidType, + type: type.to_s, + types: CHECKSUM_MAP.keys.join(', ') + end + digest end end - - digest.hexdigest - end - - private - - def load_digest(type) - digest = CHECKSUM_MAP[type.to_s.downcase.to_sym] - if digest.nil? - raise Vagrant::Errors::BoxChecksumInvalidType, - type: type.to_s, - types: CHECKSUM_MAP.keys.join(', ') - end - digest end end + +# NOTE: This class was not originally namespaced +# with the Util module so this is left for backwards +# compatibility. +FileChecksum = Vagrant::Util::FileChecksum diff --git a/lib/vagrant/util/mime.rb b/lib/vagrant/util/mime.rb index 8b1036c6d..028d595cc 100644 --- a/lib/vagrant/util/mime.rb +++ b/lib/vagrant/util/mime.rb @@ -70,7 +70,7 @@ module Vagrant # @param [String] type of the entity content def initialize(content, content_type) if !MIME::Types.include?(content_type) - MIME::Types.add(MIME::Type.new(content_type)) + MIME::Types.add(MIME::Type.new("content-type" => content_type)) end @content = content @content_type = MIME::Types[content_type].first diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 96980d1e1..f9827f950 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -47,6 +47,7 @@ module VagrantPlugins attr_accessor :box_download_insecure attr_accessor :box_download_location_trusted attr_accessor :box_download_options + attr_accessor :cloud_init_first_boot_only attr_accessor :communicator attr_accessor :graceful_halt_timeout attr_accessor :guest @@ -87,6 +88,7 @@ module VagrantPlugins @box_version = UNSET_VALUE @allow_hosts_modification = UNSET_VALUE @clone = UNSET_VALUE + @cloud_init_first_boot_only = UNSET_VALUE @communicator = UNSET_VALUE @graceful_halt_timeout = UNSET_VALUE @guest = UNSET_VALUE @@ -536,6 +538,7 @@ module VagrantPlugins @box_extra_download_options = Vagrant::Util::MapCommandOptions.map_to_command_options(@box_download_options) @allow_hosts_modification = true if @allow_hosts_modification == UNSET_VALUE @clone = nil if @clone == UNSET_VALUE + @cloud_init_first_boot_only = @cloud_init_first_boot_only == UNSET_VALUE ? true : !!@cloud_init_first_boot_only @communicator = nil if @communicator == UNSET_VALUE @graceful_halt_timeout = 60 if @graceful_halt_timeout == UNSET_VALUE @guest = nil if @guest == UNSET_VALUE diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index fe7550565..96ab23ec1 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -82,22 +82,14 @@ module VagrantPlugins b.use ForwardPorts b.use SetHostname b.use SaneDefaults - b.use Call, IsEnvSet, :cloud_init do |env, b2| - if env[:result] - b2.use CloudInitSetup - end - end + b.use CloudInitSetup b.use CleanupDisks b.use Disk b.use Customize, "pre-boot" b.use Boot b.use Customize, "post-boot" b.use WaitForCommunicator, [:starting, :running, :paused] - b.use Call, IsEnvSet, :cloud_init do |env, b2| - if env[:result] - b2.use CloudInitWait - end - end + b.use CloudInitWait b.use Customize, "post-comm" b.use CheckGuestAdditions end @@ -424,7 +416,6 @@ module VagrantPlugins end end - b.use EnvSet, cloud_init: true b.use action_start end end diff --git a/test/unit/vagrant/action/builtin/cloud_init_setup_test.rb b/test/unit/vagrant/action/builtin/cloud_init_setup_test.rb index 29c608ccf..1d9775fee 100644 --- a/test/unit/vagrant/action/builtin/cloud_init_setup_test.rb +++ b/test/unit/vagrant/action/builtin/cloud_init_setup_test.rb @@ -6,7 +6,8 @@ require "vagrant/util/mime" describe Vagrant::Action::Builtin::CloudInitSetup do let(:app) { lambda { |env| } } - let(:vm) { double("vm", disk: disk, disks: disks) } + let(:vm) { double("vm", disk: disk, disks: disks, cloud_init_first_boot_only: first_boot_only) } + let(:first_boot_only) { true } let(:disk) { double("disk") } let(:disks) { double("disk") } let(:config) { double("config", vm: vm) } @@ -59,10 +60,69 @@ describe Vagrant::Action::Builtin::CloudInitSetup do expect(subject).not_to receive(:setup_user_data) expect(subject).not_to receive(:write_cfg_iso) - expect(subject).not_to receive(:attack_disk_config) + expect(subject).not_to receive(:attach_disk_config) subject.call(env) end + + context "sentinel file" do + let(:sentinel) { double("sentinel") } + let(:sentinel_exists) { false } + let(:sentinel_contents) { "" } + + before do + allow(machine).to receive_message_chain(:data_dir, :join).with("action_cloud_init").and_return(sentinel) + allow(sentinel).to receive(:file?).and_return(sentinel_exists) + allow(sentinel).to receive(:read).and_return(sentinel_contents) + allow(sentinel).to receive(:unlink) + + allow(vm).to receive(:cloud_init_configs).and_return(cloud_init_configs) + allow(subject).to receive(:setup_user_data) + allow(subject).to receive(:write_cfg_iso) + end + + context "when file exists" do + let(:sentinel_exists) { true } + + context "when file contains machine id" do + let(:sentinel_contents) { machine.id.to_s } + + it "should not write iso configuration" do + expect(subject).not_to receive(:write_cfg_iso) + + subject.call(env) + end + + context "when configuration enables on all boots" do + let(:first_boot_only) { false } + + it "should write the iso configuration" do + expect(subject).to receive(:write_cfg_iso) + subject.call(env) + end + + it "should remove sentinel file" do + expect(sentinel).to receive(:unlink) + subject.call(env) + end + end + end + + context "when file does not contain machine id" do + let(:sentinel_contents) { "unknown-id" } + + it "should write iso configuration" do + expect(subject).to receive(:write_cfg_iso) + subject.call(env) + end + + it "should remove sentinel file" do + expect(sentinel).to receive(:unlink) + subject.call(env) + end + end + end + end end describe "#setup_user_data" do @@ -117,9 +177,18 @@ describe Vagrant::Action::Builtin::CloudInitSetup do let(:iso_path) { Pathname.new("fake/iso/path") } let(:source_dir) { Pathname.new("fake/source/path") } let(:meta_data_file) { double("meta_data_file") } + let(:sentinel) { double("sentinel") } + let(:sentinel_exists) { false } + let(:file_checksum) { double("file_checksum", checksum: checksum) } + let(:checksum) { "DUMMY-CHECKSUM-VALUE" } before do allow(meta_data_file).to receive(:write).and_return(true) + allow(machine).to receive_message_chain(:data_dir, :join).with("action_cloud_init_iso").and_return(sentinel) + allow(sentinel).to receive(:file?).and_return(sentinel_exists) + allow(sentinel).to receive(:write) + allow(Vagrant::Util::FileChecksum).to receive(:new).with(iso_path, :sha256).and_return(file_checksum) + allow(Vagrant::Util::FileChecksum).to receive(:new).with(iso_path.to_s, :sha256).and_return(file_checksum) end it "raises an error if the host capability is not supported" do @@ -140,8 +209,111 @@ describe Vagrant::Action::Builtin::CloudInitSetup do expect(vm.disks).to receive(:each) expect(meta_data).to receive(:to_yaml) + subject.write_cfg_iso(machine, env, message, meta_data) end + + context "sentinel file" do + let(:user_data) { double("user_data") } + let(:sentinel_contents) { "" } + + before do + allow(sentinel).to receive(:read).and_return(sentinel_contents) + allow(sentinel).to receive(:unlink) + + allow(host).to receive(:capability?).with(:create_iso).and_return(true) + allow(Dir).to receive(:mktmpdir).and_return(source_dir) + allow(File).to receive(:open).with("#{source_dir}/user-data", 'w').and_return(true) + allow(File).to receive(:open).with("#{source_dir}/meta-data", 'w').and_yield(meta_data_file) + allow(FileUtils).to receive(:remove_entry).with(source_dir).and_return(true) + allow(FileUtils).to receive(:remove_entry).with(source_dir).and_return(true) + allow(host).to receive(:capability).with(:create_iso, source_dir, volume_id: "cidata").and_return(iso_path) + allow(subject).to receive(:attach_disk_config) + end + + context "when file exists" do + let(:sentinel_exists) { true } + + context "when file contents is iso path" do + let(:sentinel_contents) { "#{checksum}:#{iso_path}" } + + context "when file contents path exists" do + before do + expect(File).to receive(:exist?).with(iso_path.to_s).and_return(true) + end + + it "should not create iso" do + expect(host).not_to receive(:capability) + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + + it "should attach with the iso path" do + expect(subject).to receive(:attach_disk_config).with(machine, env, iso_path.to_path) + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + + it "should not write the sentinel file" do + expect(sentinel).not_to receive(:write) + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + end + + context "when file contents path does not exist" do + before do + expect(File).to receive(:exist?).with(iso_path.to_s).and_return(false) + end + + it "should create iso" do + expect(host).to receive(:capability).with(:create_iso, source_dir, volume_id: "cidata").and_return(iso_path) + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + + it "should remove the sentinel file" do + expect(sentinel).to receive(:unlink) + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + + it "should write the sentinel file" do + expect(sentinel).to receive(:write).with("#{checksum}:#{iso_path}") + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + end + + context "when file contents checksum does not match existing file checksum" do + let(:sentinel_contents) { "BAD-CHECKSUM-VALUE:#{iso_path}" } + + it "should create iso" do + expect(host).to receive(:capability).with(:create_iso, source_dir, volume_id: "cidata").and_return(iso_path) + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + + it "should remove the sentinel file" do + expect(sentinel).to receive(:unlink) + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + + it "should write the sentinel file" do + expect(sentinel).to receive(:write).with("#{checksum}:#{iso_path}") + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + end + end + end + + context "when file does not exist" do + let(:sentinel_exists) { false } + + it "should create iso" do + expect(host).to receive(:capability).with(:create_iso, source_dir, volume_id: "cidata").and_return(iso_path) + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + + it "should write the sentinel file" do + expect(sentinel).to receive(:write).with("#{checksum}:#{iso_path}") + subject.write_cfg_iso(machine, env, user_data, meta_data) + end + end + end end describe "#attach_disk_config" do diff --git a/test/unit/vagrant/action/builtin/cloud_init_wait_test.rb b/test/unit/vagrant/action/builtin/cloud_init_wait_test.rb index 7c27ba788..25cbac04c 100644 --- a/test/unit/vagrant/action/builtin/cloud_init_wait_test.rb +++ b/test/unit/vagrant/action/builtin/cloud_init_wait_test.rb @@ -8,22 +8,30 @@ describe Vagrant::Action::Builtin::CloudInitWait do let(:app) { lambda { |env| } } let(:config) { double("config", :vm => vm) } let(:comm) { double("comm") } - let(:machine) { double("machie", :config => config, :communicate => comm, :name => "test") } + let(:machine) { double("machine", :config => config, :communicate => comm, :name => "test", id: "m-id", data_dir: data_dir) } + let(:data_dir) { double("data_dir") } let(:ui) { Vagrant::UI::Silent.new } let(:env) { { machine: machine, ui: ui} } + let(:sentinel) { double("sentinel_path", unlink: nil) } let(:subject) { described_class.new(app, env) } describe "#call" do + let(:sentinel_exists) { false } + let(:sentinel_contents) { "" } + + before do + allow(data_dir).to receive(:join).with("action_cloud_init").and_return(sentinel) + allow(sentinel).to receive(:file?).and_return(sentinel_exists) + allow(sentinel).to receive(:read).and_return(sentinel_contents) + allow(sentinel).to receive(:write).with(machine.id) + allow(comm).to receive(:test).with("command -v cloud-init").and_return(true) + allow(comm).to receive(:sudo).with("cloud-init status --wait", error_check: false).and_return(0) + end context "cloud init configuration exists" do - let(:vm) { double("vm", cloud_init_configs: ["some config"]) } - before do - allow(comm).to receive(:test).with("command -v cloud-init").and_return(true) - end - it "waits for cloud init to be executed" do expect(comm).to receive(:sudo).with("cloud-init status --wait", any_args).and_return(0) subject.call(env) @@ -40,10 +48,51 @@ describe Vagrant::Action::Builtin::CloudInitWait do expect { subject.call(env) }. to raise_error(Vagrant::Errors::CloudInitCommandFailed) end + + context "when sentinel file exists" do + let(:sentinel_exists) { true } + + context "when sentinel contents is machine id" do + let(:sentinel_contents) { machine.id.to_s } + + it "should not test for cloud-init" do + expect(comm).not_to receive(:test).with(/cloud-init/) + subject.call(env) + end + + it "should not run cloud-init" do + expect(comm).not_to receive(:sudo).with(/cloud-init/, anything) + subject.call(env) + end + + it "should not write sentinel file" do + expect(sentinel).not_to receive(:write) + subject.call(env) + end + end + + context "when sentinel content is not machine id" do + let(:sentinel_contents) { "unknown-id" } + + it "should test for cloud-init" do + expect(comm).to receive(:test).with(/cloud-init/) + subject.call(env) + end + + it "should run cloud-init" do + expect(comm).to receive(:sudo).with(/cloud-init/, anything) + subject.call(env) + end + + it "should write sentinel file" do + expect(sentinel).to receive(:write).with(machine.id) + subject.call(env) + end + end + end end context "no cloud init configuration" do - let(:vm) { double("vm", cloud_init_configs: []) } before do diff --git a/test/unit/vagrant/action/builtin/disk_test.rb b/test/unit/vagrant/action/builtin/disk_test.rb index 098ca2c3e..f98a58558 100644 --- a/test/unit/vagrant/action/builtin/disk_test.rb +++ b/test/unit/vagrant/action/builtin/disk_test.rb @@ -34,14 +34,14 @@ describe Vagrant::Action::Builtin::Disk do subject.call(env) end - it "continues on if no disk config present" do + it "writes a disk_meta file if no disk config is present" do allow(vm).to receive(:disks).and_return([]) subject = described_class.new(app, env) expect(app).to receive(:call).with(env).ordered expect(machine.provider).not_to receive(:capability).with(:configure_disks, disks) - expect(subject).not_to receive(:write_disk_metadata) + expect(subject).to receive(:write_disk_metadata) subject.call(env) end @@ -50,6 +50,7 @@ describe Vagrant::Action::Builtin::Disk do allow(vm).to receive(:disks).and_return(disks) allow(machine.provider).to receive(:capability?).with(:configure_disks).and_return(false) subject = described_class.new(app, env) + allow(subject).to receive(:write_disk_metadata) expect(app).to receive(:call).with(env).ordered expect(machine.provider).not_to receive(:capability).with(:configure_disks, disks) diff --git a/website/content/docs/vagrantfile/machine_settings.mdx b/website/content/docs/vagrantfile/machine_settings.mdx index 430979806..3931d0826 100644 --- a/website/content/docs/vagrantfile/machine_settings.mdx +++ b/website/content/docs/vagrantfile/machine_settings.mdx @@ -121,6 +121,12 @@ the name of the synced folder plugin. - `config.vm.cloud_init` - Stores various [cloud_init](/vagrant/docs/cloud-init) configurations on the machine. +- `config.vm.cloud_init_first_boot_only` - (boolean) - If true then the cloud-init + configuration will only be generated and attached on the first successful boot of + the machine. Subsequent boots of the machine will not generate the cloud-init + configuration and the `cloud-init wait` command will not be executed. Defaults + to `true`. + - `config.vm.communicator` (string) - The communicator type to use to connect to the guest box. By default this is `"ssh"`, but should be changed to `"winrm"` for Windows guests.