Repository: hashicorp/vagrant Branch: main Commit: 0b7d460a450e Files: 1925 Total size: 6.1 MB Directory structure: gitextract_w81_rx3j/ ├── .ci/ │ ├── .ci-utility-files/ │ │ └── common.sh │ ├── build │ ├── dev-build │ ├── generate-licenses │ ├── load-ci.sh │ ├── nightly-build │ ├── release │ ├── release-initiator │ ├── spec/ │ │ ├── clean-packet.sh │ │ ├── create-hosts.sh │ │ ├── create-packet.sh │ │ ├── env.sh │ │ ├── notify-success.sh │ │ ├── pull-log.sh │ │ └── run-test.sh │ └── sync.sh ├── .copywrite.hcl ├── .github/ │ ├── CODEOWNERS │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ ├── config.yml │ │ ├── engineering-task.md │ │ └── vagrant-feature-request.md │ ├── dependabot.yml │ └── workflows/ │ ├── backport.yml │ ├── check-legacy-links-format.yml │ ├── code.yml │ ├── dev-appimage-build.yml │ ├── dev-arch-build.yml │ ├── dev-build.yml │ ├── dev-debs-build.yml │ ├── dev-macos-build.yml │ ├── dev-rpms-build.yml │ ├── dev-windows-build.yml │ ├── initiate-release.yml │ ├── lock.yml │ ├── nightlies.yml │ ├── release.yml │ ├── testing-skipped.yml │ └── testing.yml ├── .gitignore ├── .gitmodules ├── .vimrc ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── RELEASE.md ├── Rakefile ├── Vagrantfile ├── bin/ │ └── vagrant ├── builtin/ │ ├── README.md │ ├── configvagrant/ │ │ └── main.go │ ├── httpdownloader/ │ │ ├── downloader/ │ │ │ ├── downloader.go │ │ │ └── httpmethod_string.go │ │ └── main.go │ ├── myplugin/ │ │ ├── command/ │ │ │ ├── command.go │ │ │ ├── dothing.go │ │ │ ├── host.go │ │ │ ├── info.go │ │ │ └── interactive.go │ │ ├── communicator/ │ │ │ └── communicator.go │ │ ├── host/ │ │ │ ├── alwaystrue.go │ │ │ └── cap/ │ │ │ └── write_hello.go │ │ ├── locales/ │ │ │ ├── assets/ │ │ │ │ ├── en.json │ │ │ │ └── es.json │ │ │ └── locales.go │ │ ├── main.go │ │ ├── mappers.go │ │ ├── proto/ │ │ │ ├── plugin.pb.go │ │ │ └── plugin.proto │ │ ├── provider/ │ │ │ └── happy.go │ │ └── push/ │ │ └── encouragement.go │ └── otherplugin/ │ ├── command.go │ ├── guest/ │ │ ├── alwaystrue.go │ │ └── cap/ │ │ └── write_hello.go │ └── main.go ├── contrib/ │ ├── README.md │ ├── bash/ │ │ └── completion.sh │ ├── emacs/ │ │ └── vagrant.el │ ├── st/ │ │ └── Ruby.sublime-settings │ ├── sudoers/ │ │ ├── linux-fedora │ │ ├── linux-suse │ │ ├── linux-ubuntu │ │ └── osx │ ├── vim/ │ │ └── vagrantfile.vim │ └── zsh/ │ ├── _vagrant │ └── generate_zsh_completion.rb ├── ext/ │ └── vagrant/ │ └── vagrant_ssl/ │ ├── extconf.rb │ ├── vagrant_ssl.c │ └── vagrant_ssl.h ├── keys/ │ ├── README.md │ ├── vagrant │ ├── vagrant.key.ed25519 │ ├── vagrant.key.rsa │ ├── vagrant.pub │ ├── vagrant.pub.ed25519 │ └── vagrant.pub.rsa ├── lib/ │ ├── vagrant/ │ │ ├── action/ │ │ │ ├── builder.rb │ │ │ ├── builtin/ │ │ │ │ ├── box_add.rb │ │ │ │ ├── box_check_outdated.rb │ │ │ │ ├── box_remove.rb │ │ │ │ ├── box_update.rb │ │ │ │ ├── call.rb │ │ │ │ ├── cleanup_disks.rb │ │ │ │ ├── cloud_init_setup.rb │ │ │ │ ├── cloud_init_wait.rb │ │ │ │ ├── config_validate.rb │ │ │ │ ├── confirm.rb │ │ │ │ ├── delayed.rb │ │ │ │ ├── destroy_confirm.rb │ │ │ │ ├── disk.rb │ │ │ │ ├── env_set.rb │ │ │ │ ├── graceful_halt.rb │ │ │ │ ├── handle_box.rb │ │ │ │ ├── handle_box_url.rb │ │ │ │ ├── handle_forwarded_port_collisions.rb │ │ │ │ ├── has_provisioner.rb │ │ │ │ ├── is_env_set.rb │ │ │ │ ├── is_state.rb │ │ │ │ ├── lock.rb │ │ │ │ ├── message.rb │ │ │ │ ├── mixin_provisioners.rb │ │ │ │ ├── mixin_synced_folders.rb │ │ │ │ ├── prepare_clone.rb │ │ │ │ ├── provision.rb │ │ │ │ ├── provisioner_cleanup.rb │ │ │ │ ├── set_hostname.rb │ │ │ │ ├── ssh_exec.rb │ │ │ │ ├── ssh_run.rb │ │ │ │ ├── synced_folder_cleanup.rb │ │ │ │ ├── synced_folders.rb │ │ │ │ ├── trigger.rb │ │ │ │ └── wait_for_communicator.rb │ │ │ ├── general/ │ │ │ │ ├── package.rb │ │ │ │ ├── package_setup_files.rb │ │ │ │ └── package_setup_folders.rb │ │ │ ├── hook.rb │ │ │ ├── primary_runner.rb │ │ │ ├── runner.rb │ │ │ └── warden.rb │ │ ├── action.rb │ │ ├── alias.rb │ │ ├── batch_action.rb │ │ ├── box.rb │ │ ├── box_collection.rb │ │ ├── box_metadata.rb │ │ ├── bundler.rb │ │ ├── capability_host.rb │ │ ├── cli.rb │ │ ├── config/ │ │ │ ├── loader.rb │ │ │ ├── v1/ │ │ │ │ ├── dummy_config.rb │ │ │ │ ├── loader.rb │ │ │ │ └── root.rb │ │ │ ├── v1.rb │ │ │ ├── v2/ │ │ │ │ ├── dummy_config.rb │ │ │ │ ├── loader.rb │ │ │ │ ├── root.rb │ │ │ │ └── util.rb │ │ │ ├── v2.rb │ │ │ └── version_base.rb │ │ ├── config.rb │ │ ├── environment.rb │ │ ├── errors.rb │ │ ├── guest.rb │ │ ├── host.rb │ │ ├── machine.rb │ │ ├── machine_index.rb │ │ ├── machine_state.rb │ │ ├── patches/ │ │ │ ├── builder/ │ │ │ │ └── mkmf.rb │ │ │ ├── fake_ftp.rb │ │ │ ├── log4r.rb │ │ │ ├── net-ssh.rb │ │ │ ├── rubygems.rb │ │ │ └── timeout_error.rb │ │ ├── plugin/ │ │ │ ├── manager.rb │ │ │ ├── state_file.rb │ │ │ ├── v1/ │ │ │ │ ├── command.rb │ │ │ │ ├── communicator.rb │ │ │ │ ├── config.rb │ │ │ │ ├── errors.rb │ │ │ │ ├── guest.rb │ │ │ │ ├── host.rb │ │ │ │ ├── manager.rb │ │ │ │ ├── plugin.rb │ │ │ │ ├── provider.rb │ │ │ │ └── provisioner.rb │ │ │ ├── v1.rb │ │ │ ├── v2/ │ │ │ │ ├── command.rb │ │ │ │ ├── communicator.rb │ │ │ │ ├── components.rb │ │ │ │ ├── config.rb │ │ │ │ ├── errors.rb │ │ │ │ ├── guest.rb │ │ │ │ ├── host.rb │ │ │ │ ├── manager.rb │ │ │ │ ├── plugin.rb │ │ │ │ ├── provider.rb │ │ │ │ ├── provisioner.rb │ │ │ │ ├── push.rb │ │ │ │ ├── synced_folder.rb │ │ │ │ └── trigger.rb │ │ │ └── v2.rb │ │ ├── plugin.rb │ │ ├── registry.rb │ │ ├── shared_helpers.rb │ │ ├── ui.rb │ │ ├── util/ │ │ │ ├── ansi_escape_code_remover.rb │ │ │ ├── busy.rb │ │ │ ├── caps.rb │ │ │ ├── checkpoint_client.rb │ │ │ ├── command_deprecation.rb │ │ │ ├── counter.rb │ │ │ ├── credential_scrubber.rb │ │ │ ├── curl_helper.rb │ │ │ ├── deep_merge.rb │ │ │ ├── directory.rb │ │ │ ├── downloader.rb │ │ │ ├── env.rb │ │ │ ├── experimental.rb │ │ │ ├── file_checksum.rb │ │ │ ├── file_mode.rb │ │ │ ├── file_mutex.rb │ │ │ ├── guest_hosts.rb │ │ │ ├── guest_inspection.rb │ │ │ ├── guest_networks.rb │ │ │ ├── hash_with_indifferent_access.rb │ │ │ ├── install_cli_autocomplete.rb │ │ │ ├── io.rb │ │ │ ├── ipv4_interfaces.rb │ │ │ ├── is_port_open.rb │ │ │ ├── keypair.rb │ │ │ ├── line_buffer.rb │ │ │ ├── line_ending_helpers.rb │ │ │ ├── logging_formatter.rb │ │ │ ├── map_command_options.rb │ │ │ ├── mime.rb │ │ │ ├── network_ip.rb │ │ │ ├── numeric.rb │ │ │ ├── platform.rb │ │ │ ├── powershell.rb │ │ │ ├── presence.rb │ │ │ ├── retryable.rb │ │ │ ├── safe_chdir.rb │ │ │ ├── safe_env.rb │ │ │ ├── safe_exec.rb │ │ │ ├── safe_puts.rb │ │ │ ├── scoped_hash_override.rb │ │ │ ├── shell_quote.rb │ │ │ ├── silence_warnings.rb │ │ │ ├── ssh.rb │ │ │ ├── stacked_proc_runner.rb │ │ │ ├── string_block_editor.rb │ │ │ ├── subprocess.rb │ │ │ ├── template_renderer.rb │ │ │ ├── uploader.rb │ │ │ ├── which.rb │ │ │ └── windows_path.rb │ │ ├── util.rb │ │ ├── vagrantfile.rb │ │ └── version.rb │ └── vagrant.rb ├── plugins/ │ ├── README.md │ ├── commands/ │ │ ├── autocomplete/ │ │ │ ├── command/ │ │ │ │ ├── install.rb │ │ │ │ └── root.rb │ │ │ └── plugin.rb │ │ ├── box/ │ │ │ ├── command/ │ │ │ │ ├── add.rb │ │ │ │ ├── download_mixins.rb │ │ │ │ ├── list.rb │ │ │ │ ├── outdated.rb │ │ │ │ ├── prune.rb │ │ │ │ ├── remove.rb │ │ │ │ ├── repackage.rb │ │ │ │ ├── root.rb │ │ │ │ └── update.rb │ │ │ └── plugin.rb │ │ ├── cap/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── cloud/ │ │ │ ├── auth/ │ │ │ │ ├── login.rb │ │ │ │ ├── logout.rb │ │ │ │ ├── middleware/ │ │ │ │ │ ├── add_authentication.rb │ │ │ │ │ └── add_downloader_authentication.rb │ │ │ │ ├── plugin.rb │ │ │ │ ├── root.rb │ │ │ │ └── whoami.rb │ │ │ ├── box/ │ │ │ │ ├── create.rb │ │ │ │ ├── delete.rb │ │ │ │ ├── plugin.rb │ │ │ │ ├── root.rb │ │ │ │ ├── show.rb │ │ │ │ └── update.rb │ │ │ ├── client/ │ │ │ │ └── client.rb │ │ │ ├── errors.rb │ │ │ ├── list.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── plugin.rb │ │ │ ├── provider/ │ │ │ │ ├── create.rb │ │ │ │ ├── delete.rb │ │ │ │ ├── plugin.rb │ │ │ │ ├── root.rb │ │ │ │ ├── update.rb │ │ │ │ └── upload.rb │ │ │ ├── publish.rb │ │ │ ├── root.rb │ │ │ ├── search.rb │ │ │ ├── util.rb │ │ │ └── version/ │ │ │ ├── create.rb │ │ │ ├── delete.rb │ │ │ ├── plugin.rb │ │ │ ├── release.rb │ │ │ ├── revoke.rb │ │ │ ├── root.rb │ │ │ └── update.rb │ │ ├── destroy/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── global-status/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── halt/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── help/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── init/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── list-commands/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── login/ │ │ │ └── plugin.rb │ │ ├── package/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── plugin/ │ │ │ ├── action/ │ │ │ │ ├── expunge_plugins.rb │ │ │ │ ├── install_gem.rb │ │ │ │ ├── license_plugin.rb │ │ │ │ ├── list_plugins.rb │ │ │ │ ├── plugin_exists_check.rb │ │ │ │ ├── repair_plugins.rb │ │ │ │ ├── uninstall_plugin.rb │ │ │ │ └── update_gems.rb │ │ │ ├── action.rb │ │ │ ├── command/ │ │ │ │ ├── base.rb │ │ │ │ ├── expunge.rb │ │ │ │ ├── install.rb │ │ │ │ ├── license.rb │ │ │ │ ├── list.rb │ │ │ │ ├── mixin_install_opts.rb │ │ │ │ ├── repair.rb │ │ │ │ ├── root.rb │ │ │ │ ├── uninstall.rb │ │ │ │ └── update.rb │ │ │ ├── gem_helper.rb │ │ │ └── plugin.rb │ │ ├── port/ │ │ │ ├── command.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ └── plugin.rb │ │ ├── powershell/ │ │ │ ├── command.rb │ │ │ ├── errors.rb │ │ │ ├── plugin.rb │ │ │ └── scripts/ │ │ │ ├── enable_psremoting.ps1 │ │ │ └── reset_trustedhosts.ps1 │ │ ├── provider/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── provision/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── push/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── rdp/ │ │ │ ├── command.rb │ │ │ ├── config.rb │ │ │ ├── errors.rb │ │ │ └── plugin.rb │ │ ├── reload/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── resume/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── snapshot/ │ │ │ ├── command/ │ │ │ │ ├── delete.rb │ │ │ │ ├── list.rb │ │ │ │ ├── pop.rb │ │ │ │ ├── push.rb │ │ │ │ ├── push_shared.rb │ │ │ │ ├── restore.rb │ │ │ │ ├── root.rb │ │ │ │ └── save.rb │ │ │ └── plugin.rb │ │ ├── ssh/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── ssh_config/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── status/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── suspend/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── up/ │ │ │ ├── command.rb │ │ │ ├── middleware/ │ │ │ │ └── store_box_metadata.rb │ │ │ ├── plugin.rb │ │ │ └── start_mixins.rb │ │ ├── upload/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── validate/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── version/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ ├── winrm/ │ │ │ ├── command.rb │ │ │ └── plugin.rb │ │ └── winrm_config/ │ │ ├── command.rb │ │ └── plugin.rb │ ├── communicators/ │ │ ├── none/ │ │ │ ├── communicator.rb │ │ │ └── plugin.rb │ │ ├── ssh/ │ │ │ ├── communicator.rb │ │ │ └── plugin.rb │ │ ├── winrm/ │ │ │ ├── command_filter.rb │ │ │ ├── command_filters/ │ │ │ │ ├── cat.rb │ │ │ │ ├── chmod.rb │ │ │ │ ├── chown.rb │ │ │ │ ├── grep.rb │ │ │ │ ├── mkdir.rb │ │ │ │ ├── rm.rb │ │ │ │ ├── test.rb │ │ │ │ ├── uname.rb │ │ │ │ └── which.rb │ │ │ ├── communicator.rb │ │ │ ├── config.rb │ │ │ ├── errors.rb │ │ │ ├── helper.rb │ │ │ ├── plugin.rb │ │ │ └── shell.rb │ │ └── winssh/ │ │ ├── communicator.rb │ │ ├── config.rb │ │ └── plugin.rb │ ├── guests/ │ │ ├── alma/ │ │ │ ├── cap/ │ │ │ │ └── flavor.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── alpine/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── nfs_client.rb │ │ │ │ ├── rsync.rb │ │ │ │ └── smb.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── alt/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── flavor.rb │ │ │ │ ├── network_scripts_dir.rb │ │ │ │ └── rsync.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── amazon/ │ │ │ ├── cap/ │ │ │ │ ├── configure_networks.rb │ │ │ │ └── flavor.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── arch/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── nfs.rb │ │ │ │ ├── rsync.rb │ │ │ │ └── smb.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── atomic/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ └── docker.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── bsd/ │ │ │ ├── cap/ │ │ │ │ ├── file_system.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── mount_virtualbox_shared_folder.rb │ │ │ │ ├── nfs.rb │ │ │ │ └── public_key.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── centos/ │ │ │ ├── cap/ │ │ │ │ └── flavor.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── coreos/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ └── docker.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── darwin/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── choose_addressable_ip_addr.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── darwin_version.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── mount_smb_shared_folder.rb │ │ │ │ ├── mount_vmware_shared_folder.rb │ │ │ │ ├── rsync.rb │ │ │ │ ├── shell_expand_guest_path.rb │ │ │ │ └── verify_vmware_hgfs.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── debian/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── nfs.rb │ │ │ │ ├── rsync.rb │ │ │ │ └── smb.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── dragonflybsd/ │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── elementary/ │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── esxi/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── mount_nfs_folder.rb │ │ │ │ └── public_key.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── fedora/ │ │ │ ├── cap/ │ │ │ │ └── flavor.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── freebsd/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── mount_virtualbox_shared_folder.rb │ │ │ │ ├── rsync.rb │ │ │ │ └── shell_expand_guest_path.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── funtoo/ │ │ │ ├── cap/ │ │ │ │ └── configure_networks.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── gentoo/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ └── configure_networks.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── haiku/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── insert_public_key.rb │ │ │ │ ├── remove_public_key.rb │ │ │ │ └── rsync.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── kali/ │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── linux/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── choose_addressable_ip_addr.rb │ │ │ │ ├── file_system.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── mount_smb_shared_folder.rb │ │ │ │ ├── mount_virtualbox_shared_folder.rb │ │ │ │ ├── network_interfaces.rb │ │ │ │ ├── nfs.rb │ │ │ │ ├── persist_mount_shared_folder.rb │ │ │ │ ├── port.rb │ │ │ │ ├── public_key.rb │ │ │ │ ├── read_ip_address.rb │ │ │ │ ├── reboot.rb │ │ │ │ ├── rsync.rb │ │ │ │ └── shell_expand_guest_path.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── mint/ │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── netbsd/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── rsync.rb │ │ │ │ └── shell_expand_guest_path.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── nixos/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ └── nfs_client.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── omnios/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── mount_nfs_folder.rb │ │ │ │ └── rsync.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── openbsd/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── rsync.rb │ │ │ │ └── shell_expand_guest_path.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── openwrt/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── insert_public_key.rb │ │ │ │ ├── remove_public_key.rb │ │ │ │ └── rsync.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── photon/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ └── docker.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── pld/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── flavor.rb │ │ │ │ └── network_scripts_dir.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── redhat/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── flavor.rb │ │ │ │ ├── network_scripts_dir.rb │ │ │ │ ├── nfs_client.rb │ │ │ │ ├── rsync.rb │ │ │ │ └── smb.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── rocky/ │ │ │ ├── cap/ │ │ │ │ └── flavor.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── slackware/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ └── configure_networks.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── smartos/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── insert_public_key.rb │ │ │ │ ├── mount_nfs.rb │ │ │ │ ├── remove_public_key.rb │ │ │ │ └── rsync.rb │ │ │ ├── config.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── solaris/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── file_system.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── insert_public_key.rb │ │ │ │ ├── mount_virtualbox_shared_folder.rb │ │ │ │ ├── remove_public_key.rb │ │ │ │ └── rsync.rb │ │ │ ├── config.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── solaris11/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ └── configure_networks.rb │ │ │ ├── config.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── suse/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── network_scripts_dir.rb │ │ │ │ ├── nfs_client.rb │ │ │ │ └── rsync.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── tinycore/ │ │ │ ├── cap/ │ │ │ │ ├── change_host_name.rb │ │ │ │ ├── configure_networks.rb │ │ │ │ ├── halt.rb │ │ │ │ ├── mount_nfs.rb │ │ │ │ └── rsync.rb │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── trisquel/ │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ ├── ubuntu/ │ │ │ ├── guest.rb │ │ │ └── plugin.rb │ │ └── windows/ │ │ ├── cap/ │ │ │ ├── change_host_name.rb │ │ │ ├── choose_addressable_ip_addr.rb │ │ │ ├── configure_networks.rb │ │ │ ├── file_system.rb │ │ │ ├── halt.rb │ │ │ ├── mount_shared_folder.rb │ │ │ ├── public_key.rb │ │ │ ├── reboot.rb │ │ │ └── rsync.rb │ │ ├── config.rb │ │ ├── errors.rb │ │ ├── guest.rb │ │ ├── guest_network.rb │ │ ├── plugin.rb │ │ └── scripts/ │ │ ├── mount_volume.ps1.erb │ │ ├── reboot_detect.ps1 │ │ ├── set_work_network.ps1 │ │ └── winrs_v3_get_adapters.ps1 │ ├── hosts/ │ │ ├── alt/ │ │ │ ├── cap/ │ │ │ │ └── nfs.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── arch/ │ │ │ ├── cap/ │ │ │ │ └── nfs.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── bsd/ │ │ │ ├── cap/ │ │ │ │ ├── nfs.rb │ │ │ │ ├── path.rb │ │ │ │ └── ssh.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── darwin/ │ │ │ ├── cap/ │ │ │ │ ├── configured_ip_addresses.rb │ │ │ │ ├── fs_iso.rb │ │ │ │ ├── nfs.rb │ │ │ │ ├── path.rb │ │ │ │ ├── provider_install_virtualbox.rb │ │ │ │ ├── rdp.rb │ │ │ │ ├── smb.rb │ │ │ │ └── version.rb │ │ │ ├── host.rb │ │ │ ├── plugin.rb │ │ │ └── scripts/ │ │ │ └── install_virtualbox.sh │ │ ├── freebsd/ │ │ │ ├── cap/ │ │ │ │ └── nfs.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── gentoo/ │ │ │ ├── cap/ │ │ │ │ └── nfs.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── linux/ │ │ │ ├── cap/ │ │ │ │ ├── fs_iso.rb │ │ │ │ ├── nfs.rb │ │ │ │ ├── rdp.rb │ │ │ │ └── ssh.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── null/ │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── redhat/ │ │ │ ├── cap/ │ │ │ │ └── nfs.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── slackware/ │ │ │ ├── cap/ │ │ │ │ └── nfs.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── suse/ │ │ │ ├── cap/ │ │ │ │ └── nfs.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ ├── void/ │ │ │ ├── cap/ │ │ │ │ └── nfs.rb │ │ │ ├── host.rb │ │ │ └── plugin.rb │ │ └── windows/ │ │ ├── cap/ │ │ │ ├── configured_ip_addresses.rb │ │ │ ├── fs_iso.rb │ │ │ ├── nfs.rb │ │ │ ├── provider_install_virtualbox.rb │ │ │ ├── ps.rb │ │ │ ├── rdp.rb │ │ │ ├── smb.rb │ │ │ └── ssh.rb │ │ ├── host.rb │ │ ├── plugin.rb │ │ └── scripts/ │ │ ├── check_credentials.ps1 │ │ ├── host_info.ps1 │ │ ├── install_virtualbox.ps1 │ │ ├── set_share.ps1 │ │ ├── set_ssh_key_permissions.ps1 │ │ ├── unset_share.ps1 │ │ └── utils/ │ │ └── VagrantSSH/ │ │ └── VagrantSSH.psm1 │ ├── kernel_v1/ │ │ ├── config/ │ │ │ ├── nfs.rb │ │ │ ├── package.rb │ │ │ ├── ssh.rb │ │ │ ├── vagrant.rb │ │ │ └── vm.rb │ │ └── plugin.rb │ ├── kernel_v2/ │ │ ├── config/ │ │ │ ├── cloud_init.rb │ │ │ ├── disk.rb │ │ │ ├── package.rb │ │ │ ├── push.rb │ │ │ ├── ssh.rb │ │ │ ├── ssh_connect.rb │ │ │ ├── trigger.rb │ │ │ ├── vagrant.rb │ │ │ ├── vm.rb │ │ │ ├── vm_provisioner.rb │ │ │ ├── vm_subvm.rb │ │ │ └── vm_trigger.rb │ │ └── plugin.rb │ ├── providers/ │ │ ├── docker/ │ │ │ ├── action/ │ │ │ │ ├── build.rb │ │ │ │ ├── compare_synced_folders.rb │ │ │ │ ├── connect_networks.rb │ │ │ │ ├── create.rb │ │ │ │ ├── destroy.rb │ │ │ │ ├── destroy_build_image.rb │ │ │ │ ├── destroy_network.rb │ │ │ │ ├── forwarded_ports.rb │ │ │ │ ├── has_ssh.rb │ │ │ │ ├── host_machine.rb │ │ │ │ ├── host_machine_build_dir.rb │ │ │ │ ├── host_machine_port_checker.rb │ │ │ │ ├── host_machine_port_warning.rb │ │ │ │ ├── host_machine_required.rb │ │ │ │ ├── host_machine_sync_folders.rb │ │ │ │ ├── host_machine_sync_folders_disable.rb │ │ │ │ ├── init_state.rb │ │ │ │ ├── is_build.rb │ │ │ │ ├── is_host_machine_created.rb │ │ │ │ ├── login.rb │ │ │ │ ├── prepare_forwarded_port_collision_params.rb │ │ │ │ ├── prepare_networks.rb │ │ │ │ ├── prepare_nfs_settings.rb │ │ │ │ ├── prepare_nfs_valid_ids.rb │ │ │ │ ├── prepare_ssh.rb │ │ │ │ ├── pull.rb │ │ │ │ ├── start.rb │ │ │ │ ├── stop.rb │ │ │ │ └── wait_for_running.rb │ │ │ ├── action.rb │ │ │ ├── cap/ │ │ │ │ ├── has_communicator.rb │ │ │ │ ├── proxy_machine.rb │ │ │ │ └── public_address.rb │ │ │ ├── command/ │ │ │ │ ├── exec.rb │ │ │ │ ├── logs.rb │ │ │ │ └── run.rb │ │ │ ├── communicator.rb │ │ │ ├── config.rb │ │ │ ├── driver/ │ │ │ │ └── compose.rb │ │ │ ├── driver.rb │ │ │ ├── errors.rb │ │ │ ├── executor/ │ │ │ │ ├── local.rb │ │ │ │ └── vagrant.rb │ │ │ ├── hostmachine/ │ │ │ │ └── Vagrantfile │ │ │ ├── plugin.rb │ │ │ ├── provider.rb │ │ │ └── synced_folder.rb │ │ ├── hyperv/ │ │ │ ├── action/ │ │ │ │ ├── check_access.rb │ │ │ │ ├── check_enabled.rb │ │ │ │ ├── configure.rb │ │ │ │ ├── delete_vm.rb │ │ │ │ ├── export.rb │ │ │ │ ├── import.rb │ │ │ │ ├── is_windows.rb │ │ │ │ ├── message_will_not_destroy.rb │ │ │ │ ├── net_set_mac.rb │ │ │ │ ├── net_set_vlan.rb │ │ │ │ ├── package.rb │ │ │ │ ├── package_metadata_json.rb │ │ │ │ ├── package_setup_files.rb │ │ │ │ ├── package_setup_folders.rb │ │ │ │ ├── package_vagrantfile.rb │ │ │ │ ├── read_guest_ip.rb │ │ │ │ ├── read_state.rb │ │ │ │ ├── resume_vm.rb │ │ │ │ ├── set_name.rb │ │ │ │ ├── snapshot_delete.rb │ │ │ │ ├── snapshot_restore.rb │ │ │ │ ├── snapshot_save.rb │ │ │ │ ├── start_instance.rb │ │ │ │ ├── stop_instance.rb │ │ │ │ ├── suspend_vm.rb │ │ │ │ └── wait_for_ip_address.rb │ │ │ ├── action.rb │ │ │ ├── cap/ │ │ │ │ ├── cleanup_disks.rb │ │ │ │ ├── configure_disks.rb │ │ │ │ ├── public_address.rb │ │ │ │ ├── snapshot_list.rb │ │ │ │ └── validate_disk_ext.rb │ │ │ ├── config.rb │ │ │ ├── driver.rb │ │ │ ├── errors.rb │ │ │ ├── plugin.rb │ │ │ ├── provider.rb │ │ │ └── scripts/ │ │ │ ├── add_dvd.ps1 │ │ │ ├── attach_disk_drive.ps1 │ │ │ ├── check_hyperv.ps1 │ │ │ ├── check_hyperv_access.ps1 │ │ │ ├── clone_vhd.ps1 │ │ │ ├── configure_vm.ps1 │ │ │ ├── create_snapshot.ps1 │ │ │ ├── delete_snapshot.ps1 │ │ │ ├── delete_vm.ps1 │ │ │ ├── dismount_vhd.ps1 │ │ │ ├── export_vm.ps1 │ │ │ ├── file_sync.ps1 │ │ │ ├── get_network_config.ps1 │ │ │ ├── get_network_mac.ps1 │ │ │ ├── get_scsi_controller.ps1 │ │ │ ├── get_switches.ps1 │ │ │ ├── get_vhd.ps1 │ │ │ ├── get_vm_status.ps1 │ │ │ ├── has_vmcx_support.ps1 │ │ │ ├── import_vm.ps1 │ │ │ ├── list_hdds.ps1 │ │ │ ├── list_snapshots.ps1 │ │ │ ├── new_vhd.ps1 │ │ │ ├── remove_disk_drive.ps1 │ │ │ ├── remove_dvd.ps1 │ │ │ ├── resize_disk_drive.ps1 │ │ │ ├── restore_snapshot.ps1 │ │ │ ├── resume_vm.ps1 │ │ │ ├── set_enhanced_session_transport_type.ps1 │ │ │ ├── set_name.ps1 │ │ │ ├── set_network_mac.ps1 │ │ │ ├── set_network_vlan.ps1 │ │ │ ├── set_vm_integration_services.ps1 │ │ │ ├── start_vm.ps1 │ │ │ ├── stop_vm.ps1 │ │ │ ├── suspend_vm.ps1 │ │ │ └── utils/ │ │ │ ├── VagrantMessages/ │ │ │ │ └── VagrantMessages.psm1 │ │ │ └── VagrantVM/ │ │ │ └── VagrantVM.psm1 │ │ └── virtualbox/ │ │ ├── action/ │ │ │ ├── boot.rb │ │ │ ├── check_accessible.rb │ │ │ ├── check_created.rb │ │ │ ├── check_guest_additions.rb │ │ │ ├── check_running.rb │ │ │ ├── check_virtualbox.rb │ │ │ ├── clean_machine_folder.rb │ │ │ ├── clear_forwarded_ports.rb │ │ │ ├── clear_network_interfaces.rb │ │ │ ├── created.rb │ │ │ ├── customize.rb │ │ │ ├── destroy.rb │ │ │ ├── destroy_unused_network_interfaces.rb │ │ │ ├── discard_state.rb │ │ │ ├── export.rb │ │ │ ├── forced_halt.rb │ │ │ ├── forward_ports.rb │ │ │ ├── import.rb │ │ │ ├── import_master.rb │ │ │ ├── is_paused.rb │ │ │ ├── is_running.rb │ │ │ ├── is_saved.rb │ │ │ ├── match_mac_address.rb │ │ │ ├── message_already_running.rb │ │ │ ├── message_not_created.rb │ │ │ ├── message_not_running.rb │ │ │ ├── message_will_not_destroy.rb │ │ │ ├── network.rb │ │ │ ├── network_fix_ipv6.rb │ │ │ ├── package.rb │ │ │ ├── package_setup_files.rb │ │ │ ├── package_setup_folders.rb │ │ │ ├── package_vagrantfile.rb │ │ │ ├── prepare_clone_snapshot.rb │ │ │ ├── prepare_forwarded_port_collision_params.rb │ │ │ ├── prepare_nfs_settings.rb │ │ │ ├── prepare_nfs_valid_ids.rb │ │ │ ├── resume.rb │ │ │ ├── sane_defaults.rb │ │ │ ├── set_default_nic_type.rb │ │ │ ├── set_name.rb │ │ │ ├── setup_package_files.rb │ │ │ ├── snapshot_delete.rb │ │ │ ├── snapshot_restore.rb │ │ │ ├── snapshot_save.rb │ │ │ └── suspend.rb │ │ ├── action.rb │ │ ├── cap/ │ │ │ ├── cleanup_disks.rb │ │ │ ├── configure_disks.rb │ │ │ ├── mount_options.rb │ │ │ ├── public_address.rb │ │ │ └── validate_disk_ext.rb │ │ ├── cap.rb │ │ ├── config.rb │ │ ├── driver/ │ │ │ ├── base.rb │ │ │ ├── meta.rb │ │ │ ├── version_4_0.rb │ │ │ ├── version_4_1.rb │ │ │ ├── version_4_2.rb │ │ │ ├── version_4_3.rb │ │ │ ├── version_5_0.rb │ │ │ ├── version_5_1.rb │ │ │ ├── version_5_2.rb │ │ │ ├── version_6_0.rb │ │ │ ├── version_6_1.rb │ │ │ ├── version_7_0.rb │ │ │ ├── version_7_1.rb │ │ │ └── version_7_2.rb │ │ ├── model/ │ │ │ ├── forwarded_port.rb │ │ │ ├── storage_controller.rb │ │ │ └── storage_controller_array.rb │ │ ├── plugin.rb │ │ ├── provider.rb │ │ ├── synced_folder.rb │ │ └── util/ │ │ └── compile_forwarded_ports.rb │ ├── provisioners/ │ │ ├── ansible/ │ │ │ ├── cap/ │ │ │ │ └── guest/ │ │ │ │ ├── alpine/ │ │ │ │ │ └── ansible_install.rb │ │ │ │ ├── arch/ │ │ │ │ │ └── ansible_install.rb │ │ │ │ ├── debian/ │ │ │ │ │ └── ansible_install.rb │ │ │ │ ├── facts.rb │ │ │ │ ├── fedora/ │ │ │ │ │ └── ansible_install.rb │ │ │ │ ├── freebsd/ │ │ │ │ │ └── ansible_install.rb │ │ │ │ ├── pip/ │ │ │ │ │ └── pip.rb │ │ │ │ ├── posix/ │ │ │ │ │ └── ansible_installed.rb │ │ │ │ ├── redhat/ │ │ │ │ │ └── ansible_install.rb │ │ │ │ ├── suse/ │ │ │ │ │ └── ansible_install.rb │ │ │ │ └── ubuntu/ │ │ │ │ └── ansible_install.rb │ │ │ ├── config/ │ │ │ │ ├── base.rb │ │ │ │ ├── guest.rb │ │ │ │ └── host.rb │ │ │ ├── constants.rb │ │ │ ├── errors.rb │ │ │ ├── helpers.rb │ │ │ ├── plugin.rb │ │ │ └── provisioner/ │ │ │ ├── base.rb │ │ │ ├── guest.rb │ │ │ └── host.rb │ │ ├── cfengine/ │ │ │ ├── cap/ │ │ │ │ ├── debian/ │ │ │ │ │ └── cfengine_install.rb │ │ │ │ ├── linux/ │ │ │ │ │ ├── cfengine_installed.rb │ │ │ │ │ └── cfengine_needs_bootstrap.rb │ │ │ │ ├── redhat/ │ │ │ │ │ └── cfengine_install.rb │ │ │ │ └── suse/ │ │ │ │ └── cfengine_install.rb │ │ │ ├── config.rb │ │ │ ├── plugin.rb │ │ │ └── provisioner.rb │ │ ├── chef/ │ │ │ ├── cap/ │ │ │ │ ├── debian/ │ │ │ │ │ └── chef_install.rb │ │ │ │ ├── freebsd/ │ │ │ │ │ ├── chef_install.rb │ │ │ │ │ └── chef_installed.rb │ │ │ │ ├── linux/ │ │ │ │ │ └── chef_installed.rb │ │ │ │ ├── omnios/ │ │ │ │ │ ├── chef_install.rb │ │ │ │ │ └── chef_installed.rb │ │ │ │ ├── redhat/ │ │ │ │ │ └── chef_install.rb │ │ │ │ ├── suse/ │ │ │ │ │ └── chef_install.rb │ │ │ │ └── windows/ │ │ │ │ ├── chef_install.rb │ │ │ │ └── chef_installed.rb │ │ │ ├── command_builder.rb │ │ │ ├── config/ │ │ │ │ ├── base.rb │ │ │ │ ├── base_runner.rb │ │ │ │ ├── chef_apply.rb │ │ │ │ ├── chef_client.rb │ │ │ │ ├── chef_solo.rb │ │ │ │ └── chef_zero.rb │ │ │ ├── installer.rb │ │ │ ├── omnibus.rb │ │ │ ├── plugin.rb │ │ │ └── provisioner/ │ │ │ ├── base.rb │ │ │ ├── chef_apply.rb │ │ │ ├── chef_client.rb │ │ │ ├── chef_solo.rb │ │ │ └── chef_zero.rb │ │ ├── container/ │ │ │ ├── client.rb │ │ │ ├── config.rb │ │ │ ├── installer.rb │ │ │ ├── plugin.rb │ │ │ └── provisioner.rb │ │ ├── docker/ │ │ │ ├── cap/ │ │ │ │ ├── centos/ │ │ │ │ │ ├── docker_install.rb │ │ │ │ │ └── docker_start_service.rb │ │ │ │ ├── debian/ │ │ │ │ │ ├── docker_install.rb │ │ │ │ │ └── docker_start_service.rb │ │ │ │ ├── fedora/ │ │ │ │ │ └── docker_install.rb │ │ │ │ ├── linux/ │ │ │ │ │ ├── docker_configure_vagrant_user.rb │ │ │ │ │ ├── docker_daemon_running.rb │ │ │ │ │ └── docker_installed.rb │ │ │ │ └── windows/ │ │ │ │ └── docker_daemon_running.rb │ │ │ ├── client.rb │ │ │ ├── config.rb │ │ │ ├── installer.rb │ │ │ ├── plugin.rb │ │ │ └── provisioner.rb │ │ ├── file/ │ │ │ ├── config.rb │ │ │ ├── plugin.rb │ │ │ └── provisioner.rb │ │ ├── podman/ │ │ │ ├── cap/ │ │ │ │ ├── centos/ │ │ │ │ │ └── podman_install.rb │ │ │ │ ├── linux/ │ │ │ │ │ └── podman_installed.rb │ │ │ │ └── redhat/ │ │ │ │ └── podman_install.rb │ │ │ ├── client.rb │ │ │ ├── config.rb │ │ │ ├── installer.rb │ │ │ ├── plugin.rb │ │ │ └── provisioner.rb │ │ ├── puppet/ │ │ │ ├── config/ │ │ │ │ ├── puppet.rb │ │ │ │ └── puppet_server.rb │ │ │ ├── plugin.rb │ │ │ └── provisioner/ │ │ │ ├── puppet.rb │ │ │ └── puppet_server.rb │ │ ├── salt/ │ │ │ ├── bootstrap_downloader.rb │ │ │ ├── config.rb │ │ │ ├── errors.rb │ │ │ ├── plugin.rb │ │ │ └── provisioner.rb │ │ └── shell/ │ │ ├── config.rb │ │ ├── plugin.rb │ │ └── provisioner.rb │ ├── pushes/ │ │ ├── atlas/ │ │ │ ├── config.rb │ │ │ ├── errors.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── plugin.rb │ │ │ └── push.rb │ │ ├── ftp/ │ │ │ ├── adapter.rb │ │ │ ├── config.rb │ │ │ ├── errors.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── plugin.rb │ │ │ └── push.rb │ │ ├── heroku/ │ │ │ ├── config.rb │ │ │ ├── errors.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── plugin.rb │ │ │ └── push.rb │ │ ├── local-exec/ │ │ │ ├── config.rb │ │ │ ├── errors.rb │ │ │ ├── locales/ │ │ │ │ └── en.yml │ │ │ ├── plugin.rb │ │ │ └── push.rb │ │ └── noop/ │ │ ├── config.rb │ │ ├── plugin.rb │ │ └── push.rb │ └── synced_folders/ │ ├── nfs/ │ │ ├── action_cleanup.rb │ │ ├── config.rb │ │ ├── plugin.rb │ │ └── synced_folder.rb │ ├── rsync/ │ │ ├── command/ │ │ │ ├── rsync.rb │ │ │ └── rsync_auto.rb │ │ ├── default_unix_cap.rb │ │ ├── helper.rb │ │ ├── plugin.rb │ │ └── synced_folder.rb │ ├── smb/ │ │ ├── cap/ │ │ │ ├── default_fstab_modification.rb │ │ │ └── mount_options.rb │ │ ├── config.rb │ │ ├── errors.rb │ │ ├── plugin.rb │ │ └── synced_folder.rb │ └── unix_mount_helpers.rb ├── scripts/ │ ├── install_rvm │ ├── setup_tests │ ├── sign.sh │ └── website_push_www.sh ├── tasks/ │ ├── acceptance.rake │ ├── bundler.rake │ └── test.rake ├── templates/ │ ├── commands/ │ │ ├── init/ │ │ │ ├── Vagrantfile.erb │ │ │ └── Vagrantfile.min.erb │ │ ├── ssh_config/ │ │ │ └── config.erb │ │ └── winrm_config/ │ │ └── config.erb │ ├── config/ │ │ ├── messages.erb │ │ └── validation_failed.erb │ ├── guests/ │ │ ├── alpine/ │ │ │ ├── network_dhcp.erb │ │ │ └── network_static.erb │ │ ├── alt/ │ │ │ ├── network_dhcp.erb │ │ │ ├── network_ipv4address.erb │ │ │ ├── network_ipv4route.erb │ │ │ └── network_static.erb │ │ ├── arch/ │ │ │ ├── default_network/ │ │ │ │ ├── network_dhcp.erb │ │ │ │ ├── network_static.erb │ │ │ │ └── network_static6.erb │ │ │ └── systemd_networkd/ │ │ │ ├── network_dhcp.erb │ │ │ ├── network_static.erb │ │ │ └── network_static6.erb │ │ ├── coreos/ │ │ │ └── etcd.service.erb │ │ ├── debian/ │ │ │ ├── network_dhcp.erb │ │ │ ├── network_static.erb │ │ │ └── network_static6.erb │ │ ├── freebsd/ │ │ │ ├── network_dhcp.erb │ │ │ ├── network_static.erb │ │ │ └── network_static6.erb │ │ ├── funtoo/ │ │ │ ├── network_dhcp.erb │ │ │ ├── network_static.erb │ │ │ └── network_static6.erb │ │ ├── gentoo/ │ │ │ ├── network_dhcp.erb │ │ │ ├── network_static.erb │ │ │ ├── network_static6.erb │ │ │ └── network_systemd.erb │ │ ├── linux/ │ │ │ └── etc_fstab.erb │ │ ├── netbsd/ │ │ │ ├── network_dhcp.erb │ │ │ └── network_static.erb │ │ ├── nixos/ │ │ │ ├── hostname.erb │ │ │ └── network.erb │ │ ├── openbsd/ │ │ │ ├── network_dhcp.erb │ │ │ ├── network_static.erb │ │ │ └── network_static6.erb │ │ ├── redhat/ │ │ │ ├── network_dhcp.erb │ │ │ ├── network_static.erb │ │ │ └── network_static6.erb │ │ ├── slackware/ │ │ │ ├── network_dhcp.erb │ │ │ └── network_static.erb │ │ └── suse/ │ │ ├── network_dhcp.erb │ │ ├── network_static.erb │ │ └── network_static6.erb │ ├── license/ │ │ ├── license.html.tmpl │ │ ├── license.rtf.tmpl │ │ └── license.tmpl │ ├── locales/ │ │ ├── comm_winrm.yml │ │ ├── command_ps.yml │ │ ├── command_rdp.yml │ │ ├── en.yml │ │ ├── guest_windows.yml │ │ ├── providers_docker.yml │ │ ├── providers_hyperv.yml │ │ └── synced_folder_smb.yml │ ├── networking/ │ │ └── network_manager/ │ │ └── network_manager_device.erb │ ├── nfs/ │ │ ├── exports_bsd.erb │ │ ├── exports_darwin.erb │ │ └── exports_linux.erb │ ├── package_Vagrantfile.erb │ ├── provisioners/ │ │ ├── chef_client/ │ │ │ └── client.erb │ │ ├── chef_solo/ │ │ │ └── solo.erb │ │ └── chef_zero/ │ │ └── zero.erb │ └── rgloader.rb ├── test/ │ ├── acceptance/ │ │ ├── base.rb │ │ ├── provider-docker/ │ │ │ └── lifecycle_spec.rb │ │ ├── provider-virtualbox/ │ │ │ ├── linked_clone_spec.rb │ │ │ └── network_intnet_spec.rb │ │ ├── shared/ │ │ │ └── context_virtualbox.rb │ │ └── skeletons/ │ │ ├── basic_docker/ │ │ │ └── Vagrantfile │ │ ├── linked_clone/ │ │ │ └── Vagrantfile │ │ └── network_intnet/ │ │ └── Vagrantfile │ ├── config/ │ │ └── acceptance_boxes.yml │ ├── support/ │ │ └── isolated_environment.rb │ ├── unit/ │ │ ├── base.rb │ │ ├── bin/ │ │ │ └── vagrant_test.rb │ │ ├── plugins/ │ │ │ ├── commands/ │ │ │ │ ├── autocomplete/ │ │ │ │ │ └── commands/ │ │ │ │ │ └── install_test.rb │ │ │ │ ├── box/ │ │ │ │ │ └── command/ │ │ │ │ │ ├── add_test.rb │ │ │ │ │ ├── outdated_test.rb │ │ │ │ │ ├── prune_test.rb │ │ │ │ │ ├── remove_test.rb │ │ │ │ │ ├── repackage_test.rb │ │ │ │ │ └── update_test.rb │ │ │ │ ├── cap/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── cloud/ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── login_test.rb │ │ │ │ │ │ ├── logout_test.rb │ │ │ │ │ │ ├── middleware/ │ │ │ │ │ │ │ ├── add_authentication_test.rb │ │ │ │ │ │ │ └── add_downloader_authentication_test.rb │ │ │ │ │ │ └── whoami_test.rb │ │ │ │ │ ├── box/ │ │ │ │ │ │ ├── create_test.rb │ │ │ │ │ │ ├── delete_test.rb │ │ │ │ │ │ ├── show_test.rb │ │ │ │ │ │ └── update_test.rb │ │ │ │ │ ├── client_test.rb │ │ │ │ │ ├── list_test.rb │ │ │ │ │ ├── provider/ │ │ │ │ │ │ ├── create_test.rb │ │ │ │ │ │ ├── delete_test.rb │ │ │ │ │ │ ├── update_test.rb │ │ │ │ │ │ └── upload_test.rb │ │ │ │ │ ├── publish_test.rb │ │ │ │ │ ├── search_test.rb │ │ │ │ │ └── version/ │ │ │ │ │ ├── create_test.rb │ │ │ │ │ ├── delete_test.rb │ │ │ │ │ ├── release_test.rb │ │ │ │ │ ├── revoke_test.rb │ │ │ │ │ └── update_test.rb │ │ │ │ ├── destroy/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── global-status/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── init/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── list-commands/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── package/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── plugin/ │ │ │ │ │ └── action/ │ │ │ │ │ ├── expunge_plugins_test.rb │ │ │ │ │ ├── install_gem_test.rb │ │ │ │ │ ├── plugin_exists_check_test.rb │ │ │ │ │ ├── uninstall_plugin_test.rb │ │ │ │ │ └── update_gems_test.rb │ │ │ │ ├── port/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── powershell/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── provider/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── push/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── reload/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── snapshot/ │ │ │ │ │ └── command/ │ │ │ │ │ ├── delete_test.rb │ │ │ │ │ ├── list_test.rb │ │ │ │ │ ├── pop_test.rb │ │ │ │ │ ├── push_test.rb │ │ │ │ │ ├── restore_test.rb │ │ │ │ │ ├── root_test.rb │ │ │ │ │ └── save_test.rb │ │ │ │ ├── ssh_config/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── suspend/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── up/ │ │ │ │ │ ├── command_test.rb │ │ │ │ │ └── middleware/ │ │ │ │ │ └── store_box_metadata_test.rb │ │ │ │ ├── upload/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── validate/ │ │ │ │ │ └── command_test.rb │ │ │ │ ├── winrm/ │ │ │ │ │ └── command_test.rb │ │ │ │ └── winrm_config/ │ │ │ │ └── command_test.rb │ │ │ ├── communicators/ │ │ │ │ ├── none/ │ │ │ │ │ └── communicator_test.rb │ │ │ │ ├── ssh/ │ │ │ │ │ └── communicator_test.rb │ │ │ │ ├── winrm/ │ │ │ │ │ ├── command_filter_test.rb │ │ │ │ │ ├── communicator_test.rb │ │ │ │ │ ├── config_test.rb │ │ │ │ │ ├── helper_test.rb │ │ │ │ │ ├── plugin_test.rb │ │ │ │ │ └── shell_test.rb │ │ │ │ └── winssh/ │ │ │ │ └── communicator_test.rb │ │ │ ├── guests/ │ │ │ │ ├── alma/ │ │ │ │ │ └── cap/ │ │ │ │ │ └── flavor_test.rb │ │ │ │ ├── alpine/ │ │ │ │ │ ├── cap/ │ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ │ ├── nfs_client_test.rb │ │ │ │ │ │ └── rsync_test.rb │ │ │ │ │ └── plugin_test.rb │ │ │ │ ├── alt/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ ├── flavor_test.rb │ │ │ │ │ ├── network_scripts_dir_test.rb │ │ │ │ │ └── rsync_test.rb │ │ │ │ ├── amazon/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ └── flavor_test.rb │ │ │ │ ├── arch/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ ├── rsync_test.rb │ │ │ │ │ └── smb_test.rb │ │ │ │ ├── atomic/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ └── docker_test.rb │ │ │ │ ├── bsd/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── file_system_test.rb │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ ├── insert_public_key_test.rb │ │ │ │ │ ├── mount_virtual_box_shared_folder_test.rb │ │ │ │ │ ├── nfs_test.rb │ │ │ │ │ └── remove_public_key_test.rb │ │ │ │ ├── centos/ │ │ │ │ │ └── cap/ │ │ │ │ │ └── flavor_test.rb │ │ │ │ ├── coreos/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ └── docker_test.rb │ │ │ │ ├── darwin/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── choose_addressable_ip_addr_test.rb │ │ │ │ │ ├── darwin_version_test.rb │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ ├── mount_vmware_shared_folder_test.rb │ │ │ │ │ └── shell_expand_guest_path_test.rb │ │ │ │ ├── debian/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ ├── nfs_client_test.rb │ │ │ │ │ ├── rsync_test.rb │ │ │ │ │ └── smb_test.rb │ │ │ │ ├── esxi/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ └── public_key_test.rb │ │ │ │ ├── freebsd/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ ├── mount_virtual_box_shared_folder_test.rb │ │ │ │ │ ├── rsync_test.rb │ │ │ │ │ └── shell_expand_guest_path_test.rb │ │ │ │ ├── gentoo/ │ │ │ │ │ └── cap/ │ │ │ │ │ └── change_host_name_test.rb │ │ │ │ ├── haiku/ │ │ │ │ │ └── cap/ │ │ │ │ │ └── rsync_test.rb │ │ │ │ ├── linux/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── choose_addressable_ip_addr_test.rb │ │ │ │ │ ├── file_system_test.rb │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ ├── insert_public_key_test.rb │ │ │ │ │ ├── mount_nfs_test.rb │ │ │ │ │ ├── mount_shared_folder_test.rb │ │ │ │ │ ├── mount_smb_shared_folder_test.rb │ │ │ │ │ ├── mount_virtual_box_shared_folder_test.rb │ │ │ │ │ ├── network_interfaces_test.rb │ │ │ │ │ ├── nfs_client_test.rb │ │ │ │ │ ├── persist_mount_shared_folder_test.rb │ │ │ │ │ ├── port_test.rb │ │ │ │ │ ├── reboot_test.rb │ │ │ │ │ ├── remove_public_key_test.rb │ │ │ │ │ ├── rsync_test.rb │ │ │ │ │ └── shell_expand_guest_path_test.rb │ │ │ │ ├── netbsd/ │ │ │ │ │ └── cap/ │ │ │ │ │ └── shell_expand_guest_path_test.rb │ │ │ │ ├── omnios/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── mount_nfs_folder_test.rb │ │ │ │ │ └── rsync_test.rb │ │ │ │ ├── openbsd/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ ├── rsync_test.rb │ │ │ │ │ └── shell_expand_guest_path_test.rb │ │ │ │ ├── openwrt/ │ │ │ │ │ ├── cap/ │ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ │ ├── insert_public_key_test.rb │ │ │ │ │ │ ├── remove_public_key_test.rb │ │ │ │ │ │ └── rsync_test.rb │ │ │ │ │ └── guest_test.rb │ │ │ │ ├── photon/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ └── docker_test.rb │ │ │ │ ├── pld/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── flavor_test.rb │ │ │ │ │ └── network_scripts_dir_test.rb │ │ │ │ ├── redhat/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ ├── flavor_test.rb │ │ │ │ │ ├── network_scripts_dir_test.rb │ │ │ │ │ ├── nfs_client_test.rb │ │ │ │ │ ├── rsync_test.rb │ │ │ │ │ └── smb_test.rb │ │ │ │ ├── rocky/ │ │ │ │ │ └── cap/ │ │ │ │ │ └── flavor_test.rb │ │ │ │ ├── slackware/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ └── configure_networks_test.rb │ │ │ │ ├── smartos/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ ├── insert_public_key_test.rb │ │ │ │ │ ├── mount_nfs_test.rb │ │ │ │ │ ├── remove_public_key_test.rb │ │ │ │ │ └── rsync_test.rb │ │ │ │ ├── solaris/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── file_system_test.rb │ │ │ │ │ └── halt_test.rb │ │ │ │ ├── solaris11/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ └── configure_networks_test.rb │ │ │ │ ├── suse/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── configure_networks_test.rb │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ ├── network_scripts_dir_test.rb │ │ │ │ │ ├── nfs_client_test.rb │ │ │ │ │ └── rsync_test.rb │ │ │ │ ├── tinycore/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ └── halt_test.rb │ │ │ │ └── windows/ │ │ │ │ ├── cap/ │ │ │ │ │ ├── change_host_name_test.rb │ │ │ │ │ ├── file_system_test.rb │ │ │ │ │ ├── halt_test.rb │ │ │ │ │ ├── insert_public_key_test.rb │ │ │ │ │ ├── mount_shared_folder_test.rb │ │ │ │ │ ├── reboot_test.rb │ │ │ │ │ ├── remove_public_key_test.rb │ │ │ │ │ └── rsync_test.rb │ │ │ │ ├── config_test.rb │ │ │ │ └── guest_network_test.rb │ │ │ ├── hosts/ │ │ │ │ ├── bsd/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── nfs_test.rb │ │ │ │ │ ├── path_test.rb │ │ │ │ │ └── ssh_test.rb │ │ │ │ ├── darwin/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── configured_ip_addresses_test.rb │ │ │ │ │ ├── fs_iso_test.rb │ │ │ │ │ ├── nfs_test.rb │ │ │ │ │ ├── path_test.rb │ │ │ │ │ ├── rdp_test.rb │ │ │ │ │ ├── smb_test.rb │ │ │ │ │ └── version_test.rb │ │ │ │ ├── linux/ │ │ │ │ │ └── cap/ │ │ │ │ │ ├── fs_iso_test.rb │ │ │ │ │ ├── nfs_test.rb │ │ │ │ │ └── ssh_test.rb │ │ │ │ ├── void/ │ │ │ │ │ └── cap/ │ │ │ │ │ └── nfs_test.rb │ │ │ │ └── windows/ │ │ │ │ └── cap/ │ │ │ │ ├── configure_ip_addresses_test.rb │ │ │ │ ├── fs_iso_test.rb │ │ │ │ ├── smb_test.rb │ │ │ │ └── ssh_test.rb │ │ │ ├── kernel_v2/ │ │ │ │ └── config/ │ │ │ │ ├── cloud_init_test.rb │ │ │ │ ├── disk_test.rb │ │ │ │ ├── package_test.rb │ │ │ │ ├── push_test.rb │ │ │ │ ├── ssh_connect_test.rb │ │ │ │ ├── ssh_test.rb │ │ │ │ ├── trigger_test.rb │ │ │ │ ├── vagrant_test.rb │ │ │ │ ├── vm_test.rb │ │ │ │ └── vm_trigger_test.rb │ │ │ ├── providers/ │ │ │ │ ├── docker/ │ │ │ │ │ ├── action/ │ │ │ │ │ │ ├── compare_synced_folders_test.rb │ │ │ │ │ │ ├── connect_networks_test.rb │ │ │ │ │ │ ├── create_test.rb │ │ │ │ │ │ ├── destroy_network_test.rb │ │ │ │ │ │ ├── host_machine_sync_folders_test.rb │ │ │ │ │ │ ├── login_test.rb │ │ │ │ │ │ └── prepare_networks_test.rb │ │ │ │ │ ├── command/ │ │ │ │ │ │ └── exec_test.rb │ │ │ │ │ ├── config_test.rb │ │ │ │ │ ├── driver_compose_test.rb │ │ │ │ │ ├── driver_test.rb │ │ │ │ │ ├── provider_test.rb │ │ │ │ │ └── synced_folder_test.rb │ │ │ │ ├── hyperv/ │ │ │ │ │ ├── action/ │ │ │ │ │ │ ├── check_enabled_test.rb │ │ │ │ │ │ ├── configure_test.rb │ │ │ │ │ │ ├── delete_vm_test.rb │ │ │ │ │ │ ├── export_test.rb │ │ │ │ │ │ ├── import_test.rb │ │ │ │ │ │ ├── is_windows_test.rb │ │ │ │ │ │ ├── net_set_mac_test.rb │ │ │ │ │ │ ├── net_set_vlan_test.rb │ │ │ │ │ │ ├── read_guest_ip_test.rb │ │ │ │ │ │ ├── read_state_test.rb │ │ │ │ │ │ ├── set_name_test.rb │ │ │ │ │ │ └── wait_for_ip_address_test.rb │ │ │ │ │ ├── cap/ │ │ │ │ │ │ ├── cleanup_disks_test.rb │ │ │ │ │ │ └── configure_disks_test.rb │ │ │ │ │ ├── config_test.rb │ │ │ │ │ ├── driver_test.rb │ │ │ │ │ └── provider_test.rb │ │ │ │ └── virtualbox/ │ │ │ │ ├── action/ │ │ │ │ │ ├── clean_machine_folder_test.rb │ │ │ │ │ ├── import_test.rb │ │ │ │ │ ├── match_mac_address_test.rb │ │ │ │ │ ├── network_fix_ipv6_test.rb │ │ │ │ │ ├── network_test.rb │ │ │ │ │ ├── prepare_nfs_settings_test.rb │ │ │ │ │ ├── prepare_nfs_valid_ids_test.rb │ │ │ │ │ └── set_default_nic_type_test.rb │ │ │ │ ├── base.rb │ │ │ │ ├── cap/ │ │ │ │ │ ├── cleanup_disks_test.rb │ │ │ │ │ ├── configure_disks_test.rb │ │ │ │ │ ├── mount_options_test.rb │ │ │ │ │ └── public_address_test.rb │ │ │ │ ├── cap_test.rb │ │ │ │ ├── config_test.rb │ │ │ │ ├── driver/ │ │ │ │ │ ├── base.rb │ │ │ │ │ ├── version_4_0_test.rb │ │ │ │ │ ├── version_4_1_test.rb │ │ │ │ │ ├── version_4_2_test.rb │ │ │ │ │ ├── version_4_3_test.rb │ │ │ │ │ ├── version_5_0_test.rb │ │ │ │ │ ├── version_6_0_test.rb │ │ │ │ │ ├── version_6_1_test.rb │ │ │ │ │ ├── version_7_0_test.rb │ │ │ │ │ ├── version_7_1_test.rb │ │ │ │ │ └── version_7_2_test.rb │ │ │ │ ├── model/ │ │ │ │ │ ├── storage_controller_array_test.rb │ │ │ │ │ └── storage_controller_test.rb │ │ │ │ ├── provider_test.rb │ │ │ │ ├── support/ │ │ │ │ │ └── shared/ │ │ │ │ │ ├── virtualbox_driver_version_4_x_examples.rb │ │ │ │ │ ├── virtualbox_driver_version_5_x_examples.rb │ │ │ │ │ ├── virtualbox_driver_version_6_x_examples.rb │ │ │ │ │ └── virtualbox_driver_version_7_x_examples.rb │ │ │ │ └── synced_folder_test.rb │ │ │ ├── provisioners/ │ │ │ │ ├── ansible/ │ │ │ │ │ ├── cap/ │ │ │ │ │ │ └── guest/ │ │ │ │ │ │ ├── alpine/ │ │ │ │ │ │ │ └── ansible_install_test.rb │ │ │ │ │ │ ├── arch/ │ │ │ │ │ │ │ └── ansible_install_test.rb │ │ │ │ │ │ ├── debian/ │ │ │ │ │ │ │ └── ansible_install_test.rb │ │ │ │ │ │ ├── freebsd/ │ │ │ │ │ │ │ └── ansible_install_test.rb │ │ │ │ │ │ ├── pip/ │ │ │ │ │ │ │ └── pip_test.rb │ │ │ │ │ │ ├── redhat/ │ │ │ │ │ │ │ └── ansible_install_test.rb │ │ │ │ │ │ ├── shared/ │ │ │ │ │ │ │ └── pip_ansible_install_examples.rb │ │ │ │ │ │ ├── suse/ │ │ │ │ │ │ │ └── ansible_install_test.rb │ │ │ │ │ │ └── ubuntu/ │ │ │ │ │ │ └── ansible_install_test.rb │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── guest_test.rb │ │ │ │ │ │ ├── host_test.rb │ │ │ │ │ │ └── shared.rb │ │ │ │ │ └── provisioner_test.rb │ │ │ │ ├── chef/ │ │ │ │ │ ├── cap/ │ │ │ │ │ │ ├── freebsd/ │ │ │ │ │ │ │ └── chef_installed_test.rb │ │ │ │ │ │ ├── linux/ │ │ │ │ │ │ │ └── chef_installed_test.rb │ │ │ │ │ │ ├── omnios/ │ │ │ │ │ │ │ └── chef_installed_test.rb │ │ │ │ │ │ └── windows/ │ │ │ │ │ │ └── chef_installed_test.rb │ │ │ │ │ ├── command_builder_test.rb │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── base_runner_test.rb │ │ │ │ │ │ ├── base_test.rb │ │ │ │ │ │ ├── chef_apply_test.rb │ │ │ │ │ │ ├── chef_client_test.rb │ │ │ │ │ │ ├── chef_solo_test.rb │ │ │ │ │ │ └── chef_zero_test.rb │ │ │ │ │ ├── omnibus_test.rb │ │ │ │ │ └── provisioner/ │ │ │ │ │ ├── base_test.rb │ │ │ │ │ └── chef_solo_test.rb │ │ │ │ ├── container/ │ │ │ │ │ ├── client_test.rb │ │ │ │ │ └── config_test.rb │ │ │ │ ├── docker/ │ │ │ │ │ ├── config_test.rb │ │ │ │ │ ├── installer_test.rb │ │ │ │ │ ├── plugin_test.rb │ │ │ │ │ └── provisioner_test.rb │ │ │ │ ├── file/ │ │ │ │ │ ├── config_test.rb │ │ │ │ │ └── provisioner_test.rb │ │ │ │ ├── podman/ │ │ │ │ │ ├── config_test.rb │ │ │ │ │ ├── installer_test.rb │ │ │ │ │ └── provisioner_test.rb │ │ │ │ ├── puppet/ │ │ │ │ │ └── provisioner/ │ │ │ │ │ └── puppet_test.rb │ │ │ │ ├── salt/ │ │ │ │ │ ├── bootstrap_downloader_test.rb │ │ │ │ │ ├── config_test.rb │ │ │ │ │ └── provisioner_test.rb │ │ │ │ ├── shell/ │ │ │ │ │ ├── config_test.rb │ │ │ │ │ └── provisioner_test.rb │ │ │ │ └── support/ │ │ │ │ └── shared/ │ │ │ │ └── config.rb │ │ │ ├── pushes/ │ │ │ │ ├── atlas/ │ │ │ │ │ ├── config_test.rb │ │ │ │ │ └── push_test.rb │ │ │ │ ├── ftp/ │ │ │ │ │ ├── adapter_test.rb │ │ │ │ │ ├── config_test.rb │ │ │ │ │ └── push_test.rb │ │ │ │ ├── heroku/ │ │ │ │ │ ├── config_test.rb │ │ │ │ │ └── push_test.rb │ │ │ │ ├── local-exec/ │ │ │ │ │ ├── config_test.rb │ │ │ │ │ └── push_test.rb │ │ │ │ └── noop/ │ │ │ │ └── config_test.rb │ │ │ └── synced_folders/ │ │ │ ├── nfs/ │ │ │ │ ├── action_cleanup_test.rb │ │ │ │ └── config_test.rb │ │ │ ├── rsync/ │ │ │ │ ├── command/ │ │ │ │ │ ├── rsync_auto_test.rb │ │ │ │ │ └── rsync_test.rb │ │ │ │ ├── default_unix_cap_test.rb │ │ │ │ ├── helper_test.rb │ │ │ │ └── synced_folder_test.rb │ │ │ ├── smb/ │ │ │ │ ├── caps/ │ │ │ │ │ └── mount_options_test.rb │ │ │ │ └── synced_folder_test.rb │ │ │ └── unix_mount_helpers_test.rb │ │ ├── support/ │ │ │ ├── dummy_communicator.rb │ │ │ ├── dummy_provider.rb │ │ │ ├── isolated_environment.rb │ │ │ └── shared/ │ │ │ ├── action_synced_folders_context.rb │ │ │ ├── base_context.rb │ │ │ ├── capability_helpers_context.rb │ │ │ ├── plugin_command_context.rb │ │ │ └── virtualbox_context.rb │ │ ├── templates/ │ │ │ ├── commands/ │ │ │ │ └── init/ │ │ │ │ └── Vagrantfile.erb │ │ │ ├── guests/ │ │ │ │ ├── arch/ │ │ │ │ │ ├── default_network/ │ │ │ │ │ │ ├── network_dhcp_test.rb │ │ │ │ │ │ └── network_static_test.rb │ │ │ │ │ └── systemd_networkd/ │ │ │ │ │ ├── network_dhcp_test.rb │ │ │ │ │ └── network_static_test.rb │ │ │ │ ├── debian/ │ │ │ │ │ ├── network_dhcp_test.rb │ │ │ │ │ └── network_static_test.rb │ │ │ │ ├── freebsd/ │ │ │ │ │ ├── network_dhcp_test.rb │ │ │ │ │ └── network_static_test.rb │ │ │ │ ├── funtoo/ │ │ │ │ │ ├── network_dhcp_test.rb │ │ │ │ │ └── network_static_test.rb │ │ │ │ ├── gentoo/ │ │ │ │ │ ├── network_dhcp_test.rb │ │ │ │ │ ├── network_static_test.rb │ │ │ │ │ └── systemd_network_test.rb │ │ │ │ ├── netbsd/ │ │ │ │ │ ├── network_dhcp_test.rb │ │ │ │ │ └── network_static_test.rb │ │ │ │ ├── nixos/ │ │ │ │ │ └── network_test.rb │ │ │ │ ├── redhat/ │ │ │ │ │ ├── network_dhcp_test.rb │ │ │ │ │ └── network_static_test.rb │ │ │ │ └── suse/ │ │ │ │ ├── network_dhcp_test.rb │ │ │ │ ├── network_static6_test.rb │ │ │ │ └── network_static_test.rb │ │ │ └── nfs/ │ │ │ └── exports_darwin_test.rb │ │ ├── vagrant/ │ │ │ ├── action/ │ │ │ │ ├── builder_test.rb │ │ │ │ ├── builtin/ │ │ │ │ │ ├── box_add_test.rb │ │ │ │ │ ├── box_check_outdated_test.rb │ │ │ │ │ ├── box_remove_test.rb │ │ │ │ │ ├── call_test.rb │ │ │ │ │ ├── cleanup_disks_test.rb │ │ │ │ │ ├── cloud_init_setup_test.rb │ │ │ │ │ ├── cloud_init_wait_test.rb │ │ │ │ │ ├── confirm_test.rb │ │ │ │ │ ├── delayed_test.rb │ │ │ │ │ ├── disk_test.rb │ │ │ │ │ ├── env_set_test.rb │ │ │ │ │ ├── graceful_halt_test.rb │ │ │ │ │ ├── handle_box_test.rb │ │ │ │ │ ├── handle_forwarded_port_collisions_test.rb │ │ │ │ │ ├── has_provisioner_test.rb │ │ │ │ │ ├── is_env_set_test.rb │ │ │ │ │ ├── is_state_test.rb │ │ │ │ │ ├── lock_test.rb │ │ │ │ │ ├── message_test.rb │ │ │ │ │ ├── mixin_provisioners_test.rb │ │ │ │ │ ├── mixin_synced_folders_test.rb │ │ │ │ │ ├── provision_test.rb │ │ │ │ │ ├── provisioner_cleanup_test.rb │ │ │ │ │ ├── set_hostname_test.rb │ │ │ │ │ ├── ssh_exec_test.rb │ │ │ │ │ ├── ssh_run_test.rb │ │ │ │ │ ├── synced_folder_cleanup_test.rb │ │ │ │ │ ├── synced_folders_test.rb │ │ │ │ │ ├── trigger_test.rb │ │ │ │ │ └── wait_for_communicator_test.rb │ │ │ │ ├── general/ │ │ │ │ │ └── package_test.rb │ │ │ │ ├── hook_test.rb │ │ │ │ ├── runner_test.rb │ │ │ │ └── warden_test.rb │ │ │ ├── alias_test.rb │ │ │ ├── batch_action_test.rb │ │ │ ├── box_collection_test.rb │ │ │ ├── box_metadata_test.rb │ │ │ ├── box_test.rb │ │ │ ├── bundler_test.rb │ │ │ ├── capability_host_test.rb │ │ │ ├── cli_test.rb │ │ │ ├── config/ │ │ │ │ ├── loader_test.rb │ │ │ │ ├── v1/ │ │ │ │ │ ├── dummy_config_test.rb │ │ │ │ │ ├── loader_test.rb │ │ │ │ │ └── root_test.rb │ │ │ │ └── v2/ │ │ │ │ ├── dummy_config_test.rb │ │ │ │ ├── loader_test.rb │ │ │ │ ├── root_test.rb │ │ │ │ └── util_test.rb │ │ │ ├── config_test.rb │ │ │ ├── environment_test.rb │ │ │ ├── errors_test.rb │ │ │ ├── guest_test.rb │ │ │ ├── host_test.rb │ │ │ ├── machine_index_test.rb │ │ │ ├── machine_state_test.rb │ │ │ ├── machine_test.rb │ │ │ ├── plugin/ │ │ │ │ ├── manager_test.rb │ │ │ │ ├── state_file_test.rb │ │ │ │ ├── v1/ │ │ │ │ │ ├── command_test.rb │ │ │ │ │ ├── communicator_test.rb │ │ │ │ │ ├── config_test.rb │ │ │ │ │ ├── host_test.rb │ │ │ │ │ ├── manager_test.rb │ │ │ │ │ ├── plugin_test.rb │ │ │ │ │ └── provider_test.rb │ │ │ │ └── v2/ │ │ │ │ ├── command_test.rb │ │ │ │ ├── communicator_test.rb │ │ │ │ ├── components_test.rb │ │ │ │ ├── config_test.rb │ │ │ │ ├── host_test.rb │ │ │ │ ├── manager_test.rb │ │ │ │ ├── plugin_test.rb │ │ │ │ ├── provider_test.rb │ │ │ │ ├── synced_folder_test.rb │ │ │ │ └── trigger_test.rb │ │ │ ├── registry_test.rb │ │ │ ├── shared_helpers_test.rb │ │ │ ├── ui_test.rb │ │ │ ├── util/ │ │ │ │ ├── ansi_escape_code_remover_test.rb │ │ │ │ ├── caps_test.rb │ │ │ │ ├── checkpoint_client_test.rb │ │ │ │ ├── command_deprecation_test.rb │ │ │ │ ├── credential_scrubber_test.rb │ │ │ │ ├── curl_helper_test.rb │ │ │ │ ├── deep_merge_test.rb │ │ │ │ ├── directory_test.rb │ │ │ │ ├── downloader_test.rb │ │ │ │ ├── env_test.rb │ │ │ │ ├── experimental_test.rb │ │ │ │ ├── file_checksum_test.rb │ │ │ │ ├── file_mutex_test.rb │ │ │ │ ├── guest_hosts_test.rb │ │ │ │ ├── guest_inspection_test.rb │ │ │ │ ├── guest_networks_spec.rb │ │ │ │ ├── hash_with_indifferent_access_test.rb │ │ │ │ ├── install_cli_autocomplete_test.rb │ │ │ │ ├── io_test.rb │ │ │ │ ├── ipv4_interfaces_test.rb │ │ │ │ ├── is_port_open_test.rb │ │ │ │ ├── keypair_test.rb │ │ │ │ ├── line_buffer_test.rb │ │ │ │ ├── line_endings_helper_test.rb │ │ │ │ ├── map_command_options_test.rb │ │ │ │ ├── mime_test.rb │ │ │ │ ├── network_ip_test.rb │ │ │ │ ├── numeric_test.rb │ │ │ │ ├── platform_test.rb │ │ │ │ ├── powershell_test.rb │ │ │ │ ├── presence_test.rb │ │ │ │ ├── retryable_test.rb │ │ │ │ ├── safe_chdir_test.rb │ │ │ │ ├── scoped_hash_override_test.rb │ │ │ │ ├── shell_quote_test.rb │ │ │ │ ├── ssh_test.rb │ │ │ │ ├── string_block_editor_test.rb │ │ │ │ ├── subprocess_test.rb │ │ │ │ ├── uploader_test.rb │ │ │ │ └── which_test.rb │ │ │ └── vagrantfile_test.rb │ │ └── vagrant_test.rb │ └── vagrant-spec/ │ ├── .runner-vmware.sh │ ├── Vagrantfile.spec │ ├── boxes/ │ │ └── .keep │ ├── configs/ │ │ ├── vagrant-spec.config.docker.rb │ │ └── vagrant-spec.config.virtualbox.rb │ ├── readme.md │ └── scripts/ │ ├── centos-run.virtualbox.sh │ ├── centos-setup.virtualbox.sh │ ├── ubuntu-install-vagrant.sh │ ├── ubuntu-run.docker.sh │ ├── ubuntu-run.virtualbox.sh │ ├── ubuntu-setup.docker.sh │ ├── ubuntu-setup.virtualbox.sh │ ├── windows-run.virtualbox.ps1 │ └── windows-setup.virtualbox.ps1 ├── vagrant-spec.config.example.rb ├── vagrant.gemspec ├── version.txt └── website/ ├── .editorconfig ├── .env ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .stylelintrc.js ├── LICENSE.md ├── Makefile ├── README.md ├── content/ │ ├── docs/ │ │ ├── boxes/ │ │ │ ├── base.mdx │ │ │ ├── box_repository.mdx │ │ │ ├── format.mdx │ │ │ ├── index.mdx │ │ │ ├── info.mdx │ │ │ └── versioning.mdx │ │ ├── cli/ │ │ │ ├── aliases.mdx │ │ │ ├── box.mdx │ │ │ ├── cloud.mdx │ │ │ ├── connect.mdx │ │ │ ├── destroy.mdx │ │ │ ├── global-status.mdx │ │ │ ├── halt.mdx │ │ │ ├── index.mdx │ │ │ ├── init.mdx │ │ │ ├── login.mdx │ │ │ ├── machine-readable.mdx │ │ │ ├── non-primary.mdx │ │ │ ├── package.mdx │ │ │ ├── plugin.mdx │ │ │ ├── port.mdx │ │ │ ├── powershell.mdx │ │ │ ├── provision.mdx │ │ │ ├── rdp.mdx │ │ │ ├── reload.mdx │ │ │ ├── resume.mdx │ │ │ ├── rsync-auto.mdx │ │ │ ├── rsync.mdx │ │ │ ├── share.mdx │ │ │ ├── snapshot.mdx │ │ │ ├── ssh.mdx │ │ │ ├── ssh_config.mdx │ │ │ ├── status.mdx │ │ │ ├── suspend.mdx │ │ │ ├── up.mdx │ │ │ ├── upload.mdx │ │ │ ├── validate.mdx │ │ │ ├── version.mdx │ │ │ ├── winrm.mdx │ │ │ └── winrm_config.mdx │ │ ├── cloud-init/ │ │ │ ├── configuration.mdx │ │ │ ├── index.mdx │ │ │ └── usage.mdx │ │ ├── disks/ │ │ │ ├── configuration.mdx │ │ │ ├── hyperv/ │ │ │ │ ├── common-issues.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── usage.mdx │ │ │ ├── index.mdx │ │ │ ├── usage.mdx │ │ │ ├── virtualbox/ │ │ │ │ ├── common-issues.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── usage.mdx │ │ │ └── vmware/ │ │ │ ├── common-issues.mdx │ │ │ ├── index.mdx │ │ │ └── usage.mdx │ │ ├── experimental/ │ │ │ └── index.mdx │ │ ├── index.mdx │ │ ├── installation/ │ │ │ ├── backwards-compatibility.mdx │ │ │ ├── index.mdx │ │ │ ├── source.mdx │ │ │ ├── uninstallation.mdx │ │ │ ├── upgrading-from-1-0.mdx │ │ │ └── upgrading.mdx │ │ ├── multi-machine.mdx │ │ ├── networking/ │ │ │ ├── basic_usage.mdx │ │ │ ├── forwarded_ports.mdx │ │ │ ├── index.mdx │ │ │ ├── private_network.mdx │ │ │ └── public_network.mdx │ │ ├── other/ │ │ │ ├── debugging.mdx │ │ │ ├── environmental-variables.mdx │ │ │ ├── index.mdx │ │ │ ├── macos-catalina.mdx │ │ │ └── wsl.mdx │ │ ├── plugins/ │ │ │ ├── action-hooks.mdx │ │ │ ├── commands.mdx │ │ │ ├── configuration.mdx │ │ │ ├── development-basics.mdx │ │ │ ├── go-plugins/ │ │ │ │ ├── guests.mdx │ │ │ │ └── index.mdx │ │ │ ├── guest-capabilities.mdx │ │ │ ├── guests.mdx │ │ │ ├── host-capabilities.mdx │ │ │ ├── hosts.mdx │ │ │ ├── index.mdx │ │ │ ├── packaging.mdx │ │ │ ├── providers.mdx │ │ │ ├── provisioners.mdx │ │ │ └── usage.mdx │ │ ├── providers/ │ │ │ ├── basic_usage.mdx │ │ │ ├── configuration.mdx │ │ │ ├── custom.mdx │ │ │ ├── default.mdx │ │ │ ├── docker/ │ │ │ │ ├── basics.mdx │ │ │ │ ├── boxes.mdx │ │ │ │ ├── commands.mdx │ │ │ │ ├── configuration.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── networking.mdx │ │ │ ├── hyperv/ │ │ │ │ ├── boxes.mdx │ │ │ │ ├── configuration.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── limitations.mdx │ │ │ │ └── usage.mdx │ │ │ ├── index.mdx │ │ │ ├── installation.mdx │ │ │ ├── virtualbox/ │ │ │ │ ├── boxes.mdx │ │ │ │ ├── common-issues.mdx │ │ │ │ ├── configuration.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── networking.mdx │ │ │ │ └── usage.mdx │ │ │ └── vmware/ │ │ │ ├── boxes.mdx │ │ │ ├── configuration.mdx │ │ │ ├── faq.mdx │ │ │ ├── index.mdx │ │ │ ├── installation.mdx │ │ │ ├── known-issues.mdx │ │ │ ├── usage.mdx │ │ │ └── vagrant-vmware-utility.mdx │ │ ├── provisioning/ │ │ │ ├── ansible.mdx │ │ │ ├── ansible_common.mdx │ │ │ ├── ansible_intro.mdx │ │ │ ├── ansible_local.mdx │ │ │ ├── basic_usage.mdx │ │ │ ├── cfengine.mdx │ │ │ ├── chef_apply.mdx │ │ │ ├── chef_client.mdx │ │ │ ├── chef_common.mdx │ │ │ ├── chef_solo.mdx │ │ │ ├── chef_zero.mdx │ │ │ ├── docker.mdx │ │ │ ├── file.mdx │ │ │ ├── index.mdx │ │ │ ├── podman.mdx │ │ │ ├── puppet_agent.mdx │ │ │ ├── puppet_apply.mdx │ │ │ ├── salt.mdx │ │ │ └── shell.mdx │ │ ├── push/ │ │ │ ├── ftp.mdx │ │ │ ├── heroku.mdx │ │ │ ├── index.mdx │ │ │ └── local-exec.mdx │ │ ├── share/ │ │ │ ├── connect.mdx │ │ │ ├── http.mdx │ │ │ ├── index.mdx │ │ │ ├── provider.mdx │ │ │ ├── security.mdx │ │ │ └── ssh.mdx │ │ ├── synced-folders/ │ │ │ ├── basic_usage.mdx │ │ │ ├── index.mdx │ │ │ ├── nfs.mdx │ │ │ ├── rsync.mdx │ │ │ ├── smb.mdx │ │ │ └── virtualbox.mdx │ │ ├── triggers/ │ │ │ ├── configuration.mdx │ │ │ ├── index.mdx │ │ │ └── usage.mdx │ │ └── vagrantfile/ │ │ ├── index.mdx │ │ ├── machine_settings.mdx │ │ ├── ssh_settings.mdx │ │ ├── tips.mdx │ │ ├── vagrant_settings.mdx │ │ ├── vagrant_version.mdx │ │ ├── version.mdx │ │ ├── winrm_settings.mdx │ │ └── winssh_settings.mdx │ ├── intro/ │ │ ├── contributing-guide.mdx │ │ ├── index.mdx │ │ ├── support.mdx │ │ └── vs/ │ │ ├── cli-tools.mdx │ │ ├── docker.mdx │ │ ├── index.mdx │ │ └── terraform.mdx │ ├── vagrant-cloud/ │ │ ├── api/ │ │ │ ├── v1.mdx │ │ │ └── v2.mdx │ │ ├── boxes/ │ │ │ ├── architecture.mdx │ │ │ ├── catalog.mdx │ │ │ ├── create-version.mdx │ │ │ ├── create.mdx │ │ │ ├── distributing.mdx │ │ │ ├── index.mdx │ │ │ ├── lifecycle.mdx │ │ │ ├── private.mdx │ │ │ ├── release-workflow.mdx │ │ │ └── using.mdx │ │ ├── hcp-vagrant/ │ │ │ ├── migration-guide.mdx │ │ │ ├── post-migration-guide.mdx │ │ │ └── troubleshooting.mdx │ │ ├── index.mdx │ │ ├── organizations/ │ │ │ ├── authentication-policy.mdx │ │ │ ├── create.mdx │ │ │ ├── index.mdx │ │ │ └── migrate.mdx │ │ ├── request-limits.mdx │ │ ├── support.mdx │ │ └── users/ │ │ ├── authentication.mdx │ │ ├── index.mdx │ │ └── recovery.mdx │ └── vmware/ │ └── index.mdx ├── data/ │ ├── alert-banner.js │ ├── docs-nav-data.json │ ├── intro-nav-data.json │ ├── metadata.js │ ├── subnav.js │ ├── vagrant-cloud-nav-data.json │ ├── version.json │ └── vmware-nav-data.json ├── jsconfig.json ├── package.json ├── prettier.config.js ├── public/ │ └── ie-warning.js ├── redirects.js ├── scripts/ │ ├── should-build.sh │ ├── website-build.sh │ └── website-start.sh └── vercel.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .ci/.ci-utility-files/common.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2019, 2025 # SPDX-License-Identifier: MPL-2.0 # # shellcheck disable=SC2119 # shellcheck disable=SC2164 # If the bash version isn't at least 4, bail if [ "${BASH_VERSINFO:-0}" -lt "4" ]; then printf "ERROR: Expected bash version >= 4 (is: %d)" "${BASH_VERSINFO:-0}" exit 1 fi # Lets have some emojis WARNING_ICON="⚠️" ERROR_ICON="🛑" # Coloring # shellcheck disable=SC2034 TEXT_BOLD='\e[1m' TEXT_RED='\e[31m' # shellcheck disable=SC2034 TEXT_GREEN='\e[32m' TEXT_YELLOW='\e[33m' TEXT_CYAN='\e[36m' TEXT_CLEAR='\e[0m' # Common variables export full_sha="${GITHUB_SHA}" export short_sha="${full_sha:0:8}" export ident_ref="${GITHUB_REF#*/*/}" export repository="${GITHUB_REPOSITORY}" export repo_owner="${repository%/*}" export repo_name="${repository#*/}" # shellcheck disable=SC2153 export asset_cache="${ASSETS_PRIVATE_SHORTTERM}/${repository}/${GITHUB_ACTION}" export run_number="${GITHUB_RUN_NUMBER}" export run_id="${GITHUB_RUN_ID}" export job_id="${run_id}-${run_number}" readonly hc_releases_metadata_filename="release-meta.json" # This value is used in our cleanup trap to restore the value in cases # where a function call may have failed and did not restore it readonly _repository_backup="${repository}" if [ -z "${ci_bin_dir}" ]; then if ci_bin_dir="$(realpath ./.ci-bin)"; then export ci_bin_dir else echo "ERROR: Failed to create the local CI bin directory" exit 1 fi fi # We are always noninteractive export DEBIAN_FRONTEND=noninteractive # If we are on a runner and debug mode is enabled, # enable debug mode for ourselves too if [ -n "${RUNNER_DEBUG}" ]; then DEBUG=1 fi # If DEBUG is enabled and we are running tests, # flag it so we can adjust where output is sent. if [ -n "${DEBUG}" ] && [ -n "${BATS_TEST_FILENAME}" ]; then DEBUG_WITH_BATS=1 fi # Write debug output to stderr. Message template # and arguments are passed to `printf` for formatting. # # $1: message template # $#: message arguments # # NOTE: Debug output is only displayed when DEBUG is set function debug() { if [ -n "${DEBUG}" ]; then local msg_template="${1}" local i=$(( ${#} - 1 )) local msg_args=("${@:2:$i}") # Update template to include caller information msg_template=$(printf "<%s(%s:%d)> %s" "${FUNCNAME[1]}" "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${msg_template}") #shellcheck disable=SC2059 msg="$(printf "${msg_template}" "${msg_args[@]}")" if [ -n "${DEBUG_WITH_BATS}" ]; then printf "%b%s%b\n" "${TEXT_CYAN}" "${msg}" "${TEXT_CLEAR}" >&3 else printf "%b%s%b\n" "${TEXT_CYAN}" "${msg}" "${TEXT_CLEAR}" >&2 fi fi } # Wrap the pushd command so we fail # if the pushd command fails. Arguments # are just passed through. function pushd() { debug "executing 'pushd %s'" "${*}" command builtin pushd "${@}" > /dev/null || exit 1 } # Wrap the popd command so we fail # if the popd command fails. Arguments # are just passed through. # shellcheck disable=SC2120 function popd() { debug "executing 'popd %s'" "${*}" command builtin popd "${@}" || exit 1 } # Wraps the aws CLI command to support # role based access. It will check for # expected environment variables when # a role has been assumed. If they are # not found, it will assume the configured # role. If the role has already been # assumed, it will check that the credentials # have not timed out, and re-assume the # role if so. If no role information is # provided, it will just pass the command # through directly # # NOTE: Required environment variable: AWS_ASSUME_ROLE_ARN # NOTE: This was a wrapper for the AWS command that would properly # handle the assume role process and and automatically refresh # if close to expiry. With credentials being handled by the doormat # action now, this is no longer needed but remains in case it's # needed for some reason in the future. function aws_deprecated() { # Grab the actual aws cli path if ! aws_path="$(which aws)"; then (>&2 echo "AWS error: failed to locate aws cli executable") return 1 fi # First, check if the role ARN environment variable is # configured. If it is not, just pass through. if [ "${AWS_ASSUME_ROLE_ARN}" = "" ]; then "${aws_path}" "${@}" return $? fi # Check if a role has already been assumed. If it # has, validate the credentials have not timed out # and pass through. if [ "${AWS_SESSION_TOKEN}" != "" ]; then # Cut off part of the expiration so we don't end up hitting # the expiration just as we make our call expires_at=$(date -d "${AWS_SESSION_EXPIRATION} - 20 sec" "+%s") if (( "${expires_at}" > $(date +%s) )); then "${aws_path}" "${@}" return $? fi # If we are here then the credentials were not # valid so clear the session token and restore # original credentials unset AWS_SESSION_TOKEN unset AWS_SESSION_EXPIRATION export AWS_ACCESS_KEY_ID="${CORE_AWS_ACCESS_KEY_ID}" export AWS_SECRET_ACCESS_KEY="${CORE_AWS_SECRET_ACCESS_KEY}" fi # Now lets assume the role if aws_output="$("${aws_path}" sts assume-role --role-arn "${AWS_ASSUME_ROLE_ARN}" --role-session-name "VagrantCI@${repo_name}-${job_id}")"; then export CORE_AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}" export CORE_AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}" id="$(printf '%s' "${aws_output}" | jq -r .Credentials.AccessKeyId)" || failed=1 key="$(printf '%s' "${aws_output}" | jq -r .Credentials.SecretAccessKey)" || failed=1 token="$(printf '%s' "${aws_output}" | jq -r .Credentials.SessionToken)" || failed=1 expire="$(printf '%s' "${aws_output}" | jq -r .Credentials.Expiration)" || failed=1 if [ "${failed}" = "1" ]; then (>&2 echo "Failed to extract assume role credentials") return 1 fi unset aws_output export AWS_ACCESS_KEY_ID="${id}" export AWS_SECRET_ACCESS_KEY="${key}" export AWS_SESSION_TOKEN="${token}" export AWS_SESSION_EXPIRATION="${expire}" else (>&2 echo "AWS assume role error: ${aws_output}") return 1 fi # And we can execute! "${aws_path}" "${@}" } # Path to file used for output redirect # and extracting messages for warning and # failure information sent to slack function output_file() { if [ "${1}" = "clean" ] && [ -f "${ci_output_file_path}" ]; then rm -f "${ci_output_file_path}" unset ci_output_file_path fi if [ -z "${ci_output_file_path}" ] || [ ! -f "${ci_output_file_path}" ]; then ci_output_file_path="$(mktemp)" fi printf "%s" "${ci_output_file_path}" } # Write failure message, send error to configured # slack, and exit with non-zero status. If an # "$(output_file)" file exists, the last 5 lines will be # included in the slack message. # # $1: Failure message function failure() { local msg_template="${1}" local i=$(( ${#} - 1 )) local msg_args=("${@:2:$i}") # Update template to include caller information if in DEBUG mode if [ -n "${DEBUG}" ]; then msg_template=$(printf "<%s(%s:%d)> %s" "${FUNCNAME[1]}" "${BASH_SOURCE[1]}" "${BASH_LINENO[0]}" "${msg_template}") fi #shellcheck disable=SC2059 msg="$(printf "${msg_template}" "${msg_args[@]}")" if [ -n "${DEBUG_WITH_BATS}" ]; then printf "%s %b%s%b\n" "${ERROR_ICON}" "${TEXT_RED}" "${msg}" "${TEXT_CLEAR}" >&3 else printf "%s %b%s%b\n" "${ERROR_ICON}" "${TEXT_RED}" "${msg}" "${TEXT_CLEAR}" >&2 fi if [ -n "${SLACK_WEBHOOK}" ]; then if [ -f "$(output_file)" ]; then slack -s error -m "ERROR: ${msg}" -f "$(output_file)" -T 5 else slack -s error -m "ERROR: ${msg}" fi fi exit 1 } # Write warning message, send warning to configured # slack # # $1: Warning message function warn() { local msg_template="${1}" local i=$(( ${#} - 1 )) local msg_args=("${@:2:$i}") #shellcheck disable=SC2059 msg="$(printf "${msg_template}" "${msg_args[@]}")" printf "%s %b%s%b\n" "${WARNING_ICON}" "${TEXT_YELLOW}" "${msg}" "${TEXT_CLEAR}" >&2 if [ -n "${SLACK_WEBHOOK}" ]; then if [ -f "$(output_file)" ]; then slack -s warn -m "WARNING: ${msg}" -f "$(output_file)" else slack -s warn -m "WARNING: ${msg}" fi fi } # Write an informational message function info() { local msg_template="${1}\n" local i=$(( ${#} - 1 )) local msg_args=("${@:2:$i}") #shellcheck disable=SC2059 printf "${msg_template}" "${msg_args[@]}" >&2 } # Execute command while redirecting all output to # a file (file is used within fail mesage on when # command is unsuccessful). Final argument is the # error message used when the command fails. # # $@{1:$#-1}: Command to execute # $@{$#}: Failure message function wrap() { local i=$((${#} - 1)) if ! wrap_raw "${@:1:$i}"; then cat "$(output_file)" failure "${@:$#}" fi rm "$(output_file)" } # Execute command while redirecting all output to # a file. Exit status is returned. function wrap_raw() { output_file "clean" > /dev/null 2>&1 "${@}" > "$(output_file)" 2>&1 return $? } # Execute command while redirecting all output to # a file (file is used within fail mesage on when # command is unsuccessful). Command output will be # streamed during execution. Final argument is the # error message used when the command fails. # # $@{1:$#-1}: Command to execute # $@{$#}: Failure message function wrap_stream() { i=$((${#} - 1)) if ! wrap_stream_raw "${@:1:$i}"; then failure "${@:$#}" fi rm "$(output_file)" } # Execute command while redirecting all output # to a file. Command output will be streamed # during execution. Exit status is returned function wrap_stream_raw() { output_file "clean" "${@}" > "$(output_file)" 2>&1 & pid=$! until [ -f "$(output_file)" ]; do sleep 0.1 done tail -f --quiet --pid "${pid}" "$(output_file)" wait "${pid}" return $? } # Send command to packet device and wrap # execution # $@{1:$#-1}: Command to execute # $@{$#}: Failure message function pkt_wrap() { wrap packet-exec run -quiet -- "${@}" } # Send command to packet device and wrap # execution # $@: Command to execute function pkt_wrap_raw() { wrap_raw packet-exec run -quiet -- "${@}" } # Send command to packet device and wrap # execution with output streaming # $@{1:$#-1}: Command to execute # $@{$#}: Failure message function pkt_wrap_stream() { wrap_stream packet-exec run -quiet -- "${@}" } # Send command to packet device and wrap # execution with output streaming # $@: Command to execute function pkt_wrap_stream_raw() { wrap_stream_raw packet-exec run -quiet -- "${@}" } # Get the full path directory for a given # file path. File is not required to exist. # NOTE: Parent directories of given path will # be created. # # $1: file path function file_directory() { local path="${1?File path is required}" local dir if [[ "${path}" != *"/"* ]]; then dir="." else dir="${path%/*}" fi if [ ! -d "${dir}" ]; then mkdir -p "${dir}" || failure "Could not create directory (%s)" "${dir}" fi pushd "${dir}" dir="$(pwd)" || failure "Could not read directory path (%s)" "${dir}" popd printf "%s" "${dir}" } # Wait until the number of background jobs falls below # the maximum number provided. If the max number was reached # and waiting was performed until a process completed, the # string "waited" will be printed to stdout. # # NOTE: using `wait -n` would be cleaner but only became # available in bash as of 4.3 # # $1: maximum number of jobs function background_jobs_limit() { local max="${1}" if [ -z "${max}" ] || [[ "${max}" = *[!0123456789]* ]]; then failure "Maximum number of background jobs required" fi local debug_printed local jobs mapfile -t jobs <<< "$(jobs -p)" || failure "Could not read background job list" while [ "${#jobs[@]}" -ge "${max}" ]; do if [ -z "${debug_printed}" ]; then debug "max background jobs reached (%d), waiting for free process" "${max}" debug_printed="1" fi sleep 1 jobs=() local j_pids mapfile -t j_pids <<< "$(jobs -p)" || failure "Could not read background job list" for j in "${j_pids[@]}"; do if kill -0 "${j}" > /dev/null 2>&1; then jobs+=( "${j}" ) fi done done if [ -n "${debug_printed}" ]; then debug "background jobs count (%s) under max, continuing" "${#jobs[@]}" printf "waited" fi } # Reap a completed background process. If the process is # not complete, the process is ignored. The success/failure # returned from this function only applies to the process # identified by the provided PID _if_ the matching PID value # was written to stdout # # $1: PID function reap_completed_background_job() { local pid="${1}" if [ -z "${pid}" ]; then failure "PID of process to reap is required" fi if kill -0 "${pid}" > /dev/null 2>&1; then debug "requested pid to reap (%d) has not completed, ignoring" "${pid}" return 0 fi # The pid can be reaped so output the pid to indicate # any error is from the job printf "%s" "${pid}" if ! wait "${pid}"; then local code="${?}" debug "wait error code %d returned for pid %d" "${code}" "${pid}" return "${code}" fi return 0 } # Creates a cache and adds the provided items # # -d Optional description # -f Force cache (deletes cache if already exists) # # $1: name of cache # $2: artifact(s) to cache (path to artifact or directory containing artifacts) function create-cache() { local body local force local opt while getopts ":d:f" opt; do case "${opt}" in "d") body="${OPTARG}" ;; "f") force="1" ;; *) failure "Invalid flag provided" ;; esac done shift $((OPTIND-1)) cache_name="${1}" artifact_path="${2}" if [ -z "${cache_name}" ]; then failure "Cache name is required" fi if [ -z "${artifact_path}" ]; then failure "Artifact path is required" fi # Check for the cache if github_draft_release_exists "${repo_name}" "${cache_name}"; then # If forcing, delete the cache if [ -n "${force}" ]; then debug "cache '%s' found and force is set, removing" github_delete_draft_release "${cache_name}" else failure "Cache already exists (name: %s repo: %s)" "${cache_name}" "${repo_name}" fi fi # If no description is provided, then provide a default if [ -z "${body}" ]; then body="Cache name: %s\nCreate time: %s\nSource run: %s/%s/actions/runs/%s" \ "${cache_name}" "$(date)" "${GITHUB_SERVER_URL}" "${GITHUB_REPOSITORY}" "${GITHUB_RUN_ID}" fi # Make sure body is formatted if [ -n "${body}" ]; then body="$(printf "%b" "${body}")" fi response="$(github_create_release -o "${repo_owner}" -r "${repo_name}" -n "${cache_name}" -b "${body}")" || failure "Failed to create GitHub release" } # Retrieve items from cache # # -r Require cache to exist (failure if not found) # # $1: cache name # $2: destination directory function restore-cache() { local required while getopts ":r" opt; do case "${opt}" in "r") required="1" ;; *) failure "Invalid flag provided" ;; esac done shift $((OPTIND-1)) cache_name="${1}" destination="${2}" if [ -z "${cache_name}" ]; then failure "Cache name is required" fi if [ -z "${destination}" ]; then failure "Destination is required" fi # If required, check for the draft release and error if not found if [ -n "${required}" ]; then if ! github_draft_release_exists "${repo_name}" "${cache_name}"; then failure "Cache '%s' does not exist" "${cache_name}" fi fi mkdir -p "${destination}" || failure "Could not create destination directory (%s)" "${destination}" pushd "${destination}" github_draft_release_assets "${repo_name}" "${cache_name}" popd } # Submit given file to Apple's notarization service and # staple the notarization ticket. # # -i UUID: app store connect issuer ID (optional) # -j PATH: JSON file containing API key # -k ID: app store connect API key ID (optional) # -m SECS: maximum number of seconds to wait (optional, defaults to 600) # -o PATH: path to write notarized file (optional, will modify input by default) # # $1: file to notarize function notarize_file() { local creds_api_key_id local creds_api_key_path local creds_issuer_id local output_file local max_wait="600" local opt while getopts ":i:j:k:m:o:" opt; do case "${opt}" in "i") creds_api_key_id="${OPTARG}" ;; "j") creds_api_key_path="${OPTARG}" ;; "k") creds_issuer_id="${OPTARG}" ;; "m") max_wait="${OPTARG}" ;; "o") output_file="${OPTARG}" ;; *) failure "Invalid flag provided" ;; esac done shift $((OPTIND-1)) # Validate credentials were provided if [ -z "${creds_api_key_path}" ]; then failure "App store connect key path required for notarization" fi if [ ! -f "${creds_api_key_path}" ]; then failure "Invalid path provided for app store connect key path (%s)" "${creds_api_key_path}" fi # Collect auth related arguments local base_args=( "--api-key-path" "${creds_api_key_path}" ) if [ -n "${creds_api_key_id}" ]; then base_args+=( "--api-key" "${creds_api_key_id}" ) fi if [ -n "${creds_issuer_id}" ]; then base_args+=( "--api-issuer" "${creds_issuer_id}" ) fi local input_file="${1}" # Validate the input file if [ -z "${input_file}" ]; then failure "Input file is required for signing" fi if [ ! -f "${input_file}" ]; then failure "Cannot find input file (%s)" "${input_file}" fi # Check that rcodesign is available, and install # it if it is not if ! command -v rcodesign > /dev/null; then debug "rcodesign executable not found, installing..." install_github_tool "indygreg" "apple-platform-rs" "rcodesign" fi local notarize_file # If an output file path was defined, copy file # to output location before notarizing if [ -n "${output_file}" ]; then file_directory "${output_file}" # Remove file if it already exists rm -f "${output_file}" || failure "Could not modify output file (%s)" "${output_file}" cp -f "${input_file}" "${output_file}" || failure "Could not write to output file (%s)" "${output_file}" notarize_file="${output_file}" debug "notarizing file '%s' and writing to '%s'" "${input_file}" "${output_file}" else notarize_file="${input_file}" debug "notarizing file in place '%s'" "${input_file}" fi # Notarize the file local notarize_output if notarize_output="$(rcodesign \ notary-submit \ "${base_args[@]}" \ --max-wait-seconds "${max_wait}" \ --staple \ "${notarize_file}" 2>&1)"; then return 0 fi debug "notarization output: %s" "${notarize_output}" # Still here means notarization failure. Pull # the logs from the service before failing local submission_id="${notarize_output##*submission ID: }" submission_id="${submission_id%%$'\n'*}" rcodesign \ notary-log \ "${base_args[@]}" \ "${submission_id}" failure "Failed to notarize file (%s)" "${input_file}" } # Sign a file using signore. Will automatically apply # modified retry settings when larger files are submitted. # # -b NAME: binary identifier (macOS only) # -e PATH: path to entitlements file (macOS only) # -o PATH: path to write signed file (optional, will overwrite input by default) # $1: file to sign # # NOTE: If signore is not installed, a HASHIBOT_TOKEN is # required for downloading the signore release. The # token can also be set in SIGNORE_GITHUB_TOKEN if # the HASHIBOT_TOKEN is already set # # NOTE: SIGNORE_CLIENT_ID, SIGNORE_CLIENT_SECRET, and SIGNORE_SIGNER # environment variables must be set prior to calling this function function sign_file() { # Set 50M to be a largish file local largish_file_size="52428800" # Signore environment variables are required. Check # that they are set. if [ -z "${SIGNORE_CLIENT_ID}" ]; then failure "Cannot sign file, SIGNORE_CLIENT_ID is not set" fi if [ -z "${SIGNORE_CLIENT_SECRET}" ]; then failure "Cannot sign file, SIGNORE_CLIENT_SECRET is not set" fi if [ -z "${SIGNORE_SIGNER}" ]; then failure "Cannot sign file, SIGNORE_SIGNER is not set" fi local binary_identifier="" local entitlements="" local output_file="" local opt while getopts ":b:e:o:" opt; do case "${opt}" in "b") binary_identifier="${OPTARG}" ;; "e") entitlements="${OPTARG}" ;; "o") output_file="${OPTARG}" ;; *) failure "Invalid flag provided" ;; esac done shift $((OPTIND-1)) local input_file="${1}" # Check that a good input file was given if [ -z "${input_file}" ]; then failure "Input file is required for signing" fi if [ ! -f "${input_file}" ]; then failure "Cannot find input file (%s)" "${input_file}" fi # If the output file is not set it's a replacement if [ -z "${output_file}" ]; then debug "output file is unset, will replace input file (%s)" "${input_file}" output_file="${input_file}" fi # This will ensure parent directories exist file_directory "${output_file}" > /dev/null # If signore command is not installed, install it if ! command -v "signore" > /dev/null; then local hashibot_token_backup="${HASHIBOT_TOKEN}" # If the signore github token is set, apply it if [ -n "${SIGNORE_GITHUB_TOKEN}" ]; then HASHIBOT_TOKEN="${SIGNORE_GITHUB_TOKEN}" fi install_hashicorp_tool "signore" # Restore the hashibot token if it was modified HASHIBOT_TOKEN="${hashibot_token_backup}" fi # Define base set of arguments local signore_args=( "sign" "--file" "${input_file}" "--out" "${output_file}" "--match-file-mode" ) # Check the size of the file to be signed. If it's relatively # large, push up the max retries and lengthen the retry interval # NOTE: Only checked if `wc` is available local file_size="0" if command -v wc > /dev/null; then file_size="$(wc -c <"${input_file}")" || failure "Could not determine input file size" fi if [ "${file_size}" -gt "${largish_file_size}" ]; then debug "largish file detected, adjusting retry settings" signore_args+=( "--max-retries" "30" "--retry-interval" "10s" ) fi # If a binary identifier was provided then it's a macos signing if [ -n "${binary_identifier}" ]; then # shellcheck disable=SC2016 template='{type: "macos", input_format: "EXECUTABLE", binary_identifier: $identifier}' payload="$(jq -n --arg identifier "${binary_identifier}" "${template}")" || failure "Could not create signore payload for macOS signing" signore_args+=( "--signer-options" "${payload}" ) fi # If an entitlement was provided, validate the path # and add it to the args if [ -n "${entitlements}" ]; then if [ ! -f "${entitlements}" ]; then failure "Invalid path for entitlements provided (%s)" "${entitlements}" fi signore_args+=( "--entitlements" "${entitlements}" ) fi debug "signing file '%s' with arguments - %s" "${input_file}" "${signore_args[*]}" signore "${signore_args[@]}" || failure "Failed to sign file '%s'" "${input_file}" info "successfully signed file (%s)" "${input_file}" } # Create a GPG signature. This uses signore to generate a # gpg signature for a given file. If the destination # path for the signature is not provided, it will # be stored at the origin path with a .sig suffix # # $1: Path to origin file # $2: Path to store signature (optional) function gpg_sign_file() { # Check that we have something to sign if [ -z "${1}" ]; then failure "Origin file is required for signing" fi if [ ! -f "${1}" ]; then failure "Origin file does not exist (${1})" fi # Validate environment has required signore variables set if [ -z "${SIGNORE_CLIENT_ID}" ]; then failure "Cannot sign file, SIGNORE_CLIENT_ID is not set" fi if [ -z "${SIGNORE_CLIENT_SECRET}" ]; then failure "Cannot sign file, SIGNORE_CLIENT_SECRET is not set" fi if [ -z "${SIGNORE_SIGNER}" ]; then failure "Cannot sign file, SIGNORE_SIGNER is not set" fi local origin="${1}" local destination="${2}" if [ -z "${destination}" ]; then destination="${origin}.sig" debug "destination automatically set (%s)" "${destination}" fi if ! command -v signore; then debug "installing signore tool" install_hashicorp_tool "signore" fi if [ -e "${destination}" ]; then failure "File already exists at signature destination path (${destination})" fi wrap_stream signore sign --dearmor --file "${origin}" --out "${destination}" \ "Failed to sign file" } # Validate arguments for GitHub release. Checks for # two arguments and that second argument is an exiting # file asset, or directory. # # $1: GitHub tag name # $2: Asset file or directory of assets function release_validate() { if [ "${1}" = "" ]; then failure "Missing required position 1 argument (TAG) for release" fi if [ "${2}" = "" ]; then failure "Missing required position 2 argument (PATH) for release" fi if [ ! -e "${2}" ]; then failure "Path provided for release (${2}) does not exist" fi } # Generate a GitHub release # # $1: GitHub tag name # $2: Asset file or directory of assets function release() { release_validate "${@}" local tag_name="${1}" local assets="${2}" local body if [ -z "${body}" ]; then body="$(release_details "${tag_name}")" fi response="$(github_create_release -o "${repo_owner}" -r "${repo_name}" -t "${tag_name}" -n "${tag_name}" -b "${body}")" || failure "Failed to create GitHub release" local release_id release_id="$(printf "%s" "${response}" | jq -r '.id')" || failure "Failed to extract release ID from response for %s on %s" "${tag_name}" "${repository}" github_upload_release_artifacts "${repo_name}" "${release_id}" "${assets}" } # Generate a GitHub prerelease # # $1: GitHub tag name # $2: Asset file or directory of assets function prerelease() { release_validate "${@}" local ptag if [[ "${1}" != *"+"* ]]; then ptag="${1}+${short_sha}" else ptag="${1}" fi local assets="${2}" response="$(github_create_release -o "${repo_owner}" -r "${repo_name}" -t "${ptag}" -n "${ptag}" -b "${body}" -p -m)" || failure "Failed to create GitHub prerelease" local release_id release_id="$(printf "%s" "${response}" | jq -r '.id')" || failure "Failed to extract prerelease ID from response for %s on %s" "${tag_name}" "${repository}" github_upload_release_artifacts "${repo_name}" "${release_id}" "${assets}" printf "New prerelease published to %s @ %s\n" "${repo_name}" "${ptag}" >&2 printf "%s" "${ptag}" } # Generate a GitHub draft release # # $1: GitHub release name # $2: Asset file or directory of assets function draft_release() { local ptag="${1}" local assets="${2}" response="$(github_create_release -o "${repo_owner}" -r "${repo_name}" -t "${ptag}" -n "${ptag}" -b "${body}" -d)" || failure "Failed to create GitHub draft release" local release_id release_id="$(printf "%s" "${response}" | jq -r '.id')" || failure "Failed to extract draft release ID from response for %s on %s" "${tag_name}" "${repository}" github_upload_release_artifacts "${repo_name}" "${release_id}" "${assets}" printf "%s" "${ptag}" } # Generate details of the release. This will consist # of a link to the changelog if we can properly detect # it based on current location. # # $1: Tag name # # Returns: details content function release_details() { local tag_name="${1}" local proj_root if ! proj_root="$(git rev-parse --show-toplevel)"; then return fi if [ -z "$(git tag -l "${tag_name}")" ] || [ ! -f "${proj_root}/CHANGELOG.md" ]; then return fi printf "CHANGELOG:\n\nhttps://github.com/%s/blob/%s/CHANGELOG.md" "${repository}" "${tag_name}" } # Check if version string is valid for release # # $1: Version # Returns: 0 if valid, 1 if invalid function valid_release_version() { if [[ "${1}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then return 0 else return 1 fi } # Validate arguments for HashiCorp release. Ensures asset # directory exists, and checks that the SHASUMS and SHASUM.sig # files are present. # # $1: Asset directory function hashicorp_release_validate() { local directory="${1}" local sums local sigs # Directory checks debug "checking asset directory was provided" if [ -z "${directory}" ]; then failure "No asset directory was provided for HashiCorp release" fi debug "checking that asset directory exists" if [ ! -d "${directory}" ]; then failure "Asset directory for HashiCorp release does not exist (${directory})" fi # SHASUMS checks debug "checking for shasums file" sums=("${directory}/"*SHA256SUMS) if [ ${#sums[@]} -lt 1 ]; then failure "Asset directory is missing SHASUMS file" fi debug "checking for shasums signature file" sigs=("${directory}/"*SHA256SUMS.sig) if [ ${#sigs[@]} -lt 1 ]; then failure "Asset directory is missing SHASUMS signature file" fi } # Verify release assets by validating checksum properly match # and that signature file is valid # # $1: Asset directory function hashicorp_release_verify() { if [ -z "${HASHICORP_PUBLIC_GPG_KEY_ID}" ]; then failure "Cannot verify release without GPG key ID. Set HASHICORP_PUBLIC_GPG_KEY_ID." fi local directory="${1}" local gpghome pushd "${directory}" # First do a checksum validation debug "validating shasums are correct" wrap shasum -a 256 -c ./*_SHA256SUMS \ "Checksum validation of release assets failed" # Next check that the signature is valid gpghome=$(mktemp -qd) export GNUPGHOME="${gpghome}" debug "verifying shasums signature file using key: %s" "${HASHICORP_PUBLIC_GPG_KEY_ID}" wrap gpg --keyserver keyserver.ubuntu.com --recv "${HASHICORP_PUBLIC_GPG_KEY_ID}" \ "Failed to import HashiCorp public GPG key" wrap gpg --verify ./*SHA256SUMS.sig ./*SHA256SUMS \ "Validation of SHA256SUMS signature failed" rm -rf "${gpghome}" popd } # Generate releases-api metadata # # $1: Product Version # $2: Asset directory function hashicorp_release_generate_release_metadata() { local version="${1}" local directory="${2}" if ! command -v bob; then debug "bob executable not found, installing" install_hashicorp_tool "bob" fi local hc_releases_input_metadata="input-meta.json" # The '-metadata-file' flag expects valid json. Contents are not used for Vagrant. echo "{}" > "${hc_releases_input_metadata}" debug "generating release metadata information" wrap_stream bob generate-release-metadata \ -metadata-file "${hc_releases_input_metadata}" \ -in-dir "${directory}" \ -version "${version}" \ -out-file "${hc_releases_metadata_filename}" \ "Failed to generate release metadata" rm -f "${hc_releases_input_metadata}" } # Upload release metadata and assets to the staging api # # $1: Product Name (e.g. "vagrant") # $2: Product Version # $3: Asset directory function hashicorp_release_upload_to_staging() { local product="${1}" local version="${2}" local directory="${3}" if ! command -v "hc-releases"; then debug "releases-api executable not found, installing" install_hashicorp_tool "releases-api" fi if [ -z "${HC_RELEASES_STAGING_HOST}" ]; then failure "Missing required environment variable HC_RELEASES_STAGING_HOST" fi if [ -z "${HC_RELEASES_STAGING_KEY}" ]; then failure "Missing required environment variable HC_RELEASES_STAGING_KEY" fi export HC_RELEASES_HOST="${HC_RELEASES_STAGING_HOST}" export HC_RELEASES_KEY="${HC_RELEASES_STAGING_KEY}" pushd "${directory}" # Create -file parameter list for hc-releases upload local fileParams=() for file in *; do fileParams+=("-file=${file}") done debug "uploading release assets to staging" wrap_stream hc-releases upload \ -product "${product}" \ -version "${version}" \ "${fileParams[@]}" \ "Failed to upload HashiCorp release assets" popd debug "creating release metadata" wrap_stream hc-releases metadata create \ -product "${product}" \ -input "${hc_releases_metadata_filename}" \ "Failed to create metadata for HashiCorp release" unset HC_RELEASES_HOST unset HC_RELEASES_KEY } # Promote release from staging to production # # $1: Product Name (e.g. "vagrant") # $2: Product Version function hashicorp_release_promote_to_production() { local product="${1}" local version="${2}" if ! command -v "hc-releases"; then debug "releases-api executable not found, installing" install_hashicorp_tool "releases-api" fi if [ -z "${HC_RELEASES_PROD_HOST}" ]; then failure "Missing required environment variable HC_RELEASES_PROD_HOST" fi if [ -z "${HC_RELEASES_PROD_KEY}" ]; then failure "Missing required environment variable HC_RELEASES_PROD_KEY" fi if [ -z "${HC_RELEASES_STAGING_KEY}" ]; then failure "Missing required environment variable HC_RELEASES_STAGING_KEY" fi export HC_RELEASES_HOST="${HC_RELEASES_PROD_HOST}" export HC_RELEASES_KEY="${HC_RELEASES_PROD_KEY}" export HC_RELEASES_SOURCE_ENV_KEY="${HC_RELEASES_STAGING_KEY}" debug "promoting release to production" wrap_stream hc-releases promote \ -product "${product}" \ -version "${version}" \ -source-env staging \ "Failed to promote HashiCorp release to Production" unset HC_RELEASES_HOST unset HC_RELEASES_KEY unset HC_RELEASES_SOURCE_ENV_KEY } # Send the post-publish sns message # # $1: Product name (e.g. "vagrant") defaults to $repo_name # $2: AWS Region of SNS (defaults to us-east-1) function hashicorp_release_sns_publish() { local message local product="${1}" local region="${2}" if [ -z "${product}" ]; then product="${repo_name}" fi if [ -z "${region}" ]; then region="us-east-1" fi # Validate the creds properly assume role and function wrap aws_deprecated configure list \ "Failed to reconfigure AWS credentials for release notification" # Now send the release notification debug "sending release notification to package repository" message=$(jq --null-input --arg product "$product" '{"product": $product}') wrap_stream aws sns publish --region "${region}" --topic-arn "${HC_RELEASES_PROD_SNS_TOPIC}" --message "${message}" \ "Failed to send SNS message for package repository update" return 0 } # Check if a release for the given version # has been published to the HashiCorp # releases site. # # $1: Product Name # $2: Product Version function hashicorp_release_exists() { local product="${1}" local version="${2}" if curl --silent --fail --head "https://releases.hashicorp.com/${product}/${product}_${version}/" > /dev/null ; then debug "hashicorp release of %s@%s found" "${product}" "${version}" return 0 fi debug "hashicorp release of %s@%s not found" "${product}" "${version}" return 1 } # Generate the SHA256SUMS file for assets # in a given directory. # # $1: Asset Directory # $2: Product Name # $3: Product Version function generate_shasums() { local directory="${1}" local product="${2}" local version="${3}" pushd "${directory}" local shacontent debug "generating shasums file for %s@%s" "${product}" "${version}" shacontent="$(shasum -a256 ./*)" || failure "Failed to generate shasums in ${directory}" sed 's/\.\///g' <( printf "%s" "${shacontent}" ) > "${product}_${version}_SHA256SUMS" || failure "Failed to write shasums file" popd } # Generate a HashiCorp releases-api compatible release # # $1: Asset directory # $2: Product Name (e.g. "vagrant") # $3: Product Version function hashicorp_release() { local directory="${1}" local product="${2}" local version="${3}" # If the version is provided, use the discovered release version if [[ "${version}" == "" ]]; then version="${release_version}" fi debug "creating hashicorp release - product: %s version: %s assets: %s" "${product}" "${version}" "${directory}" if ! hashicorp_release_exists "${product}" "${version}"; then # Jump into our artifact directory pushd "${directory}" # If any sig files happen to have been included in here, # just remove them as they won't be using the correct # signing key rm -f ./*.sig # Generate our shasums file debug "generating shasums file for %s@%s" "${product}" "${version}" generate_shasums ./ "${product}" "${version}" # Grab the shasums file and sign it local shasum_files=(./*SHA256SUMS) local shasum_file="${shasum_files[0]}" # Remove relative prefix if found shasum_file="${shasum_file##*/}" debug "signing shasums file for %s@%s" "${product}" "${version}" gpg_sign_file "${shasum_file[0]}" # Jump back out of our artifact directory popd # Run validation and verification on release assets before # we actually do the release. debug "running release validation for %s@%s" "${product}" "${version}" hashicorp_release_validate "${directory}" debug "running release verification for %s@%s" "${product}" "${version}" hashicorp_release_verify "${directory}" # Now that the assets have been validated and verified, # peform the release setps debug "generating release metadata for %s@%s" "${product}" "${version}" hashicorp_release_generate_release_metadata "${version}" "${directory}" debug "uploading release artifacts to staging for %s@%s" "${product}" "${version}" hashicorp_release_upload_to_staging "${product}" "${version}" "${directory}" debug "promoting release to production for %s@%s" "${product}" "${version}" hashicorp_release_promote_to_production "${product}" "${version}" printf "HashiCorp release created (%s@%s)\n" "${product}" "${version}" else printf "hashicorp release not published, already exists (%s@%s)\n" "${product}" "${version}" fi # Send a notification to update the package repositories # with the new release. debug "sending packaging notification for %s@%s" "${product}" "${version}" hashicorp_release_sns_publish "${product}" } # Check if gem version is already published to RubyGems # # $1: Name of RubyGem # $2: Verision of RubyGem # $3: Custom gem server to search (optional) function is_version_on_rubygems() { local name="${1}" local version="${2}" local gemstore="${3}" if [ -z "${name}" ]; then failure "Name is required for version check on %s" "${gemstore:-RubyGems.org}" fi if [ -z "${version}" ]; then failure "Version is required for version check on %s" "${gemstore:-RubyGems.org}" fi debug "checking rubygem %s at version %s is currently published" "${name}" "${version}" local cmd_args=("gem" "search") if [ -n "${gemstore}" ]; then debug "checking rubygem publication at custom source: %s" "${gemstore}" cmd_args+=("--clear-sources" "--source" "${gemstore}") fi cmd_args+=("--remote" "--exact" "--all") local result result="$("${cmd_args[@]}" "${name}")" || failure "Failed to retreive remote version list from RubyGems" local versions="${result##*\(}" local versions="${versions%%)*}" local oifs="${IFS}" IFS=', ' local r=1 for v in $versions; do if [ "${v}" = "${version}" ]; then r=0 debug "rubygem %s at version %s was found" "${name}" "${version}" break fi done IFS="${oifs}" return $r } # Check if gem version is already published to hashigems # # $1: Name of RubyGem # $2: Verision of RubyGem function is_version_on_hashigems() { is_version_on_rubygems "${1}" "${2}" "https://gems.hashicorp.com" } # Build and release project gem to RubyGems function publish_to_rubygems() { if [ -z "${RUBYGEMS_API_KEY}" ]; then failure "RUBYGEMS_API_KEY is required for publishing to RubyGems.org" fi local gem_file="${1}" if [ -z "${gem_file}" ]; then failure "RubyGem file is required for publishing to RubyGems.org" fi if [ ! -f "${gem_file}" ]; then failure "Path provided does not exist or is not a file (%s)" "${gem_file}" fi # NOTE: Newer versions of rubygems support setting the # api key via the GEM_HOST_API_KEY environment # variable. Config file is still used so that older # versions can be used for doing pushes. gem_config="$(mktemp -p ./)" || failure "Could not create gem configuration file" # NOTE: The `--` are required due to the double dash # start of the first argument printf -- "---\n:rubygems_api_key: %s\n" "${RUBYGEMS_API_KEY}" > "${gem_config}" gem push --config-file "${gem_config}" "${gem_file}" || failure "Failed to publish RubyGem at '%s' to RubyGems.org" "${gem_file}" rm -f "${gem_config}" } # Publish gem to the hashigems repository # # $1: Path to gem file to publish function publish_to_hashigems() { local path="${1}" if [ -z "${path}" ]; then failure "Path to built gem required for publishing to hashigems" fi debug "publishing '%s' to hashigems" "${path}" # Define all the variables we'll need local user_bin local reaper local invalid local invalid_id wrap_stream gem install --user-install --no-document reaper-man \ "Failed to install dependency for hashigem generation" user_bin="$(ruby -e 'puts Gem.user_dir')/bin" reaper="${user_bin}/reaper-man" debug "using reaper-man installation at: %s" "${reaper}" # Create a temporary directory to work from local tmpdir tmpdir="$(mktemp -d -p ./)" || failure "Failed to create working directory for hashigems publish" mkdir -p "${tmpdir}/hashigems/gems" || failure "Failed to create gems directory" wrap cp "${path}" "${tmpdir}/hashigems/gems" \ "Failed to copy gem to working directory" pushd "${tmpdir}" # Run quick test to ensure bucket is accessible wrap aws s3 ls "s3://${HASHIGEMS_METADATA_BUCKET}" \ "Failed to access hashigems asset bucket" # Grab our remote metadata. If the file doesn't exist, that is always an error. debug "fetching hashigems metadata file from %s" "${HASHIGEMS_METADATA_BUCKET}" wrap aws s3 cp "s3://${HASHIGEMS_METADATA_BUCKET}/vagrant-rubygems.list" ./ \ "Failed to retrieve hashigems metadata list" # Add the new gem to the metadata file debug "adding new gem to the metadata file" wrap_stream "${reaper}" package add -S rubygems -p vagrant-rubygems.list ./hashigems/gems/*.gem \ "Failed to add new gem to hashigems metadata list" # Generate the repository debug "generating the new hashigems repository content" wrap_stream "${reaper}" repo generate -p vagrant-rubygems.list -o hashigems -S rubygems \ "Failed to generate the hashigems repository" # Upload the updated repository pushd ./hashigems debug "uploading new hashigems repository content to %s" "${HASHIGEMS_PUBLIC_BUCKET}" wrap_stream aws s3 sync . "s3://${HASHIGEMS_PUBLIC_BUCKET}" \ "Failed to upload the hashigems repository" # Store the updated metadata popd debug "uploading updated hashigems metadata file to %s" "${HASHIGEMS_METADATA_BUCKET}" wrap_stream aws s3 cp vagrant-rubygems.list "s3://${HASHIGEMS_METADATA_BUCKET}/vagrant-rubygems.list" \ "Failed to upload the updated hashigems metadata file" # Invalidate cloudfront so the new content is available local invalid debug "invalidating hashigems cloudfront distribution (%s)" "${HASHIGEMS_CLOUDFRONT_ID}" invalid="$(aws cloudfront create-invalidation --distribution-id "${HASHIGEMS_CLOUDFRONT_ID}" --paths "/*")" || failure "Invalidation of hashigems CDN distribution failed" local invalid_id invalid_id="$(printf '%s' "${invalid}" | jq -r ".Invalidation.Id")" if [ -z "${invalid_id}" ]; then failure "Failed to determine the ID of the hashigems CDN invalidation request" fi debug "hashigems cloudfront distribution invalidation identifer - %s" "${invalid_id}" # Wait for the invalidation process to complete debug "starting wait for hashigems cloudfront distribution invalidation to complete (id: %s)" "${invalid_id}" wrap aws cloudfront wait invalidation-completed --distribution-id "${HASHIGEMS_CLOUDFRONT_ID}" --id "${invalid_id}" \ "Failure encountered while waiting for hashigems CDN invalidation request to complete (ID: ${invalid_id})" debug "hashigems cloudfront distribution invalidation complete (id: %s)" "${invalid_id}" # Clean up and we are done popd rm -rf "${tmpdir}" } # Configures git for hashibot usage function hashibot_git() { wrap git config user.name "${HASHIBOT_USERNAME}" \ "Failed to setup git for hashibot usage (username)" wrap git config user.email "${HASHIBOT_EMAIL}" \ "Failed to setup git for hashibot usage (email)" wrap git remote set-url origin "https://${HASHIBOT_USERNAME}:${HASHIBOT_TOKEN}@github.com/${repository}" \ "Failed to setup git for hashibot usage (remote)" } # Get the default branch name for the current repository function default_branch() { local s s="$(git symbolic-ref refs/remotes/origin/HEAD)" || failure "Failed to determine default branch (is working directory git repository?)" printf "%s" "${s##*origin/}" } # Send a notification to slack. All flag values can be set with # environment variables using the upcased name prefixed with SLACK_, # for example: --channel -> SLACK_CHANNEL # # -c --channel CHAN Send to channel # -u --username USER Send as username # -i --icon URL User icon image # -s --state STATE Message state (success, warn, error, or color code) # -m --message MESSAGE Message to send # -M --message-file PATH Use file contents as message # -f --file PATH Send raw contents of file in message (displayed in code block) # -t --title TITLE Message title # -T --tail NUMBER Send last NUMBER lines of content from raw message file # -w --webhook URL Slack webhook function slack() { # Convert any long names to short names for arg in "$@"; do shift case "${arg}" in "--channel") set -- "${@}" "-c" ;; "--username") set -- "${@}" "-u" ;; "--icon") set -- "${@}" "-i" ;; "--state") set -- "${@}" "-s" ;; "--message") set -- "${@}" "-m" ;; "--message-file") set -- "${@}" "-M" ;; "--file") set -- "${@}" "-f" ;; "--title") set -- "${@}" "-t" ;; "--tail") set -- "${@}" "-T" ;; "--webhook") set -- "${@}" "-w" ;; *) set -- "${@}" "${arg}" ;; esac done local OPTIND opt # Default all options to values provided by environment variables local channel="${SLACK_CHANNEL}" local username="${SLACK_USERNAME}" local icon="${SLACK_ICON}" local state="${SLACK_STATE}" local message="${SLACK_MESSAGE}" local message_file="${SLACK_MESSAGE_FILE}" local file="${SLACK_FILE}" local title="${SLACK_TITLE}" local tail="${SLACK_TAIL}" local webhook="${SLACK_WEBHOOK}" while getopts ":c:u:i:s:m:M:f:t:T:w:" opt; do case "${opt}" in "c") channel="${OPTARG}" ;; "u") username="${OPTARG}" ;; "i") icon="${OPTARG}" ;; "s") state="${OPTARG}" ;; "m") message="${OPTARG}" ;; "M") message_file="${OPTARG}" ;; "f") file="${OPTARG}" ;; "t") title="${OPTARG}" ;; "T") tail="${OPTARG}" ;; "w") webhook="${OPTARG}" ;; *) failure "Invalid flag provided to slack" ;; esac done shift $((OPTIND-1)) # If we don't have a webhook provided, stop here if [ -z "${webhook}" ]; then (>&2 echo "ERROR: Cannot send Slack notification, webhook unset") return 1 fi local footer footer_icon ts # If we are using GitHub actions, format the footer if [ -n "${GITHUB_ACTIONS}" ]; then if [ -z "${icon}" ]; then icon="https://ca.slack-edge.com/T024UT03C-WG8NDATGT-f82ae03b9fca-48" fi if [ -z "${username}" ]; then username="GitHub" fi footer_icon="https://ca.slack-edge.com/T024UT03C-WG8NDATGT-f82ae03b9fca-48" footer="Actions - " fi # If no state was provided, default to good state if [ -z "${state}" ]; then state="good" fi # Convert state aliases case "${state}" in "success" | "good") state="good";; "warn" | "warning") state="warning";; "error" | "danger") state="danger";; esac # If we have a message file, read it if [ -n "${message_file}" ]; then local message_file_content message_file_content="$(<"${message_file}")" if [ -z "${message}" ]; then message="${message_file_content}" else message="${message}\n\n${message_file_content}" fi fi # If we have a file to include, add it now. Files are # displayed as raw content, so be sure to wrap with # backticks if [ -n "${file}" ]; then local file_content # If tail is provided, then only include the last n number # of lines in the file if [ -n "${tail}" ]; then if ! file_content="$(tail -n "${tail}" "${file}")"; then file_content="UNEXPECTED ERROR: Failed to tail content in file ${file}" fi else file_content="$(<"${file}")" fi if [ -n "${file_content}" ]; then message="${message}\n\n\`\`\`\n${file_content}\n\`\`\`" fi fi local attach attach_template payload payload_template ts ts="$(date '+%s')" # shellcheck disable=SC2016 attach_template='{text: $msg, color: $state, mrkdwn_in: ["text"], ts: $time' if [ -n "${title}" ]; then # shellcheck disable=SC2016 attach_template+=', title: $title' fi if [ -n "${footer}" ]; then # shellcheck disable=SC2016 attach_template+=', footer: $footer' fi if [ -n "${footer_icon}" ]; then # shellcheck disable=SC2016 attach_template+=', footer_icon: $footer_icon' fi attach_template+='}' attach=$(jq -n \ --arg msg "$(printf "%b" "${message}")" \ --arg title "${title}" \ --arg state "${state}" \ --arg time "${ts}" \ --arg footer "${footer}" \ --arg footer_icon "${footer_icon}" \ "${attach_template}" \ ) # shellcheck disable=SC2016 payload_template='{attachments: [$attachment]' if [ -n "${username}" ]; then # shellcheck disable=SC2016 payload_template+=', username: $username' fi if [ -n "${channel}" ]; then # shellcheck disable=SC2016 payload_template+=', channel: $channel' fi if [ -n "${icon}" ]; then # shellcheck disable=SC2016 payload_template+=', icon_url: $icon' fi payload_template+='}' payload=$(jq -n \ --argjson attachment "${attach}" \ --arg username "${username}" \ --arg channel "${channel}" \ --arg icon "${icon}" \ "${payload_template}" \ ) debug "sending slack message with payload: %s" "${payload}" wrap curl -SsL --fail -X POST -H "Content-Type: application/json" -d "${payload}" "${webhook}" \ "Failed to send slack notification" } # Install internal HashiCorp tools. These tools are expected to # be located in private (though not required) HashiCorp repositories. # It will attempt to download the correct artifact for the current # platform based on HashiCorp naming conventions. It expects that # the name of the repository is the name of the tool. # # $1: Name of repository function install_hashicorp_tool() { local tool_name="${1}" local extensions=("zip" "tar.gz") local asset release_content tmp if [ -z "${tool_name}" ]; then failure "Repository name is required for hashicorp tool install" fi debug "installing hashicorp tool: %s" "${tool_name}" # Swap out repository to force correct github token local repository_bak="${repository}" repository="${repo_owner}/${release_repo}" tmp="$(mktemp -d --tmpdir vagrantci-XXXXXX)" || failure "Failed to create temporary working directory" pushd "${tmp}" local platform platform="$(uname -s)" || failure "Failed to get local platform name" platform="${platform,,}" # downcase the platform name local arches=() local arch arch="$(uname -m)" || failure "Failed to get local platform architecture" arches+=("${arch}") # If the architecture is listed as x86_64, add amd64 to the # arches collection. Hashicorp naming scheme is to use amd64 in # the file name, but isn't always followed if [ "${arch}" = "x86_64" ]; then arches+=("amd64") fi release_content=$(github_request -H "Content-Type: application/json" \ "https://api.github.com/repos/hashicorp/${tool_name}/releases/latest") || failure "Failed to request latest releases for hashicorp/${tool_name}" local exten for exten in "${extensions[@]}"; do for arch in "${arches[@]}"; do local suffix="${platform}_${arch}.${exten}" debug "checking for release artifact with suffix: %s" "${suffix}" asset=$(printf "%s" "${release_content}" | jq -r \ '.assets[] | select(.name | contains("'"${suffix}"'")) | .url') if [ -n "${asset}" ]; then debug "release artifact found: %s" "${asset}" break fi done if [ -n "${asset}" ]; then break fi done if [ -z "${asset}" ]; then failure "Failed to find release of hashicorp/${tool_name} for ${platform} ${arch[0]}" fi debug "tool artifact match found for install: %s" "${asset}" github_request -o "${tool_name}.${exten}" \ -H "Accept: application/octet-stream" "${asset}" || "Failed to download latest release for hashicorp/${tool_name}" if [ "${exten}" = "zip" ]; then wrap unzip "${tool_name}.${exten}" \ "Failed to unpack latest release for hashicorp/${tool_name}" else wrap tar xf "${tool_name}.${exten}" \ "Failed to unpack latest release for hashicorp/${tool_name}" fi rm -f "${tool_name}.${exten}" local files=( ./* ) wrap chmod 0755 ./* \ "Failed to change mode on latest release for hashicorp/${tool_name}" wrap mv ./* "${ci_bin_dir}" \ "Failed to install latest release for hashicorp/${tool_name}" debug "new files added to path: %s" "${files[*]}" popd rm -rf "${tmp}" repository="${repository_bak}" # restore the repository value } # Install tool from GitHub releases. It will fetch the latest release # of the tool and install it. The proper release artifact will be matched # by a "linux_amd64" string. This command is best effort and may not work. # # $1: Organization name # $2: Repository name # $3: Tool name (optional) function install_github_tool() { local org_name="${1}" local r_name="${2}" local tool_name="${3}" if [ -z "${tool_name}" ]; then tool_name="${r_name}" fi local asset release_content tmp local artifact_list artifact basen tmp="$(mktemp -d --tmpdir vagrantci-XXXXXX)" || failure "Failed to create temporary working directory" pushd "${tmp}" debug "installing github tool %s from %s/%s" "${tool_name}" "${org_name}" "${r_name}" release_content=$(github_request -H "Content-Type: application/json" \ "https://api.github.com/repos/${org_name}/${r_name}/releases/latest") || failure "Failed to request latest releases for ${org_name}/${r_name}" asset=$(printf "%s" "${release_content}" | jq -r \ '.assets[] | select( ( (.name | contains("amd64")) or (.name | contains("x86_64")) or (.name | contains("x86-64")) ) and (.name | contains("linux")) and (.name | endswith("sha256") | not) and (.name | endswith("sig") | not)) | .url') || failure "Failed to detect latest release for ${org_name}/${r_name}" artifact="${asset##*/}" github_request -o "${artifact}" -H "Accept: application/octet-stream" "${asset}" || "Failed to download latest release for ${org_name}/${r_name}" basen="${artifact##*.}" if [ "${basen}" = "zip" ]; then wrap unzip "${artifact}" \ "Failed to unpack latest release for ${org_name}/${r_name}" rm -f "${artifact}" elif [ -n "${basen}" ]; then wrap tar xf "${artifact}" \ "Failed to unpack latest release for ${org_name}/${r_name}" rm -f "${artifact}" fi artifact_list=(./*) # If the artifact only contained a directory, get # the contents of the directory if [ "${#artifact_list[@]}" -eq "1" ] && [ -d "${artifact_list[0]}" ]; then debug "unpacked artifact contained only directory, inspecting contents" artifact_list=( "${artifact_list[0]}/"* ) fi local tool_match tool_glob_match executable_match local item for item in "${artifact_list[@]}"; do if [ "${item##*/}" = "${tool_name}" ]; then debug "tool name match found: %s" "${item}" tool_match="${item}" elif [ -e "${item}" ]; then debug "executable match found: %s" "${item}" executable_match="${item}" elif [[ "${item}" = "${tool_name}"* ]]; then debug "tool name glob match found: %s" "${item}" tool_glob_match="${item}" fi done # Install based on best match to worst match if [ -n "${tool_match}" ]; then debug "installing %s from tool name match (%s)" "${tool_name}" "${tool_match}" mv -f "${tool_match}" "${ci_bin_dir}/${tool_name}" || "Failed to install latest release of %s from %s/%s" "${tool_name}" "${org_name}" "${r_name}" elif [ -n "${tool_glob_match}" ]; then debug "installing %s from tool name glob match (%s)" "${tool_name}" "${tool_glob_match}" mv -f "${tool_glob_match}" "${ci_bin_dir}/${tool_name}" || "Failed to install latest release of %s from %s/%s" "${tool_name}" "${org_name}" "${r_name}" elif [ -n "${executable_match}" ]; then debug "installing %s from executable file match (%s)" "${tool_name}" "${executable_match}" mv -f "${executable_match}" "${ci_bin_dir}/${tool_name}" || "Failed to install latest release of %s from %s/%s" "${tool_name}" "${org_name}" "${r_name}" else failure "Failed to locate tool '%s' in latest release from %s/%s" "${org_name}" "${r_name}" fi popd rm -rf "${tmp}" } # Prepare host for packet use. It will validate the # required environment variables are set, ensure # packet-exec is installed, and setup the SSH key. function packet-setup() { # First check that we have the environment variables if [ -z "${PACKET_EXEC_TOKEN}" ]; then failure "Cannot setup packet, missing token" fi if [ -z "${PACKET_EXEC_PROJECT_ID}" ]; then failure "Cannot setup packet, missing project" fi if [ -z "${PACKET_SSH_KEY_CONTENT}" ]; then failure "Cannot setup packet, missing ssh key" fi install_hashicorp_tool "packet-exec" # Write the ssh key to disk local content content="$(base64 --decode - <<< "${PACKET_SSH_KEY_CONTENT}")" || failure "Cannot setup packet, failed to decode key" touch ./packet-key chmod 0600 ./packet-key printf "%s" "${content}" > ./packet-key local working_directory working_directory="$(pwd)" || failure "Cannot setup packet, failed to determine working directory" export PACKET_EXEC_SSH_KEY="${working_directory}/packet-key" } # Download artifact(s) from GitHub release. The artifact pattern is simply # a substring that is matched against the artifact download URL. Artifact(s) # will be downloaded to the working directory. # # $1: repository name # $2: release tag name # $3: artifact pattern (optional, all artifacts downloaded if omitted) function github_release_assets() { local req_args req_args=() local asset_pattern local release_repo="${1}" local release_name="${2}" local asset_pattern="${3}" # Swap out repository to force correct github token local repository_bak="${repository}" repository="${repo_owner}/${release_repo}" req_args+=("-H" "Accept: application/vnd.github+json") req_args+=("https://api.github.com/repos/${repository}/releases/tags/${release_name}") debug "fetching release asset list for release %s on %s" "${release_name}" "${repository}" local release_content release_content=$(github_request "${req_args[@]}") || failure "Failed to request release (${release_name}) for ${repository}" local query=".assets[]" if [ -n "${asset_pattern}" ]; then debug "applying release asset list filter %s" "${asset_pattern}" query+="$(printf ' | select(.name | contains("%s"))' "${asset_pattern}")" fi local asset_list asset_list=$(printf "%s" "${release_content}" | jq -r "${query} | .url") || failure "Failed to detect asset in release (${release_name}) for ${release_repo}" local name_list name_list=$(printf "%s" "${release_content}" | jq -r "${query} | .name") || failure "Failed to detect asset in release (${release_name}) for ${release_repo}" req_args=() req_args+=("-H" "Accept: application/octet-stream") local assets asset_names readarray -t assets < <(printf "%s" "${asset_list}") readarray -t asset_names < <(printf "%s" "${name_list}") local idx for ((idx=0; idx<"${#assets[@]}"; idx++ )); do local asset="${assets[$idx]}" local artifact="${asset_names[$idx]}" github_request "${req_args[@]}" -o "${artifact}" "${asset}" || "Failed to download asset (${artifact}) in release ${release_name} for ${repository}" printf "downloaded release asset %s from release %s on %s\n" "${artifact}" "${release_name}" "${repository}" done repository="${repository_bak}" # restore the repository value } # Basic helper to create a GitHub prerelease # # $1: repository name # $2: tag name for release # $3: path to artifact(s) - single file or directory function github_prerelease() { local prerelease_repo="${1}" local tag_name="${2}" local artifacts="${3}" if [ -z "${prerelease_repo}" ]; then failure "Name of repository required for prerelease release" fi if [ -z "${tag_name}" ]; then failure "Name is required for prerelease release" fi if [ -z "${artifacts}" ]; then failure "Artifacts path is required for prerelease release" fi if [ ! -e "${artifacts}" ]; then failure "No artifacts found at provided path (${artifacts})" fi local prerelease_target="${repo_owner}/${prerelease_repo}" # Create the prerelease local response response="$(github_create_release -p -t "${tag_name}" -o "${repo_owner}" -r "${prerelease_repo}" )" || failure "Failed to create prerelease on %s/%s" "${repo_owner}" "${prerelease_repo}" # Extract the release ID from the response local release_id release_id="$(printf "%s" "${response}" | jq -r '.id')" || failure "Failed to extract prerelease ID from response for ${tag_name} on ${prerelease_target}" github_upload_release_artifacts "${prerelease_repo}" "${release_id}" "${artifacts}" } # Upload artifacts to a release # # $1: target repository name # $2: release ID # $3: path to artifact(s) - single file or directory function github_upload_release_artifacts() { local target_repo_name="${1}" local release_id="${2}" local artifacts="${3}" if [ -z "${target_repo_name}" ]; then failure "Repository name required for release artifact upload" fi if [ -z "${release_id}" ]; then failure "Release ID require for release artifact upload" fi if [ -z "${artifacts}" ]; then failure "Artifacts required for release artifact upload" fi if [ ! -e "${artifacts}" ]; then failure "No artifacts found at provided path for release artifact upload (%s)" "${artifacts}" fi # Swap out repository to force correct github token local repository_bak="${repository}" repository="${repo_owner}/${target_repo_name}" local req_args=("-X" "POST" "-H" "Content-Type: application/octet-stream") # Now upload the artifacts to the draft release local artifact_name if [ -f "${artifacts}" ]; then debug "uploading %s to release ID %s on %s" "${artifact}" "${release_id}" "${repository}" artifact_name="${artifacts##*/}" req_args+=("https://uploads.github.com/repos/${repository}/releases/${release_id}/assets?name=${artifact_name}" "--data-binary" "@${artifacts}") if ! github_request "${req_args[@]}" > /dev/null ; then failure "Failed to upload artifact '${artifacts}' to draft release on ${repository}" fi printf "Uploaded release artifact: %s\n" "${artifact_name}" >&2 # Everything is done so get on outta here return 0 fi # Push into the directory pushd "${artifacts}" local artifact_path # Walk through each item and upload for artifact_path in * ; do if [ ! -f "${artifact_path}" ]; then debug "skipping '%s' as it is not a file" "${artifact_path}" continue fi artifact_name="${artifact_path##*/}" debug "uploading %s/%s to release ID %s on %s" "${artifacts}" "${artifact_name}" "${release_id}" "${repository}" local r_args=( "${req_args[@]}" ) r_args+=("https://uploads.github.com/repos/${repository}/releases/${release_id}/assets?name=${artifact_name}" "--data-binary" "@${artifact_path}") if ! github_request "${r_args[@]}" > /dev/null ; then failure "Failed to upload artifact '${artifact_name}' in '${artifacts}' to draft release on ${repository}" fi printf "Uploaded release artifact: %s\n" "${artifact_name}" >&2 done repository="${repository_bak}" } # Basic helper to create a GitHub draft release # # $1: repository name # $2: tag name for release # $3: path to artifact(s) - single file or directory function github_draft_release() { local draft_repo="${1}" local tag_name="${2}" local artifacts="${3}" if [ -z "${draft_repo}" ]; then failure "Name of repository required for draft release" fi if [ -z "${tag_name}" ]; then failure "Name is required for draft release" fi if [ -z "${artifacts}" ]; then failure "Artifacts path is required for draft release" fi if [ ! -e "${artifacts}" ]; then failure "No artifacts found at provided path (%s)" "${artifacts}" fi # Create the draft release local response response="$(github_create_release -d -t "${tag_name}" -o "${repo_owner}" -r "${draft_repo}" )" || failure "Failed to create draft release on %s" "${repo_owner}/${draft_repo}" # Extract the release ID from the response local release_id release_id="$(printf "%s" "${response}" | jq -r '.id')" || failure "Failed to extract draft release ID from response for %s on %s" "${tag_name}" "${repo_owner}/${draft_repo}" github_upload_release_artifacts "${draft_repo}" "${release_id}" "${artifacts}" } # Create a GitHub release # # -b BODY - body of release # -c COMMITISH - commitish of release # -n NAME - name of the release # -o OWNER - repository owner (required) # -r REPO - repository name (required) # -t TAG_NAME - tag name for release (required) # -d - draft release # -p - prerelease # -g - generate release notes # -m - make release latest # # NOTE: Artifacts for release must be uploaded using `github_upload_release_artifacts` function github_create_release() { local OPTIND opt owner repo tag_name # Values that can be null local body commitish name # Values we default local draft="false" local generate_notes="false" local make_latest="false" local prerelease="false" while getopts ":b:c:n:o:r:t:dpgm" opt; do case "${opt}" in "b") body="${OPTARG}" ;; "c") commitish="${OPTARG}" ;; "n") name="${OPTARG}" ;; "o") owner="${OPTARG}" ;; "r") repo="${OPTARG}" ;; "t") tag_name="${OPTARG}" ;; "d") draft="true" ;; "p") prerelease="true" ;; "g") generate_notes="true" ;; "m") make_latest="true" ;; *) failure "Invalid flag provided to github_create_release" ;; esac done shift $((OPTIND-1)) # Sanity check if [ -z "${owner}" ]; then failure "Repository owner value is required for GitHub release" fi if [ -z "${repo}" ]; then failure "Repository name is required for GitHub release" fi if [ -z "${tag_name}" ] && [ "${draft}" != "true" ]; then failure "Tag name is required for GitHub release" fi if [ "${draft}" = "true" ] && [ "${prerelease}" = "true" ]; then failure "Release cannot be both draft and prerelease" fi # If no name is provided, use the tag name value if [ -z "${name}" ]; then name="${tag_name}" fi # shellcheck disable=SC2016 local payload_template='{tag_name: $tag_name, draft: $draft, prerelease: $prerelease, generate_release_notes: $generate_notes, make_latest: $make_latest' local jq_args=("-n" "--arg" "tag_name" "${tag_name}" "--arg" "make_latest" "${make_latest}" "--argjson" "draft" "${draft}" "--argjson" "generate_notes" "${generate_notes}" "--argjson" "prerelease" "${prerelease}" ) if [ -n "${commitish}" ]; then # shellcheck disable=SC2016 payload_template+=', target_commitish: $commitish' jq_args+=("--arg" "commitish" "${commitish}") fi if [ -n "${name}" ]; then # shellcheck disable=SC2016 payload_template+=', name: $name' jq_args+=("--arg" "name" "${name}") fi if [ -n "${body}" ]; then # shellcheck disable=SC2016 payload_template+=', body: $body' jq_args+=("--arg" "body" "${body}") fi payload_template+='}' # Generate the payload local payload payload="$(jq "${jq_args[@]}" "${payload_template}" )" || failure "Could not generate GitHub release JSON payload" local target_repo="${owner}/${repo}" # Set repository to get correct token behavior on request local repository_bak="${repository}" repository="${target_repo}" # Craft our request arguments local req_args=("-X" "POST" "https://api.github.com/repos/${target_repo}/releases" "-d" "${payload}") # Create the draft release local response if ! response="$(github_request "${req_args[@]}")"; then failure "Could not create github release on ${target_repo}" fi # Restore the repository repository="${repository_bak}" local rel_type if [ "${draft}" = "true" ]; then rel_type="draft release" elif [ "${prerelease}" = "true" ]; then rel_type="prerelease" else rel_type="release" fi # Report new draft release was created printf "New %s '%s' created on '%s'\n" "${rel_type}" "${tag_name}" "${target_repo}" >&2 # Print the response printf "%s" "${response}" } # Check if a github release exists by tag name # NOTE: This can be used for release and prerelease checks. # Draft releases must use the github_draft_release_exists # function. # # $1: repository name # $2: release tag name function github_release_exists() { local release_repo="${1}" local release_name="${2}" if [ -z "${release_repo}" ]; then failure "Repository name required for release lookup" fi if [ -z "${release_name}" ]; then failure "Release name required for release lookup" fi # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${release_repo}" local result="1" if github_request \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${repository}/releases/tags/${release_name}" > /dev/null; then debug "release '${release_name}' found in ${repository}" result="0" else debug "release '${release_name}' not found in ${repository}" fi # Restore repository value repository="${repository_bak}" return "${result}" } # Check if a github release exists using fuzzy match # # $1: repository name # $2: release name function github_release_exists_fuzzy() { local release_repo="${1}" local release_name="${2}" if [ -z "${release_repo}" ]; then failure "Repository name required for draft release lookup" fi if [ -z "${release_name}" ]; then failure "Release name required for draft release lookup" fi # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${release_repo}" local page=$((1)) local matched_name while [ -z "${release_content}" ]; do local release_list release_list="$(github_request \ -H "Content-Type: application/json" \ "https://api.github.com/repos/${repository}/releases?per_page=100&page=${page}")" || failure "Failed to request releases list for ${repository}" # If there's no more results, just bust out of the loop if [ "$(jq 'length' <( printf "%s" "${release_list}" ))" -lt "1" ]; then break fi local names name_list n matched_name name_list="$(printf "%s" "${release_list}" | jq '.[] | .name')" || failure "Could not generate name list" # shellcheck disable=SC2206 names=( $name_list ) for n in "${names[@]}"; do if [[ "${n}" =~ $release_name ]]; then matched_name="${n}" break fi done if [ -n "${matched_name}" ]; then break fi ((page++)) done # Restore the $repository value repository="${repository_bak}" if [ -z "${matched_name}" ]; then debug "did not locate release named %s for %s" "${release_name}" "${repo_owner}/${release_repo}" return 1 fi debug "found release name %s in %s (pattern: %s)" "${matched_name}" "${repo_owner}/${release_repo}" "${release_name}" return 0 } # Check if a draft release exists by name # # $1: repository name # $2: release name function github_draft_release_exists() { local release_repo="${1}" local release_name="${2}" if [ -z "${release_repo}" ]; then failure "Repository name required for draft release lookup" fi if [ -z "${release_name}" ]; then failure "Release name required for draft release lookup" fi # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${release_repo}" local page=$((1)) local release_content while [ -z "${release_content}" ]; do local release_list release_list="$(github_request \ -H "Content-Type: application/json" \ "https://api.github.com/repos/${repository}/releases?per_page=100&page=${page}")" || failure "Failed to request releases list for ${repository}" # If there's no more results, just bust out of the loop if [ "$(jq 'length' <( printf "%s" "${release_list}" ))" -lt "1" ]; then break fi query="$(printf '.[] | select(.name == "%s")' "${release_name}")" release_content=$(printf "%s" "${release_list}" | jq -r "${query}") ((page++)) done # Restore the $repository value repository="${repository_bak}" if [ -z "${release_content}" ]; then debug "did not locate draft release named %s for %s" "${release_name}" "${repo_owner}/${release_repo}" return 1 fi debug "found draft release name %s in %s" "${release_name}" "${repo_owner}/${release_repo}" return 0 } # Download artifact(s) from GitHub draft release. A draft release is not # attached to a tag and therefore is referenced by the release name directly. # The artifact pattern is simply a substring that is matched against the # artifact download URL. Artifact(s) will be downloaded to the working directory. # # $1: repository name # $2: release name # $3: artifact pattern (optional, all artifacts downloaded if omitted) function github_draft_release_assets() { local release_repo_name="${1}" local release_name="${2}" local asset_pattern="${3}" if [ -z "${release_repo_name}" ]; then failure "Repository name is required for draft release asset fetching" fi if [ -z "${release_name}" ]; then failure "Draft release name is required for draft release asset fetching" fi # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${release_repo_name}" local page=$((1)) local release_content query while [ -z "${release_content}" ]; do local release_list release_list=$(github_request -H "Content-Type: application/json" \ "https://api.github.com/repos/${repository}/releases?per_page=100&page=${page}") || failure "Failed to request releases list for ${repository}" # If there's no more results, just bust out of the loop if [ "$(jq 'length' <( printf "%s" "${release_list}" ))" -lt "1" ]; then debug "did not locate draft release named %s in %s" "${release_name}" "${repository}" break fi query="$(printf '.[] | select(.name == "%s")' "${release_name}")" release_content=$(printf "%s" "${release_list}" | jq -r "${query}") ((page++)) done query=".assets[]" if [ -n "${asset_pattern}" ]; then debug "apply pattern filter to draft assets: %s" "${asset_pattern}" query+="$(printf ' | select(.name | contains("%s"))' "${asset_pattern}")" fi local asset_list asset_list=$(printf "%s" "${release_content}" | jq -r "${query} | .url") || failure "Failed to detect asset in release (${release_name}) for ${repository}" local name_list name_list=$(printf "%s" "${release_content}" | jq -r "${query} | .name") || failure "Failed to detect asset in release (${release_name}) for ${repository}" debug "draft release assets list: %s" "${name_list}" local assets asset_names readarray -t assets < <(printf "%s" "${asset_list}") readarray -t asset_names < <(printf "%s" "${name_list}") if [ "${#assets[@]}" -ne "${#asset_names[@]}" ]; then failure "Failed to match download assets with names in release list for ${repository}" fi local idx for ((idx=0; idx<"${#assets[@]}"; idx++ )); do local asset="${assets[$idx]}" local artifact="${asset_names[$idx]}" github_request -o "${artifact}" \ -H "Accept: application/octet-stream" "${asset}" || "Failed to download asset in release (${release_name}) for ${repository} - ${artifact}" printf "downloaded draft release asset at %s\n" "${artifact}" >&2 done repository_bak="${repository}" # restore repository value } # This function is identical to the github_draft_release_assets # function above with one caveat: it does not download the files. # Each file that would be downloaded is simply touched in the # current directory. This provides an easy way to check the # files that would be downloaded without actually downloading # them. # # An example usage of this can be seen in the vagrant package # building where we use this to enable building missing substrates # or packages on re-runs and only download the artifacts if # actually needed. function github_draft_release_asset_names() { local release_reponame="${1}" local release_name="${2}" local asset_pattern="${3}" if [ -z "${release_reponame}" ]; then failure "Repository name is required for draft release assets names" fi if [ -z "${release_name}" ]; then failure "Release name is required for draft release asset names" fi # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${release_reponame}" local page=$((1)) local release_content query while [ -z "${release_content}" ]; do local release_list release_list=$(github_request H "Content-Type: application/json" \ "https://api.github.com/repos/${repository}/releases?per_page=100&page=${page}") || failure "Failed to request releases list for ${repository}" # If there's no more results, just bust out of the loop if [ "$(jq 'length' <( printf "%s" "${release_list}" ))" -lt "1" ]; then debug "did not locate draft release named %s in %s" "${release_name}" "${repository}" break fi query="$(printf '.[] | select(.name == "%s")' "${release_name}")" release_content=$(printf "%s" "${release_list}" | jq -r "${query}") ((page++)) done query=".assets[]" if [ -n "${asset_pattern}" ]; then debug "apply pattern filter to draft assets: %s" "${asset_pattern}" query+="$(printf ' | select(.name | contains("%s"))' "${asset_pattern}")" fi local name_list name_list=$(printf "%s" "${release_content}" | jq -r "${query} | .name") || failure "Failed to detect asset in release (${release_name}) for ${repository}" debug "draft release assets list: %s" "${name_list}" local asset_names readarray -t asset_names < <(printf "%s" "${name_list}") local idx for ((idx=0; idx<"${#asset_names[@]}"; idx++ )); do local artifact="${asset_names[$idx]}" touch "${artifact}" || failure "Failed to touch release asset at path: %s" "${artifact}" printf "touched draft release asset at %s\n" "${artifact}" >&2 done repository_bak="${repository}" # restore repository value } # Delete a github release by tag name # NOTE: Releases and prereleases can be deleted using this # function. For draft releases use github_delete_draft_release # # $1: tag name of release # $2: repository name (optional, defaults to current repository name) function github_delete_release() { local release_name="${1}" local release_repo="${2:-$repo_name}" if [ -z "${release_name}" ]; then failure "Release name is required for deletion" fi if [ -z "${release_repo}" ]; then failure "Repository is required for release deletion" fi # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${release_repo}" # Fetch the release first local release_content release_content="$(github_request \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${repository}/releases/tags/${release_name}")" || failure "Failed to fetch release information for '${release_name}' in ${repository}" # Get the release id to reference in delete request local rel_id rel_id="$(jq -r '.id' <( printf "%s" "${release_content}" ) )" || failure "Failed to read release id for '${release_name}' in ${repository}" debug "deleting github release '${release_name}' in ${repository} with id ${rel_id}" # Send the deletion request github_request \ -X "DELETE" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${repository}/releases/${rel_id}" > /dev/null || failure "Failed to delete release '${release_name}' in ${repository}" # Restore repository value repository="${repository_bak}" } # Delete draft release with given name # # $1: name of draft release # $2: repository name (optional, defaults to current repository name) function github_delete_draft_release() { local draft_name="${1}" local delete_repo="${2:-$repo_name}" if [ -z "${draft_name}" ]; then failure "Draft name is required for deletion" fi if [ -z "${delete_repo}" ]; then failure "Repository is required for draft deletion" fi # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${delete_repo}" local draft_ids=() local page=$((1)) while true; do local release_list list_length release_list=$(github_request -H "Content-Type: application/json" \ "https://api.github.com/repos/${repository}/releases?per_page=100&page=${page}") || failure "Failed to request releases list for draft deletion on ${repository}" list_length="$(jq 'length' <( printf "%s" "${release_list}" ))" || failure "Failed to calculate release length for draft deletion on ${repository}" # If the list is empty then there are no more releases to process if [ -z "${list_length}" ] || [ "${list_length}" -lt 1 ]; then debug "no releases returned for page %d in repository %s" "${page}" "${repository}" break fi local entry i release_draft release_id release_name for (( i=0; i < "${list_length}"; i++ )); do entry="$(jq ".[$i]" <( printf "%s" "${release_list}" ))" || failure "Failed to read entry for draft deletion on ${repository}" release_draft="$(jq -r '.draft' <( printf "%s" "${entry}" ))" || failure "Failed to read entry draft for draft deletion on ${repository}" release_id="$(jq -r '.id' <( printf "%s" "${entry}" ))" || failure "Failed to read entry ID for draft deletion on ${repository}" release_name="$(jq -r '.name' <( printf "%s" "${entry}" ))" || failure "Failed to read entry name for draft deletion on ${repository}" # If the names don't match, skip if [ "${release_name}" != "${draft_name}" ]; then debug "skipping release deletion, name mismatch (%s != %s)" "${release_name}" "${draft_name}" continue fi # If the release is not a draft, fail if [ "${release_draft}" != "true" ]; then debug "skipping release '%s' (ID: %s) from '%s' - release is not a draft" "${draft_name}" "${release_id}" "${repository}" continue fi # If we are here, we found a match draft_ids+=( "${release_id}" ) done ((page++)) done # If no draft ids were found, the release was not found # so we can just return success if [ "${#draft_ids[@]}" -lt "1" ]; then debug "no draft releases found matching name %s in %s" "${draft_name}" "${repository}" repository="${repository_bak}" # restore repository value before return return 0 fi # Still here? Okay! Delete the draft(s) local draft_id for draft_id in "${draft_ids[@]}"; do info "Deleting draft release %s from %s (ID: %d)\n" "${draft_name}" "${repository}" "${draft_id}" github_request -X DELETE "https://api.github.com/repos/${repository}/releases/${draft_id}" || failure "Failed to prune draft release ${draft_name} from ${repository}" done repository="${repository_bak}" # restore repository value before return } # Delete prerelease with given name # # $1: tag name of prerelease # $2: repository name (optional, defaults to current repository name) function github_delete_prerelease() { local tag_name="${1}" local delete_repo="${2:-$repo_name}" if [ -z "${tag_name}" ]; then failure "Tag name is required for deletion" fi if [ -z "${delete_repo}" ]; then failure "Repository is required for prerelease deletion" fi # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${delete_repo}" local prerelease prerelease=$(github_request -H "Content-Type: application/vnd.github+json" \ "https://api.github.com/repos/${repository}/releases/tags/${tag_name}") || failure "Failed to get prerelease %s from %s" "${tag_name}" "${repository}" local prerelease_id prerelease_id="$(jq -r '.id' <( printf "%s" "${prerelease}" ))" || failure "Failed to read prerelease ID for %s on %s" "${tag_name}" "${repository}" local is_prerelease is_prerelease="$(jq -r '.prerelease' <( printf "%s" "${prerelease}" ))" || failure "Failed to read prerelease status for %s on %s" "${tag_name}" "${repository}" # Validate the matched release is a prerelease if [ "${is_prerelease}" != "true" ]; then failure "Prerelease %s on %s is not marked as a prerelease, cannot delete" "${tag_name}" "${repository}" fi info "Deleting prerelease %s from repository %s" "${tag_name}" "${repository}" github_request -X DELETE "https://api.github.com/repos/${repository}/releases/${prerelease_id}" || failure "Failed to delete prerelease %s from %s" "${tag_name}" "${repository}" repository="${repository_bak}" # restore repository value before return } # Delete any draft releases that are older than the # given number of days # # $1: days # $2: repository name (optional, defaults to current repository name) function github_draft_release_prune() { github_release_prune "draft" "${@}" } # Delete any prereleases that are older than the # given number of days # # $1: days # $2: repository name (optional, defaults to current repository name) function github_prerelease_prune() { github_release_prune "prerelease" "${@}" } # Delete any releases of provided type that are older than the # given number of days # # $1: type (prerelease or draft) # $2: days # $3: repository name (optional, defaults to current repository name) function github_release_prune() { local prune_type="${1}" if [ -z "${prune_type}" ]; then failure "Type is required for release pruning" fi if [ "${prune_type}" != "draft" ] && [ "${prune_type}" != "prerelease" ]; then failure "Invalid release pruning type provided '%s' (supported: draft or prerelease)" "${prune_type}" fi local days="${2}" if [ -z "${days}" ]; then failure "Number of days to retain is required for pruning" fi if [[ "${days}" = *[!0123456789]* ]]; then failure "Invalid value provided for days to retain when pruning (%s)" "${days}" fi local prune_repo="${3:-$repo_name}" if [ -z "${prune_repo}" ]; then failure "Repository name is required for pruning" fi local prune_seconds now now="$(date '+%s')" prune_seconds=$(("${now}"-("${days}" * 86400))) # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${prune_repo}" debug "deleting %ss over %d days old from %s" "${prune_type}" "${days}" "${repository}" local page=$((1)) while true; do local release_list list_length release_list=$(github_request -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${repository}/releases?per_page=100&page=${page}") || failure "Failed to request releases list for pruning on ${repository}" list_length="$(jq 'length' <( printf "%s" "${release_list}" ))" || failure "Failed to calculate release length for pruning on ${repository}" if [ -z "${list_length}" ] || [ "${list_length}" -lt "1" ]; then debug "releases listing page %d for %s is empty" "${page}" "${repository}" break fi local entry i release_type release_name release_id release_create date_check for (( i=0; i < "${list_length}"; i++ )); do entry="$(jq ".[${i}]" <( printf "%s" "${release_list}" ))" || failure "Failed to read entry for pruning on %s" "${repository}" release_type="$(jq -r ".${prune_type}" <( printf "%s" "${entry}" ))" || failure "Failed to read entry %s for pruning on %s" "${prune_type}" "${repository}" release_name="$(jq -r '.name' <( printf "%s" "${entry}" ))" || failure "Failed to read entry name for pruning on %s" "${repository}" release_id="$(jq -r '.id' <( printf "%s" "${entry}" ))" || failure "Failed to read entry ID for pruning on %s" "${repository}" release_create="$(jq -r '.created_at' <( printf "%s" "${entry}" ))" || failure "Failed to read entry created date for pruning on %s" "${repository}" date_check="$(date --date="${release_create}" '+%s')" || failure "Failed to parse entry created date for pruning on %s" "${repository}" if [ "${release_type}" != "true" ]; then debug "Skipping %s on %s because release is not a %s" "${release_name}" "${repository}" "${prune_type}" continue fi if [ "$(( "${date_check}" ))" -lt "${prune_seconds}" ]; then info "Deleting release %s from %s\n" "${release_name}" "${prune_repo}" github_request -X DELETE "https://api.github.com/repos/${repository}/releases/${release_id}" || failure "Failed to prune %s %s from %s" "${prune_type}" "${release_name}" "${repository}" fi done ((page++)) done repository="${repository_bak}" # restore the repository value } # Delete all but the latest N number of releases of the provided type # # $1: type (prerelease or draft) # $2: number of releases to retain # $3: repository name (optional, defaults to current repository name) function github_release_prune_retain() { local prune_type="${1}" if [ -z "${prune_type}" ]; then failure "Type is required for release pruning" fi if [ "${prune_type}" != "draft" ] && [ "${prune_type}" != "prerelease" ]; then failure "Invalid release pruning type provided '%s' (supported: draft or prerelease)" "${prune_type}" fi local retain="${2}" if [ -z "${retain}" ]; then failure "Number of releases to retain is required for pruning" fi if [[ "${retain}" = *[!0123456789]* ]]; then failure "Invalid value provided for number of releases to retain when pruning (%s)" "${days}" fi local prune_repo="${3:-$repo_name}" if [ -z "${prune_repo}" ]; then failure "Repository name is required for pruning" fi # Override repository value to get correct token automatically local repository_bak="${repository}" repository="${repo_owner}/${prune_repo}" debug "pruning all %s type releases except latest %d releases" "${prune_type}" "${retain}" local prune_list=() local page=$((1)) while true; do local release_list list_length release_list=$(github_request -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${repository}/releases?per_page=100&page=${page}&sort=created_at&direction=desc") || failure "Failed to request releases list for pruning on ${repository}" list_length="$(jq 'length' <( printf "%s" "${release_list}" ))" || failure "Failed to calculate release length for pruning on ${repository}" if [ -z "${list_length}" ] || [ "${list_length}" -lt "1" ]; then debug "releases listing page %d for %s is empty" "${page}" "${repository}" break fi local entry i release_type release_name release_id release_create date_check for (( i=0; i < "${list_length}"; i++ )); do entry="$(jq ".[${i}]" <( printf "%s" "${release_list}" ))" || failure "Failed to read entry for pruning on %s" "${repository}" release_type="$(jq -r ".${prune_type}" <( printf "%s" "${entry}" ))" || failure "Failed to read entry %s for pruning on %s" "${prune_type}" "${repository}" release_name="$(jq -r '.name' <( printf "%s" "${entry}" ))" || failure "Failed to read entry name for pruning on %s" "${repository}" release_id="$(jq -r '.id' <( printf "%s" "${entry}" ))" || failure "Failed to read entry ID for pruning on %s" "${repository}" if [ "${release_type}" != "true" ]; then debug "Skipping %s on %s because release is not a %s" "${release_name}" "${repository}" "${prune_type}" continue fi debug "adding %s '%s' to prune list (ID: %s)" "${prune_type}" "${release_name}" "${release_id}" prune_list+=( "${release_id}" ) done (( page++ )) done local prune_count="${#prune_list[@]}" local prune_trim=$(( "${prune_count}" - "${retain}" )) # If there won't be any remaining items in the list, bail if [ "${prune_trim}" -le 0 ]; then debug "no %ss in %s to prune" "${prune_type}" "${repository}" repository="${repository_bak}" # restore the repository value return 0 fi # Trim down the list to what should be deleted prune_list=("${prune_list[@]:$retain:$prune_trim}") # Now delete what is left in the list local r_id for r_id in "${prune_list[@]}"; do debug "deleting release (ID: %s) from %s" "${r_id}" "${repository}" github_request -X DELETE "https://api.github.com/repos/${repository}/releases/${r_id}" || failure "Failed to prune %s %s from %s" "${prune_type}" "${r_id}" "${repository}" done repository="${repository_bak}" # restore the repository value } # Grab the correct github token to use for authentication. The # rules used for the token to return are as follows: # # * only $GITHUB_TOKEN is set: $GITHUB_TOKEN # * only $HASHIBOT_TOKEN is set: $HASHIBOT_TOKEN # # when both $GITHUB_TOKEN and $HASHIBOT_TOKEN are set: # # * $repository value matches $GITHUB_REPOSITORY: $GITHUB_TOKEN # * $repository value does not match $GITHUB_REPOSITORY: $HASHIBOT_TOKEN # # Will return `0` when a token is returned, `1` when no token is returned function github_token() { local gtoken # Return immediately if no tokens are available if [ -z "${GITHUB_TOKEN}" ] && [ -z "${HASHIBOT_TOKEN}" ]; then debug "no github or hashibot token set" return 1 fi # Return token if only one token exists if [ -n "${GITHUB_TOKEN}" ] && [ -z "${HASHIBOT_TOKEN}" ]; then debug "only github token set" printf "%s\n" "${GITHUB_TOKEN}" return 0 elif [ -n "${HASHIBOT_TOKEN}" ] && [ -z "${GITHUB_TOKEN}" ]; then debug "only hashibot token set" printf "%s\n" "${HASHIBOT_TOKEN}" return 0 fi # If the $repository matches the original $GITHUB_REPOSITORY use the local token if [ "${repository}" = "${GITHUB_REPOSITORY}" ]; then debug "prefer github token " printf "%s\n" "${GITHUB_TOKEN}" return 0 fi # Still here, then we send back that hashibot token printf "%s\n" "${HASHIBOT_TOKEN}" return 0 } # This function is used to make requests to the GitHub API. It # accepts the same argument list that would be provided to the # curl executable. It will check the response status and if a # 429 is received (rate limited) it will pause until the defined # rate limit reset time and then try again. # # NOTE: Informative information (like rate limit pausing) will # be printed to stderr. The response body will be printed to # stdout. Return value of the function will be the exit code # from the curl process. function github_request() { local request_exit=0 local info_prefix="__info__" local info_tmpl="${info_prefix}:code=%{response_code}:header=%{size_header}:download=%{size_download}:file=%{filename_effective}" local raw_response_content local curl_cmd=("curl" "-w" "${info_tmpl}" "-i" "-SsL" "--fail") local gtoken # Only add the authentication token if we have one if gtoken="$(github_token)"; then curl_cmd+=("-H" "Authorization: token ${gtoken}") fi # Attach the rest of the arguments curl_cmd+=("${@#}") debug "initial request: %s" "${curl_cmd[*]}" # Make our request raw_response_content="$("${curl_cmd[@]}")" || request_exit="${?}" # Define the status here since we will set it in # the conditional below of something weird happens local status # Check if our response content starts with the info prefix. # If it does, we need to extract the headers from the file. if [[ "${raw_response_content}" = "${info_prefix}"* ]]; then debug "extracting request information from: %s" "${raw_response_content}" raw_response_content="${raw_response_content#"${info_prefix}":code=}" local response_code="${raw_response_content%%:*}" debug "response http code: %s" "${response_code}" raw_response_content="${raw_response_content#*:header=}" local header_size="${raw_response_content%%:*}" debug "response header size: %s" "${header_size}" raw_response_content="${raw_response_content#*:download=}" local download_size="${raw_response_content%%:*}" debug "response file size: %s" "${download_size}" raw_response_content="${raw_response_content#*:file=}" local file_name="${raw_response_content}" debug "response file name: %s" "${file_name}" if [ -f "${file_name}" ]; then # Read the headers from the file and place them in the # raw_response_content to be processed local download_fd exec {download_fd}<"${file_name}" debug "file descriptor created for header grab (source: %s): %q" "${file_name}" "${download_fd}" debug "reading response header content from %s" "${file_name}" read -r -N "${header_size}" -u "${download_fd}" raw_response_content # Close our descriptor debug "closing file descriptor: %q" "${download_fd}" exec {download_fd}<&- # Now trim the headers from the file content debug "trimming response header content from %s" "${file_name}" tail -c "${download_size}" "${file_name}" > "${file_name}.trimmed" || failure "Could not trim headers from downloaded file (%s)" "${file_name}" mv -f "${file_name}.trimmed" "${file_name}" || failure "Could not replace downloaded file with trimmed file (%s)" "${file_name}" else debug "expected file not found (%s)" "${file_name}" status="${response_code}" fi else # Since the response wasn't written to a file, trim the # info from the end of the response if [[ "${raw_response_content}" != *"${info_prefix}"* ]]; then debug "github request response does not include information footer" failure "Unexpected error encountered, partial GitHub response returned" fi raw_response_content="${raw_response_content%"${info_prefix}"*}" fi local ratelimit_reset local ratelimit_remaining local response_content="" # Read the response into lines for processing local lines mapfile -t lines < <( printf "%s" "${raw_response_content}" ) # Process the lines to extract out status and rate # limit information. Populate the response_content # variable with the actual response value local i for (( i=0; i < "${#lines[@]}"; i++ )); do # The line will have a trailing `\r` so just # trim it off local line="${lines[$i]%%$'\r'*}" # strip any leading/trailing whitespace characters read -rd '' line <<< "${line}" if [ -z "${line}" ] && [[ "${status}" = "2"* ]]; then local start="$(( i + 1 ))" local remain="$(( "${#lines[@]}" - "${start}" ))" local response_lines=("${lines[@]:$start:$remain}") response_content="${response_lines[*]}" break fi if [[ "${line}" == "HTTP/"* ]]; then status="${line##* }" debug "http status found: %d" "${status}" fi if [[ "${line}" == "x-ratelimit-reset"* ]]; then ratelimit_reset="${line##*ratelimit-reset: }" debug "ratelimit reset time found: %s" "${ratelimit_reset}" fi if [[ "${line}" == "x-ratelimit-remaining"* ]]; then ratelimit_remaining="${line##*ratelimit-remaining: }" debug "ratelimit requests remaining: %d" "${ratelimit_remaining}" fi done # If the status was not detected, force an error if [ -z "${status}" ]; then failure "Failed to detect response status for GitHub request" fi # If the status was a 2xx code then everything is good # and we can return the response and be done if [[ "${status}" = "2"* ]]; then printf "%s" "${response_content}" return 0 fi # If we are being rate limited, print a notice and then # wait until the rate limit will be reset if [[ "${status}" = "429" ]] || [[ "${status}" = "403" ]]; then debug "request returned %d status, checking for rate limiting" "${status}" # If the ratelimit reset was not detected force an error if [ -z "${ratelimit_reset}" ]; then failure "Failed to detect rate limit reset time for GitHub request" fi # If there are requests still available against the ratelimt # and the status is a 403, then it's an actual 403 and not # a ratelimit if [[ "${status}" = "403" ]] && [[ "${ratelimit_remaining}" -gt 0 ]]; then failure "Request failed with 403 status response" fi debug "rate limiting has been detected on request" local reset_date reset_date="$(date --date="@${ratelimit_reset}")" || failure "Failed to GitHub parse ratelimit reset timestamp (${ratelimit_reset})" local now now="$( date '+%s' )" || failure "Failed to get current timestamp in ratelimit check" local reset_wait="$(( "${ratelimit_reset}" - "${now}" + 2))" printf "GitHub rate limit encountered, reset at %s (waiting %d seconds)\n" \ "${reset_date}" "${reset_wait}" >&2 sleep "${reset_wait}" || failure "Pause for GitHub rate limited request retry failed" github_request "${@}" return "${?}" fi # At this point we just need to return error information printf "GitHub request returned HTTP status: %d\n" "${status}" >&2 printf "Response body: %s\n" "${response_content}" >&2 return "${request_exit}" } # Lock issues which have been closed for longer than # provided number of days. A date can optionally be # provided which will be used as the earliest date to # search. A message can optionally be provided which # will be added as a comment in the issue before locking. # # -d: number of days # -m: message to include when locking the issue (optional) # -s: date to begin searching from (optional) function lock_issues() { local OPTIND opt days start since message while getopts ":d:s:m:" opt; do case "${opt}" in "d") days="${OPTARG}" ;; "s") start="${OPTARG}" ;; "m") message="${OPTARG}" ;; *) failure "Invalid flag provided to lock_issues" ;; esac done shift $((OPTIND-1)) # If days where not provided, return error if [ -z "${days}" ]; then failure "Number of days since closed required for locking issues" fi # If a start date was provided, check that it is a format we can read if [ -n "${start}" ]; then if ! since="$(date --iso-8601=seconds --date="${start}" 2> /dev/null)"; then failure "$(printf "Start date provided for issue locking could not be parsed (%s)" "${start}")" fi fi debug "locking issues that have been closed for at least %d days" "${days}" local req_args=() # Start with basic setup req_args+=("-H" "Accept: application/vnd.github+json") # Add authorization header req_args+=("-H" "Authorization: token ${GITHUB_TOKEN}") # Construct our request endpoint local req_endpoint="https://api.github.com/repos/${repository}/issues" # Page counter for requests local page=$(( 1 )) # Request arguments local req_params=("per_page=20" "state=closed") # If we have a start time, include it if [ -n "${since}" ]; then req_params+=("since=${since}") fi # Compute upper bound for issues we can close local lock_seconds now now="$(date '+%s')" lock_seconds=$(("${now}"-("${days}" * 86400))) while true; do # Join all request parameters with '&' local IFS_BAK="${IFS}" IFS="&" local all_params=("${req_params[*]}" "page=${page}") local params="${all_params[*]}" IFS="${IFS_BAK}" local issue_list issue_count # Make our request to get a page of issues issue_list="$(github_request "${req_args[@]}" "${req_endpoint}?${params}")" || failure "Failed to get repository issue list for ${repository}" issue_count="$(jq 'length' <( printf "%s" "${issue_list}" ))" || failure "Failed to compute count of issues in list for ${repository}" if [ -z "${issue_count}" ] || [ "${issue_count}" -lt 1 ]; then break fi # Iterate through the list local i for (( i=0; i < "${issue_count}"; i++ )); do # Extract the issue we are going to process local issue issue="$(jq ".[${i}]" <( printf "%s" "${issue_list}" ))" || failure "Failed to extract issue from list for ${repository}" # Grab the ID of this issue local issue_id issue_id="$(jq -r '.id' <( printf "%s" "${issue}" ))" || failure "Failed to read ID of issue for ${repository}" # First check if issue is already locked local issue_locked issue_locked="$(jq -r '.locked' <( printf "%s" "${issue}" ))" || failure "Failed to read locked state of issue for ${repository}" if [ "${issue_locked}" == "true" ]; then debug "Skipping %s#%s because it is already locked" "${repository}" "${issue_id}" continue fi # Get the closed date local issue_closed issue_closed="$(jq -r '.closed_at' <( printf "%s" "${issue}" ))" || failure "Failed to read closed at date of issue for ${repository}" # Convert closed date to unix timestamp local date_check date_check="$( date --date="${issue_closed}" '+%s' )" || failure "Failed to parse closed at date of issue for ${repository}" # Check if the issue is old enough to be locked if [ "$(( "${date_check}" ))" -lt "${lock_seconds}" ]; then printf "Locking issue %s#%s\n" "${repository}" "${issue_id}" >&2 # If we have a comment to add before locking, do that now if [ -n "${message}" ]; then local message_json message_json=$(jq -n \ --arg msg "$(printf "%b" "${message}")" \ '{body: $msg}' ) || failure "Failed to create issue comment JSON content for ${repository}" debug "adding issue comment before locking on %s#%s" "${repository}" "${issue_id}" github_request "${req_args[@]}" -X POST "${req_endpoint}/${issue_id}/comments" -d "${message_json}" || failure "Failed to create issue comment on ${repository}#${issue_id}" fi # Lock the issue github_request "${req_args[@]}" -X PUT "${req_endpoint}/${issue_id}/lock" -d '{"lock_reason":"resolved"}' || failure "Failed to lock issue ${repository}#${issue_id}" fi done ((page++)) done } # Send a repository dispatch to the defined repository # # $1: repository name # $2: event type (single word string) # $n: "key=value" pairs to build payload (optional) # function github_repository_dispatch() { local drepo_name="${1}" local event_type="${2}" if [ -z "${drepo_name}" ]; then failure "Repository name is required for repository dispatch" fi # shellcheck disable=SC2016 local payload_template='{"vagrant-ci": $vagrant_ci' local jqargs=("--arg" "vagrant_ci" "true") local arg for arg in "${@:3}"; do local payload_key="${arg%%=*}" local payload_value="${arg##*=}" payload_template+=", \"${payload_key}\": \$${payload_key}" # shellcheck disable=SC2089 jqargs+=("--arg" "${payload_key}" "${payload_value}") done payload_template+="}" # NOTE: we want the arguments to be expanded below local payload payload=$(jq -n "${jqargs[@]}" "${payload_template}" ) || failure "Failed to generate repository dispatch payload" # shellcheck disable=SC2016 local msg_template='{event_type: $event_type, client_payload: $payload}' local msg msg=$(jq -n \ --argjson payload "${payload}" \ --arg event_type "${event_type}" \ "${msg_template}" \ ) || failure "Failed to generate repository dispatch message" # Update repository value to get correct token local repository_bak="${repository}" repository="${repo_owner}/${drepo_name}" github_request -X "POST" \ -H 'Accept: application/vnd.github.everest-v3+json' \ --data "${msg}" \ "https://api.github.com/repos/${repo_owner}/${drepo_name}/dispatches" || failure "Repository dispatch to ${repo_owner}/${drepo_name} failed" # Restore the repository value repository="${repository_bak}" } # Copy a function to a new name # # $1: Original function name # $2: Copy function name function copy_function() { local orig="${1}" local new="${2}" local fn fn="$(declare -f "${orig}")" || failure "Orignal function (${orig}) not defined" fn="${new}${fn#*"${orig}"}" eval "${fn}" } # Rename a function to a new name # # $1: Original function name # $2: New function name function rename_function() { local orig="${1}" copy_function "${@}" unset -f "${orig}" } # Cleanup wrapper so we get some output that cleanup is starting function _cleanup() { debug "* Running cleanup task..." # Always restore this value for cases where a failure # happened within a function while this value was in # a modified state repository="${_repository_backup}" cleanup } # Stub cleanup method which can be redefined # within actual script function cleanup() { debug "** No cleanup tasks defined" } # Only setup our cleanup trap and fail alias when not in testing if [ -z "${BATS_TEST_FILENAME}" ]; then trap _cleanup EXIT # This is a compatibility alias for existing scripts which # use the common.sh library. BATS support defines a `fail` # function so it has been renamed `failure` to prevent the # name collision. When not running under BATS we enable the # `fail` function so any scripts that have not been updated # will not be affected. copy_function "failure" "fail" fi # Make sure the CI bin directory exists if [ ! -d "${ci_bin_dir}" ]; then wrap mkdir -p "${ci_bin_dir}" \ "Failed to create CI bin directory" fi # Always ensure CI bin directory is in PATH if [[ "${PATH}" != *"${ci_bin_dir}"* ]]; then export PATH="${PATH}:${ci_bin_dir}" fi # Enable debugging. This needs to be enabled with # extreme caution when used on public repositories. # Output with debugging enabled will likely include # secret values which should not be publicly exposed. # # If repository is public, FORCE_PUBLIC_DEBUG environment # variable must also be set. priv_args=("-H" "Accept: application/json") # If we have a token available, use it for the check query if [ -n "${HASHIBOT_TOKEN}" ]; then priv_args+=("-H" "Authorization: token ${GITHUB_TOKEN}") elif [ -n "${GITHUB_TOKEN}" ]; then priv_args+=("-H" "Authorization: token ${HASHIBOT_TOKEN}") fi if [ -n "${GITHUB_ACTIONS}" ]; then priv_check="$(curl "${priv_args[@]}" -s "https://api.github.com/repos/${GITHUB_REPOSITORY}" | jq .private)" || failure "Repository visibility check failed" fi # If the value wasn't true we unset it to indicate not private. The # repository might actually be private but we weren't supplied a # token (or one with correct permissions) so we fallback to the safe # assumption of not private. if [ "${priv_check}" != "true" ]; then readonly is_public="1" readonly is_private="" else # shellcheck disable=SC2034 readonly is_public="" # shellcheck disable=SC2034 readonly is_private="1" fi # Check if we are running a job created by a tag. If so, # mark this as being a release job and set the release_version if [[ "${GITHUB_REF}" == *"refs/tags/"* ]]; then export tag="${GITHUB_REF##*tags/}" if valid_release_version "${tag}"; then readonly release=1 export release_version="${tag##*v}" else readonly release fi else # shellcheck disable=SC2034 readonly release fi # Seed an initial output file output_file > /dev/null 2>&1 ================================================ FILE: .ci/build ================================================ #!/usr/bin/env bash csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../" && pwd )" . "${root}/.ci/load-ci.sh" if [ "${#}" -ne 1 ]; then printf "Usage: %s ARTIFACTS_DIR\n" "${0}" >&2 exit 1 fi dest="${1}" mkdir -p "${dest}" || failure "Could not create destination directory (%s)" "${dest}" pushd "${dest}" dest="$(pwd)" || failure "Could not read destination directory path" popd # Move to root of project pushd "${root}" info "Building Vagrant RubyGem..." wrap gem build ./*.gemspec \ "Failed to build Vagrant RubyGem" # Get the path of the gem files=( vagrant*.gem ) gem="${files[0]}" if [ ! -f "${gem}" ]; then debug "could not locate gem in %s" "${files[*]}" failure "Unable to locate built Vagrant RubyGem" fi wrap mv "${gem}" "${dest}" \ "Failed to relocate Vagrant RubyGem" printf "build-artifacts-path=%s\n" "${dest}" ================================================ FILE: .ci/dev-build ================================================ #!/usr/bin/env bash csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../" && pwd )" . "${root}/.ci/load-ci.sh" if [ "${#}" -ne 3 ]; then printf "Usage: %s BRANCH COMMIT_ID BUILD_TYPE\n" "${0}" >&2 exit 1 fi branch="${1}" full_sha="${2}" build_type="${3}" if [ -z "${branch}" ]; then failure "Branch variable is unset, required for dev build" fi if [ -z "${full_sha}" ]; then failure "The full_sha variable is unexpectedly missing, cannot trigger dev build" fi if [ -z "${build_type}" ]; then failure "The build type is required for triggering dev build" fi # Trim the reference prefix if needed if [[ "${branch}" = *"refs/heads"* ]]; then debug "trimming branch reference value - %s" "${branch}" branch="${branch##*refs/heads/}" debug "trimmed branch value - %s" "${branch}" fi info "Triggering development build %s (%s)" "${branch}" "${full_sha}" github_repository_dispatch "vagrant-builders" "${build_type}" "commit_id=${full_sha}" "branch=${branch}" ================================================ FILE: .ci/generate-licenses ================================================ #!/usr/bin/env bash csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../" && pwd )" . "${root}/.ci/load-ci.sh" if [ "${#}" -ne 1 ]; then printf "Usage: %s LICENSE_DIR\n" "${0}" exit 1 fi license_dir="${1}" if [ ! -d "${license_dir}" ]; then mkdir -p "${license_dir}" || failure "Unable to create license directory" fi pushd "${license_dir}" license_dir="$(pwd)" || failure "Could not read license directory path" popd # Move to the root pushd "${root}" info "Generating Vagrant license files" version="$(< ./version.txt)" || failure "Unable to read version file" license_date="$(date "+%Y")" || failure "Unable to generate year for license" license_template="./templates/license/license.html.tmpl" license_destination="${license_dir}/LICENSE.html" debug "Updating license file: ${license_destination}" if [ ! -f "${license_template}" ]; then failure "Unable to locate license template (${license_template})" fi sed "s/%VERSION%/${version}/" "${license_template}" > "${license_destination}" || failure "Unable to update version in ${license_destination}" sed -i "s/%YEAR%/${license_date}/" "${license_destination}" || failure "Unable to update year in ${license_destination}" license_template="./templates/license/license.rtf.tmpl" license_destination="${license_dir}/LICENSE.rtf" debug "Updating license file: ${license_destination}" if [ ! -f "${license_template}" ]; then failure "Unable to locate license template (${license_template})" fi sed "s/%VERSION%/${version}/" "${license_template}" > "${license_destination}" || failure "Unable to update version in ${license_destination}" sed -i "s/%YEAR%/${license_date}/" "${license_destination}" || failure "Unable to update year in ${license_destination}" license_template="./templates/license/license.tmpl" license_destination="${license_dir}/LICENSE.txt" debug "Updating license file: ${license_destination}" if [ ! -f "${license_template}" ]; then failure "Unable to locate license template (${license_template})" fi sed "s/%VERSION%/${version}/" "${license_template}" > "${license_destination}" || failure "Unable to update version in ${license_destination}" sed -i "s/%YEAR%/${license_date}/" "${license_destination}" || failure "Unable to update year in ${license_destination}" ================================================ FILE: .ci/load-ci.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2019, 2025 # SPDX-License-Identifier: MPL-2.0 csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done if ! root="$( cd -P "$( dirname "$csource" )/../" && pwd )"; then echo "⛔ ERROR: Failed to determine root local directory ⛔" >&2 exit 1 fi export root export ci_bin_dir="${root}/.ci/.ci-utility-files" # shellcheck source=/dev/null if ! source "${ci_bin_dir}/common.sh"; then echo "⛔ ERROR: Failed to source Vagrant CI common file ⛔" >&2 exit 1 fi export PATH="${PATH}:${ci_bin_dir}" # And we are done! debug "VagrantCI Loaded" ================================================ FILE: .ci/nightly-build ================================================ #!/usr/bin/env bash csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../" && pwd )" . "${root}/.ci/load-ci.sh" if [ "${#}" -ne 1 ]; then printf "Usage: %s COMMIT_ID\n" "${0}" >&2 exit 1 fi full_sha="${1}" if [ -z "${full_sha}" ]; then failure "The full_sha variable is unexpectedly missing, cannot trigger nightly build" fi info "Triggering nightly build %s (%s)" "${tag}" "${full_sha}" github_repository_dispatch "vagrant-builders" "nightlies" "commit_id=${full_sha}" ================================================ FILE: .ci/release ================================================ #!/usr/bin/env bash csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../" && pwd )" . "${root}/.ci/load-ci.sh" if [ "${#}" -ne 2 ]; then printf "Usage: %s TAG COMMIT_ID\n" "${0}" >&2 exit 1 fi tag="${1}" full_sha="${2}" if [ -z "${tag}" ]; then failure "Tag variable is unset, required for release" fi if [ -z "${full_sha}" ]; then failure "The full_sha variable is unexpectedly missing, cannot trigger release" fi # Trim the prefix of the tag if it hasn't been if [[ "${tag}" = *"refs/tags"* ]]; then debug "trimming tag reference value - %s" "${tag}" tag="${tag##*refs/tags/}" debug "trimmed tag value - %s" "${tag}" fi info "Triggering release %s (%s)" "${tag}" "${full_sha}" github_repository_dispatch "vagrant-builders" "hashicorp-release" "commit_id=${full_sha}" "tag=${tag}" ================================================ FILE: .ci/release-initiator ================================================ #!/usr/bin/env bash csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../" && pwd )" . "${root}/.ci/load-ci.sh" if [ "${#}" -ne 1 ]; then printf "Usage: %s VERSION\n" "${0}" >&2 exit 1 fi version="${1}" info "Updating repository files for ${version} release" if [[ "${version}" = "v"* ]]; then failure "Invalid version format, cannot start with 'v': %s" "${version}" fi if ! valid_release_version "${version}"; then failure "Invalid version format provided: %s" "${version}" fi debug "Configuring git" hashibot_git debug "Updating version.txt with version value: %s" "${version}" if [ ! -f "version.txt" ]; then failure "Unable to locate version.txt file" fi printf "%s" "${version}" > version.txt debug "Updating CHANGELOG.md" if [ ! -f "CHANGELOG.md" ]; then failure "Unable to locate CHANGLOG.md file" fi datestamp="$(date "+%B %d, %Y")" || failure "Unable to generate date" printf "## %s (%s)\n" "${version}" "${datestamp}" > .CHANGELOG.md.new grep -v UNRELEASED < CHANGELOG.md >> .CHANGELOG.md.new || failure "Unable to update CHANGELOG contents" mv .CHANGELOG.md.new CHANGELOG.md || failure "Unable to overwrite CHANGELOG file" license_date="$(date "+%Y")" || failure "Unable to generate year for license" license_template="./templates/license/license.tmpl" license_destination="./LICENSE" debug "Updating license file: ${license_destination}" if [ ! -f "${license_template}" ]; then failure "Unable to locate license template (${license_template})" fi if [ ! -f "${license_destination}" ]; then failure "Unable to locate license destination (${license_destination})" fi sed "s/%VERSION%/${version}/" "${license_template}" > "${license_destination}" || failure "Unable to update version in ${license_destination}" sed -i "s/%YEAR%/${license_date}/" "${license_destination}" || failure "Unable to update year in ${license_destination}" debug "Updating download version in website source" version_file="./website/data/version.json" if [ ! -f "${version_file}" ]; then failure "Unable to locate version data file (%s)" "${version_file}" fi sed -i "s/ \"VERSION\":.*,/ \"VERSION\": \"${version}\",/" "${version_file}" || failure "Unable to update version data file (%s)" "${version_file}" debug "Commit version updates" # display changes before commit git status git add version.txt CHANGELOG.md LICENSE "${version_file}" || failure "Unable to stage updated release files for commit" git commit -m "Release ${version}" || failure "Unable to commit updated files for release" release_tag="v${version}" debug "Creating new tag %s" "${release_tag}" git tag "${release_tag}" # Generate a new version for development version_prefix="${version%.*}" patch="${version##*.}" new_patch=$(( "${patch}" + 1 )) dev_version="${version_prefix}.${new_patch}.dev" debug "Updating files for new development - %s" "${dev_version}" debug "Updating version.txt with version value: %s" "${dev_version}" printf "%s\n" "${dev_version}" > version.txt debug "Updating CHANGELOG" printf "## %s (UNRELEASED)\n\nFEATURES:\n\nIMPROVEMENTS:\n\nBUG FIXES:\n\n" "${dev_version}" > .CHANGELOG.md.new cat CHANGELOG.md >> .CHANGELOG.md.new mv .CHANGELOG.md.new CHANGELOG.md || failure "Unable to overwrite CHANGELOG file" debug "Updating LICENSE" sed "s/%VERSION%/${dev_version}/" "${license_template}" > LICENSE || failure "Unable to update LICENSE" debug "Commit development version updates" # display changes before commit git status git add version.txt CHANGELOG.md LICENSE || failure "Unable to stage updated development files for commit" git commit -m "Update files for new development ${dev_version}" || failure "Unable to commit updated files for development" # Now that all changes are complete, push debug "Pushing all changes to origin" git push origin main || failure "Unable to push changes to main" git push origin "${release_tag}" || failure "Unable to push tag to main" ================================================ FILE: .ci/spec/clean-packet.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../../" && pwd )" . "${root}/.ci/load-ci.sh" . "${root}/.ci/spec/env.sh" pushd "${root}" > "${output}" echo "Cleaning up packet device..." unset PACKET_EXEC_PERSIST unset PACKET_EXEC_PRE_BUILTINS # spec test configuration, defined by action runners, used by Vagrant on packet export PKT_VAGRANT_HOST_BOXES="${VAGRANT_HOST_BOXES}" export PKT_VAGRANT_GUEST_BOXES="${VAGRANT_GUEST_BOXES}" # other vagrant-spec options export PKT_VAGRANT_HOST_MEMORY="${VAGRANT_HOST_MEMORY:-10000}" export PKT_VAGRANT_CWD="test/vagrant-spec/" export PKT_VAGRANT_VAGRANTFILE=Vagrantfile.spec ### wrap_stream packet-exec run -- "vagrant destroy -f" \ "Vagrant failed to destroy remaining vagrant-spec guests during clean up" echo "Finished destroying spec test hosts" ================================================ FILE: .ci/spec/create-hosts.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../../" && pwd )" . "${root}/.ci/load-ci.sh" . "${root}/.ci/spec/env.sh" pushd "${root}" > "${output}" # spec test configuration, defined by action runners, used by Vagrant on packet export PKT_VAGRANT_HOST_BOXES="${VAGRANT_HOST_BOXES}" export PKT_VAGRANT_GUEST_BOXES="${VAGRANT_GUEST_BOXES}" # other vagrant-spec options export PKT_VAGRANT_HOST_MEMORY="${VAGRANT_HOST_MEMORY:-10000}" export PKT_VAGRANT_CWD="test/vagrant-spec/" export PKT_VAGRANT_VAGRANTFILE=Vagrantfile.spec export PKT_VAGRANT_SPEC_PROVIDERS="${VAGRANT_SPEC_PROVIDERS}" ### # Grab vagrant-spec gem and place inside root dir of Vagrant repo wrap aws s3 cp "${ASSETS_PRIVATE_BUCKET}/hashicorp/vagrant-spec/vagrant-spec.gem" "vagrant-spec.gem" \ "Could not download vagrant-spec.gem from s3 asset bucket" ### # Grab vagrant installer and place inside root dir of Vagrant repo if [ -z "${VAGRANT_PRERELEASE_VERSION}" ]; then INSTALLER_URL=`curl -s https://api.github.com/repos/hashicorp/vagrant-installers/releases | jq -r '.[0].assets[] | select(.name | contains("_x86_64.deb")) | .browser_download_url'` else INSTALLER_URL=`curl -s https://api.github.com/repos/hashicorp/vagrant-installers/releases/tags/${VAGRANT_PRERELEASE_VERSION} | jq -r '.assets[] | select(.name | contains("_x86_64.deb")) | .browser_download_url'` fi wrap curl -fLO ${INSTALLER_URL} \ "Could not download vagrant installers" ### # Run the job echo "Creating vagrant spec guests..." wrap_stream packet-exec run -upload -- "vagrant up --no-provision --provider vmware_desktop" \ "Vagrant Acceptance host creation command failed" echo "Finished bringing up vagrant spec guests" ================================================ FILE: .ci/spec/create-packet.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 export PACKET_EXEC_PREFER_FACILITIES="${PACKET_EXEC_PREFER_FACILITIES:-iad2,dfw2,dfw1,ny5,ny7,ewr1,la4,lax1,lax2,tr2,ch3,ord1,ord4}" csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../../" && pwd )" . "${root}/.ci/load-ci.sh" . "${root}/.ci/spec/env.sh" pushd "${root}" > "${output}" # Ensure we have a packet device to connect echo "Creating packet device if needed..." packet-exec info if [ $? -ne 0 ]; then wrap_stream packet-exec create \ "Failed to create packet device" fi echo "Finished creating spec test packet instance" ================================================ FILE: .ci/spec/env.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # packet and job configuration export SLACK_USERNAME="Vagrant" export SLACK_ICON="https://media.giphy.com/media/yIQ5glQeheYE0/200.gif" export SLACK_TITLE="Vagrant-Spec Test Runner" export SLACK_CHANNEL="CLYRTANRH" # CLYRTANRH is ID of #team-vagrant-spam-channel export PACKET_EXEC_DEVICE_NAME="${PACKET_EXEC_DEVICE_NAME:-spec-ci-boxes}" export PACKET_EXEC_DEVICE_SIZE="${PACKET_EXEC_DEVICE_SIZE:-c3.small.x86,c3.medium.x86}" export PACKET_EXEC_PREFER_FACILITIES="${PACKET_EXEC_PREFER_FACILITIES:-la4,dc10,dc13,ny7,pa4,md2}" export PACKET_EXEC_OPERATING_SYSTEM="${PACKET_EXEC_OPERATING_SYSTEM:-ubuntu_18_04}" export PACKET_EXEC_PRE_BUILTINS="${PACKET_EXEC_PRE_BUILTINS:-InstallVagrant,InstallVirtualBox,InstallVmware,InstallVagrantVmware}" export PACKET_EXEC_QUIET="1" export PACKET_EXEC_PERSIST="1" # job_id is provided by common.sh export PACKET_EXEC_REMOTE_DIRECTORY="${job_id}" export PKT_VAGRANT_CLOUD_TOKEN="${VAGRANT_CLOUD_TOKEN}" # Pass Hashibot Credentials down to packet-exec run commands so they can fetch # private github repos during build export PKT_HASHIBOT_USERNAME="${HASHIBOT_USERNAME}" export PKT_HASHIBOT_TOKEN="${HASHIBOT_TOKEN}" ### ================================================ FILE: .ci/spec/notify-success.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../../" && pwd )" . "${root}/.ci/load-ci.sh" . "${root}/.ci/spec/env.sh" pushd "${root}" > "${output}" slack -m 'Tests have passed!' ================================================ FILE: .ci/spec/pull-log.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../../" && pwd )" . "${root}/.ci/load-ci.sh" . "${root}/.ci/spec/env.sh" pushd "${root}" > "${output}" # Use same setup as run-tests.sh so `vagrant ssh` will work. unset PACKET_EXEC_PRE_BUILTINS # spec test configuration, defined by action runners, used by Vagrant on packet export PKT_VAGRANT_HOST_BOXES="${VAGRANT_HOST_BOXES}" export PKT_VAGRANT_GUEST_BOXES="${VAGRANT_GUEST_BOXES}" # other vagrant-spec options export PKT_VAGRANT_HOST_MEMORY="${VAGRANT_HOST_MEMORY:-10000}" export PKT_VAGRANT_CWD="test/vagrant-spec/" export PKT_VAGRANT_VAGRANTFILE=Vagrantfile.spec export PKT_VAGRANT_SPEC_PROVIDERS="${VAGRANT_SPEC_PROVIDERS}" export PKT_VAGRANT_DOCKER_IMAGES="${VAGRANT_DOCKER_IMAGES}" echo "Pulling log..." packet-exec run -download vagrant-spec.log "vagrant ssh -c \"cat /tmp/vagrant-spec.log\" > vagrant-spec.log" ================================================ FILE: .ci/spec/run-test.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../../" && pwd )" . "${root}/.ci/load-ci.sh" . "${root}/.ci/spec/env.sh" pushd "${root}" > "${output}" # Assumes packet is already set up unset PACKET_EXEC_PRE_BUILTINS # spec test configuration, defined by action runners, used by Vagrant on packet export PKT_VAGRANT_HOST_BOXES="${VAGRANT_HOST_BOXES}" export PKT_VAGRANT_GUEST_BOXES="${VAGRANT_GUEST_BOXES}" # other vagrant-spec options export PKT_VAGRANT_HOST_MEMORY="${VAGRANT_HOST_MEMORY:-10000}" export PKT_VAGRANT_CWD="test/vagrant-spec/" export PKT_VAGRANT_VAGRANTFILE=Vagrantfile.spec export PKT_VAGRANT_SPEC_PROVIDERS="${VAGRANT_SPEC_PROVIDERS}" export PKT_VAGRANT_DOCKER_IMAGES="${VAGRANT_DOCKER_IMAGES}" ### # Run the job echo "Running vagrant spec tests..." # Need to make memory customizable for windows hosts wrap_stream packet-exec run "vagrant provision" \ "Vagrant Acceptance testing command failed" echo "Finished vagrant spec tests" ================================================ FILE: .ci/sync.sh ================================================ #!/usr/bin/env bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 csource="${BASH_SOURCE[0]}" while [ -h "$csource" ] ; do csource="$(readlink "$csource")"; done root="$( cd -P "$( dirname "$csource" )/../" && pwd )" . "${root}/.ci/load-ci.sh" export PATH="${PATH}:${root}/.ci" pushd "${root}" > "${output}" if [ "${repo_name}" = "vagrant" ]; then remote_repository="hashicorp/vagrant-acceptance" else fail "This repository is not configured to sync vagrant to mirror repository" fi wrap git config pull.rebase false \ "Failed to configure git pull strategy" echo "Adding remote mirror repository '${remote_repository}'..." wrap git remote add mirror "https://${HASHIBOT_USERNAME}:${HASHIBOT_TOKEN}@github.com/${remote_repository}" \ "Failed to add mirror '${remote_repository}' for sync" echo "Updating configured remotes..." wrap_stream git remote update mirror \ "Failed to update mirror repository (${remote_repository}) for sync" rb=$(git branch -r --list "mirror/${ident_ref}") if [ "${rb}" != "" ]; then echo "Pulling ${ident_ref} from mirror..." wrap_stream git pull mirror "${ident_ref}" \ "Failed to pull ${ident_ref} from mirror repository (${remote_repository}) for sync" fi echo "Pushing ${ident_ref} to mirror..." wrap_stream git push mirror "${ident_ref}" \ "Failed to sync mirror repository (${remote_repository})" ================================================ FILE: .copywrite.hcl ================================================ schema_version = 1 project { license = "BUSL-1.1" copyright_year = 2024 header_ignore = [ "internal/pkg/defaults/**", "internal/pkg/spinner/**", "internal/server/bindata_ui.go", "internal/server/gen/**", "lib/vagrant/protobufs/**", "thirdparty/**", ] } ================================================ FILE: .github/CODEOWNERS ================================================ * @hashicorp/Vagrant ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Code of Conduct HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. Please read the full text at https://www.hashicorp.com/community-guidelines ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to Vagrant **First:** We like to encourage you to contribute to the repository. If you're unsure or afraid of _anything_, just ask or submit the issue or pull request anyways. You won't be yelled at for giving your best effort. The worst that can happen is that you'll be politely asked to change something. We appreciate any sort of contributions, and don't want a wall of rules to get in the way of that. However, for those individuals who want a bit more guidance on the best way to contribute to the project, read on. This document will cover what we're looking for. By addressing all the points we're looking for, it raises the chances we can quickly merge or address your contributions. Before opening a new issue or pull request, we do appreciate if you take some time to search for [possible duplicates](https://github.com/hashicorp/vagrant/issues?q=sort%3Aupdated-desc), or similar discussions on [HashiCorp Discuss](https://discuss.hashicorp.com/c/vagrant/24). On GitHub, you can scope searches by labels to narrow things down. To ensure that the Vagrant community remains an open and safe space for everyone we also follow the [HashiCorp community guidelines](https://www.hashicorp.com/community-guidelines). When contributing to Vagrant, please respect these guidelines. ## Issues ### Reporting an Issue **Tip:** We have provided a [GitHub issue template](https://github.com/hashicorp/vagrant/blob/main/.github/ISSUE_TEMPLATE/bug-report.md). By respecting the proposed format and filling all the relevant sections, you'll strongly help the Vagrant collaborators to handle your request the best possible way. ### Issue Lifecycle 1. The issue is reported. 2. The issue is verified and categorized by Vagrant collaborator(s). Categorization is done via GitHub tags for different dimensions (like issue type, affected components, pending actions, etc.) 3. Unless it is critical, the issue is left for a period of time, giving outside contributors a chance to address the issue. Later, the issue may be assigned to a Vagrant collaborator and planned for a specific release [milestone](https://github.com/hashicorp/vagrant/milestones) 4. The issue is addressed in a pull request or commit. The issue will be referenced in the commit message so that the code that fixes it is clearly linked. 5. The issue is closed. Sometimes, valid issues will be closed to keep the issue tracker clean. The issue is still indexed and available for future viewers, or can be re-opened if necessary. 6. The issue is locked. After about 30 days the issue will be locked. This is done to keep issue activity in open issues and encourge users to open a new issue if an old issue is being encountered again. ## Pull Requests Thank you for contributing! Here you'll find information on what to include in your Pull Request (“PR” for short) to ensure it is reviewed quickly, and possibly accepted. Before starting work on a new feature or anything besides a minor bug fix, it is highly recommended to first initiate a discussion with the Vagrant community (either via a GitHub issue or [HashiCorp Discuss](https://discuss.hashicorp.com/c/vagrant/24)). This will save you from wasting days implementing a feature that could be rejected in the end. No pull request template is provided on GitHub. The expected changes are often already described and validated in an existing issue, that obviously should be referenced. The Pull Request thread should be mainly used for the code review. **Tip:** Make it small! A focused PR gives you the best chance of having it accepted. Then, repeat if you have more to propose! ### Vagrant Go The Vagrant port to Go is currently in an alpha state and under heavy development. Please refer to [this issue](https://github.com/hashicorp/vagrant/issues/12819) before starting or submitting pull requests related to Vagrant Go. ### Setup a development installation of Vagrant *A Vagrantfile is provided that should take care setting up a VM for running the rspec tests.* If you only need to run those tests and don't also want to run a development version of Vagrant from a host machine then it's recommended to use that. There are a few prerequisites for setting up a development environment with Vagrant. Ensure you have the following installed on your machine: * git * bsdtar * curl #### Install Ruby It's nice to have a way to control what version of ruby is installed, so you may want to install [rvm](https://rvm.io/rvm/install), [chruby](https://github.com/postmodern/chruby#install) or [rbenv](https://github.com/rbenv/rbenv#installation). For Windows [ruby installer](https://rubyinstaller.org/) is recommended. #### Setup Vagrant Clone Vagrant's repository from GitHub into the directory where you keep code on your machine: ``` $ git clone --recurse-submodules https://github.com/hashicorp/vagrant.git ``` Next, move into the newly created `./vagrant` directory. ``` $ cd ./vagrant ``` All commands will be run from this path. Now, run the `bundle` command to install the Ruby dependencies: ``` $ bundle install ``` You can now run Vagrant by running `bundle exec vagrant` from inside that directory. ##### Setting up Vagrant-go Add the generated `binstubs` to your `PATH` ``` $ export PATH=/path/to/my/vagrant/binstubs ``` Install go using the method of your choice. Build the Vagrant go binary using `make` ``` $ make ``` This will generate a `./vagrant` binary in your project root. ### How to prepare your pull request Once you're confident that your upcoming changes will be accepted: * In your forked repository, create a topic branch for your upcoming patch. * Usually this is based on the main branch. * Checkout a new branch based on main; `git checkout -b my-contrib main` Please avoid working directly on the `main` branch. * Make focused commits of logical units and describe them properly. * Avoid re-formatting of the existing code. * Check for unnecessary whitespace with `git diff --check` before committing. * Tests are required in each pull request. There are some exceptions like docs changes and dependency constraint updates. * Assure nothing is broken by running manual tests, and all the automated tests. ### Running tests Vagrant uses rspec to run tests. Once your Vagrant bundle is installed from Git repository, you can run the test suite with: bundle exec rake This will run the unit test suite, which should come back all green! If you are developing Vagrant on a machine that already has a Vagrant package installation present, both will attempt to use the same folder for their configuration (location of this folder depends on system). This can cause errors when Vagrant attempts to load plugins. In this case, override the `VAGRANT_HOME` environment variable for your development version of Vagrant before running any commands to be some new folder within the project or elsewhere on your machine. For example, in Bash: export VAGRANT_HOME=~/.vagrant-dev You can now run Vagrant commands against the development version: bundle exec vagrant ### Acceptance Tests Vagrant also comes with an acceptance test suite that does black-box tests of various Vagrant components. Note that these tests are **extremely slow** because actual VMs are spun up and down. The full test suite can take hours. Instead, try to run focused component tests. To run the acceptance test suite, first copy `vagrant-spec.config.example.rb` to `vagrant-spec.config.rb` and modify it to valid values. The places you should fill in are clearly marked. Next, see the components that can be tested: ``` $ rake acceptance:components cli provider/virtualbox/basic ... ``` Then, run one of those components: ``` $ rake acceptance:run COMPONENTS="cli" ... ``` ### Submit Changes * Push your changes to a topic branch in your fork of the repository. * Open a PR to the original repository and choose the right original branch you want to patch (main for most cases). * If not done in commit messages (which you really should do) please reference and update your issue with the code changes. * Even if you have write access to the repository, do not directly push or merge your own pull requests. Let another team member review your PR and approve. ### Pull Request Lifecycle 1. You are welcome to submit your PR for commentary or review before it is fully completed. Please prefix the title of your PR with "[WIP]" to indicate this. It's also a good idea to include specific questions or items you'd like feedback on. 2. Sign the [HashiCorp CLA](#hashicorp-cla). If you haven't signed the CLA yet, a bot will ask you to do so. You only need to sign the CLA once. If you've already signed the CLA, the CLA status will be green automatically. 3. The PR is categorized by Vagrant collaborator(s), applying GitHub tags similarly to issues triage. 4. Once you believe your PR is ready to be merged, you can remove any "[WIP]" prefix from the title and a Vagrant collaborator will review. 5. One of the Vagrant collaborators will look over your contribution and either provide comments letting you know if there is anything left to do. We do our best to provide feedback in a timely manner, but it may take some time for us to respond. 6. Once all outstanding comments have been addressed, your contribution will be merged! Merged PRs will be included in the next Vagrant release. The Vagrant contributors will take care of updating the CHANGELOG as they merge. 7. We might decide that a PR should be closed. We'll make sure to provide clear reasoning when this happens. ## HashiCorp CLA We require all contributors to sign the [HashiCorp CLA](https://www.hashicorp.com/cla). In simple terms, the CLA affirms that the work you're contributing is original, that you grant HashiCorp permission to use that work (including license to any patents necessary), and that HashiCorp may relicense your work for our commercial products if necessary. Note that this description is a summary and the specific legal terms should be read directly in the [CLA](https://www.hashicorp.com/cla). The CLA does not change the terms of the standard open source license used by our software such as MPL2 or MIT. You are still free to use our projects within your own projects or businesses, republish modified source, and more. Please reference the appropriate license of this project to learn more. To sign the CLA, open a pull request as usual. If you haven't signed the CLA yet, a bot will respond with a link asking you to sign the CLA. We cannot merge any pull request until the CLA is signed. You only need to sign the CLA once. If you've signed the CLA before, the bot will not respond to your PR and your PR will be allowed to merge. # Additional Resources * [HashiCorp Community Guidelines](https://www.hashicorp.com/community-guidelines) * [General GitHub documentation](https://help.github.com/) * [GitHub pull request documentation](https://help.github.com/send-pull-requests/) ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug Report about: Let us know about an unexpected error, a crash, or an incorrect behavior. labels: "waiting-intake" --- ### Debug output ### Expected behavior ### Actual behavior ### Reproduction information #### Vagrant version #### Host operating system #### Guest operating system #### Steps to reproduce 1. 2. 3. #### Vagrantfile ```ruby # Copy-paste your Vagrantfile here. Remove any sensitive information such as passwords, authentication tokens, or email addresses. ``` ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 contact_links: - name: Ask a Question url: https://discuss.hashicorp.com/c/vagrant about: If you have a question or are looking for advice, please post on our Discuss forum. ================================================ FILE: .github/ISSUE_TEMPLATE/engineering-task.md ================================================ --- name: Vagrant Engineering about: For Vagrant Engineers to track tasks. --- ## Description 2 - 3 sentences that define the solution or changes. ## Use case (optional) How will this affect the end user? ## Supporting material Add links to related issues or supporting documentation. ================================================ FILE: .github/ISSUE_TEMPLATE/vagrant-feature-request.md ================================================ --- name: Vagrant Feature request about: Suggest an idea or enhancement for Vagrant title: 'Enhancement Request: Your description here' labels: ['enhancement', 'waiting-intake'] assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/backport.yml ================================================ --- name: Backport Assistant Runner on: pull_request_target: types: - closed - labeled jobs: backport: if: github.event.pull_request.merged runs-on: ubuntu-latest container: hashicorpdev/backport-assistant:0.2.3 steps: - name: Backport changes to stable-website run: | backport-assistant backport -automerge env: BACKPORT_LABEL_REGEXP: "backport/(?Pwebsite)" BACKPORT_TARGET_TEMPLATE: "stable-{{.target}}" GITHUB_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} ================================================ FILE: .github/workflows/check-legacy-links-format.yml ================================================ name: Legacy Link Format Checker on: push: paths: - "website/content/**/*.mdx" - "website/data/*-nav-data.json" jobs: check-links: uses: hashicorp/dev-portal/.github/workflows/docs-content-check-legacy-links-format.yml@475289345d312552b745224b46895f51cc5fc490 with: repo-owner: "hashicorp" repo-name: "vagrant" commit-sha: ${{ github.sha }} mdx-directory: "website/content" nav-data-directory: "website/data" ================================================ FILE: .github/workflows/code.yml ================================================ on: push: branches: - 'main' - 'spec-test-*' jobs: sync-acceptance: if: github.repository == 'hashicorp/vagrant' runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false fetch-depth: 0 - name: Sync Acceptance Testing Repository run: ./.ci/sync.sh working-directory: ${{github.workspace}} env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} HASHIBOT_USERNAME: ${{ secrets.HASHIBOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} ================================================ FILE: .github/workflows/dev-appimage-build.yml ================================================ name: Appimage Vagrant Development Build on: push: branches: 'dev-appimage-*' workflow_dispatch: jobs: trigger-build: if: github.repository == 'hashicorp/vagrant' name: Trigger Vagrant Appimage Development Build runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Trigger Development Build run: ./.ci/dev-build "${BRANCH}" "${COMMIT_ID}" build-appimage env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} BRANCH: ${{ github.ref_name }} COMMIT_ID: ${{ github.sha }} ================================================ FILE: .github/workflows/dev-arch-build.yml ================================================ name: Arch Linux Vagrant Development Build on: push: branches: 'dev-arch-*' workflow_dispatch: jobs: trigger-build: if: github.repository == 'hashicorp/vagrant' name: Trigger Vagrant Arch Linux Development Build runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Trigger Development Build run: ./.ci/dev-build "${BRANCH}" "${COMMIT_ID}" build-arch env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} BRANCH: ${{ github.ref_name }} COMMIT_ID: ${{ github.sha }} ================================================ FILE: .github/workflows/dev-build.yml ================================================ name: Full Vagrant Development Build on: push: branches: 'build-*' workflow_dispatch: jobs: trigger-build: if: github.repository == 'hashicorp/vagrant' name: Trigger Vagrant Development Build runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Trigger Development Build run: ./.ci/dev-build "${BRANCH}" "${COMMIT_ID}" build env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} BRANCH: ${{ github.ref_name }} COMMIT_ID: ${{ github.sha }} ================================================ FILE: .github/workflows/dev-debs-build.yml ================================================ name: DEB Vagrant Development Build on: push: branches: 'dev-debs-*' workflow_dispatch: jobs: trigger-build: if: github.repository == 'hashicorp/vagrant' name: Trigger Vagrant DEB Development Build runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Trigger Development Build run: ./.ci/dev-build "${BRANCH}" "${COMMIT_ID}" build-debs env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} BRANCH: ${{ github.ref_name }} COMMIT_ID: ${{ github.sha }} ================================================ FILE: .github/workflows/dev-macos-build.yml ================================================ name: macOS Vagrant Development Build on: push: branches: 'dev-macos-*' workflow_dispatch: jobs: trigger-build: if: github.repository == 'hashicorp/vagrant' name: Trigger Vagrant macOS Development Build runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Trigger Development Build run: ./.ci/dev-build "${BRANCH}" "${COMMIT_ID}" build-macos env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} BRANCH: ${{ github.ref_name }} COMMIT_ID: ${{ github.sha }} ================================================ FILE: .github/workflows/dev-rpms-build.yml ================================================ name: RPM Vagrant Development Build on: push: branches: 'dev-rpms-*' workflow_dispatch: jobs: trigger-build: if: github.repository == 'hashicorp/vagrant' name: Trigger Vagrant RPM Development Build runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Trigger Development Build run: ./.ci/dev-build "${BRANCH}" "${COMMIT_ID}" build-rpms env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} BRANCH: ${{ github.ref_name }} COMMIT_ID: ${{ github.sha }} ================================================ FILE: .github/workflows/dev-windows-build.yml ================================================ name: Windows Vagrant Development Build on: push: branches: 'dev-windows-*' workflow_dispatch: jobs: trigger-build: if: github.repository == 'hashicorp/vagrant' name: Trigger Vagrant Windows Development Build runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Trigger Development Build run: ./.ci/dev-build "${BRANCH}" "${COMMIT_ID}" build-windows env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} BRANCH: ${{ github.ref_name }} COMMIT_ID: ${{ github.sha }} ================================================ FILE: .github/workflows/initiate-release.yml ================================================ name: Start Vagrant Release Process on: workflow_dispatch: inputs: release_version: description: 'Release Version (example: 1.0.0)' required: true type: string jobs: start-release: if: github.repository == 'hashicorp/vagrant' name: Initiate Release runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # NOTE: custom token is used so pushed tag will trigger release workflow token: ${{ secrets.HASHIBOT_TOKEN }} - name: Run initiator run: ./.ci/release-initiator "${VERSION}" env: VERSION: ${{ inputs.release_version }} HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} HASHIBOT_USERNAME: ${{ vars.HASHIBOT_USERNAME }} HASHIBOT_EMAIL: ${{ vars.HASHIBOT_EMAIL }} ================================================ FILE: .github/workflows/lock.yml ================================================ name: 'Lock Threads' on: schedule: - cron: '50 1 * * *' jobs: lock: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: github-token: ${{ github.token }} issue-inactive-days: '30' pr-inactive-days: '30' ================================================ FILE: .github/workflows/nightlies.yml ================================================ name: Vagrant Nightly Builds on: schedule: - cron: 30 4 * * * jobs: trigger-nightly: if: github.repository == 'hashicorp/vagrant' name: Trigger Nightly Build runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Trigger Nightly Build run: ./.ci/nightly-build "${COMMIT_ID}" env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} COMMIT_ID: ${{ github.sha }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Vagrant Release on: push: tags: 'v*' jobs: trigger-release: if: github.repository == 'hashicorp/vagrant' name: Trigger Installers Build runs-on: ubuntu-latest steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Trigger Build run: ./.ci/release "${TAG}" "${COMMIT_ID}" env: HASHIBOT_TOKEN: ${{ secrets.HASHIBOT_TOKEN }} TAG: ${{ github.ref }} COMMIT_ID: ${{ github.sha }} ================================================ FILE: .github/workflows/testing-skipped.yml ================================================ name: Vagrant Ruby Tests on: pull_request: branches: - main ignored-paths: - 'bin/**' - 'lib/**' - 'plugins/**' - 'test/**' - 'Gemfile' - 'templates/**' - 'vagrant.gemspec' - 'Rakefile' jobs: unit-tests-ruby: runs-on: ubuntu-latest continue-on-error: true strategy: matrix: ruby: [ '3.1', '3.2', '3.3', '3.4' ] name: Vagrant unit tests on Ruby ${{ matrix.ruby }} steps: - name: Stubbed for skip run: "echo 'No testing required in changeset'" ================================================ FILE: .github/workflows/testing.yml ================================================ name: Vagrant Ruby Tests on: push: branches: - main - 'test-*' paths: - 'bin/**' - 'lib/**' - 'plugins/**' - 'test/**' - 'templates/**' - 'Gemfile' - 'vagrant.gemspec' - 'Rakefile' pull_request: branches: - main paths: - 'bin/**' - 'lib/**' - 'plugins/**' - 'test/**' - 'Gemfile' - 'templates/**' - 'vagrant.gemspec' - 'Rakefile' jobs: unit-tests-ruby: runs-on: ubuntu-latest continue-on-error: true strategy: matrix: ruby: [ '3.1', '3.2', '3.3', '3.4' ] name: Vagrant unit tests on Ruby ${{ matrix.ruby }} steps: - name: Code Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Setup Ruby uses: ruby/setup-ruby@dffb23f65a78bba8db45d387d5ea1bbd6be3ef18 # v1.293.0 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: install dependencies run: | sudo apt-get update sudo apt-get install -yq libarchive-tools - name: Run Tests run: bundle exec rake test:unit ================================================ FILE: .gitignore ================================================ # OS-specific .DS_Store # Editor swapfiles .*.sw? *~ # Vagrant stuff acceptance_config.yml boxes/* /.vagrant /website/.vagrant /website/build /vagrant-spec.config.rb test/vagrant-spec/.vagrant/ .vagrant/ /vagrant /pkg data.db vagrant-restore.db.lock cmd/vagrant/Vagrantfile binstubs/ # Bundler/Rubygems *.gem .bundle pkg/* /Gemfile.lock test/tmp/ /exec .ruby-bundle vendor/bundle # Documentation _site/* .yardoc/ doc/ # Python *.pyc # Rubinius *.rbc # IDE junk .idea/* *.iml .project .vscode # Ruby Managers .rbenv .rbenv-gemsets .ruby-gemset .ruby-version .rvmrc # Box storage for spec test/vagrant-spec/boxes/*.box simple_speed.py cover.out # nektos/act secrets file .secrets # delve debug binary __debug_bin # solargraph (ruby lsp) & rubocop .solargraph.yml .rubocop.yml # Ignore generated binaries bin/vagrant-go* # extension tmp* lib/vagrant/vagrant_ssl.so # direnv directory .direnv ================================================ FILE: .gitmodules ================================================ ================================================ FILE: .vimrc ================================================ " tabstop settings set tabstop=2 set softtabstop=2 set shiftwidth=2 set expandtab ================================================ FILE: .yardopts ================================================ -m markdown ================================================ FILE: CHANGELOG.md ================================================ ## 2.4.10.dev (UNRELEASED) FEATURES: - provider/virtualbox: Add support for LSI Logic SAS storage controller [GH-13692] IMPROVEMENTS: - provisioner/ansible: Add support for dynamically selecting ansible-playbook inventory argument option based on ansible-core version [GH-13740] - docs: Add deprecation notice for documentation under website directory [GH-13746] - docs: Fix typos and lingustic issues in various places [GH-13731] BUG FIXES: - core: Fix issue with missing translations when running vagrant login [GH-13747] - provider/hyperv: Preserve primary disk when resizing disks [GH-13748] ## 2.4.9 (August 21, 2025) FEATURES: - provider/virtualbox: Add support for VirtualBox 7.2 [GH-13709] IMPROVEMENTS: - docs: Update the node version for the docs site [GH-13713] - docs: Remove outdated link for Vagrant Cloud [GH-13710] BUG FIXES: - provisioner/ansible: Fix OS version detection, when installing Ansible on RHEL-like operating systems [GH-13701] ## 2.4.8 (August 05, 2025) IMPROVEMENTS: - core: Improve error message when no matching provider is found for a box [GH-13693] - core: Improve error message on box add failures [GH-13687] BUG FIXES: - core: Fix box add action when adding a box directly from a file [GH-13699] - provider/hyperv: Fix XML configuration parsing logic, and add additional checks for minimum amount of memory and CPU [GH-13691] - core: Fix guest network configuration when more than one network interface is present [GH-13686] ## 2.4.7 (June 17, 2025) BUG FIXES: - guests/linux: Fix /etc/fstab clean up behavior [GH-13681] - provider/docker: Fix auto generated container names [GH-13678] - provider/hyperv: Fix import for XML based configuration [GH-13674] ## 2.4.6 (May 21, 2025) IMPROVEMENTS: - core: Improve error messages on box add failures [GH-13660] - core: Only generate and attach ISO for cloud-init on first boot [GH-13666] - host/windows: Add basic oscdimg detection on Windows [GH-13668] - provider/hyperv: Enable cloud-init support [GH-13671] - provider/virtualbox: Allow link-local IPv6 addresses for hostonly [GH-12653] BUG FIXES: - command: Remove server mode checks [GH-13657] - core: Prevent cloud-init from regenerating and attaching ISO [GH-13666] - provider/hyperv: Extract machine ID if collection returned [GH-13669] - provider/hyperv: Fix import failure due to lack of resources [GH-13670] - provider/virtualbox: Fix VirtualBox private network setup [GH-13659] ## 2.4.5 (April 23, 2025) FEATURES: - communicator/none: Add experimental none communicator [GH-13651] BUG FIXES: - core/bundler: Handle multiple versions for system specifications [GH-13652] ## 2.4.4 (April 21, 2025) IMPROVEMENTS: - communicator/ssh: Update connect retry behavior, make configurable [GH-13628] - core: Better behavior outside installers [GH-13636] - guest/amazonlinux: Support networkd based configuration [GH-13626] - guest/arch: Update networking for recent versions [GH-13640] - guest/rhel: Fix networking setup in recent versions [GH-13625] - host/darwin: Remove HFS from ISO creation [GH-13561] - provider/hyperv: Add dvd disk support [GH-13642] - provider/hyperv: Update primary disk detection [GH-13643] - provider/virtualbox: Add VirtioSCSI storage controller support [GH-13587] - util/powershell: Prefer using pwsh when available [GH-13648] BUG FIXES: - command/box: Fix architecture constraints in outdated/updated command [GH-13601] - command/box: Fix architecture constraint on provider matches [GH-13647] - communicators/winrm: Catch IO::Timeout when waiting for communicator [GH-13606] - communicators/ssh: Catch IO::Timeout when waiting for communicator [GH-13606] - guest/alpine: Fix DHCP assigned default route behavior [GH-13633] - provider/docker: Handle variation in error text during image removal [GH-13564] - provider/virtualbox: Use interface name for hostonly configuration [GH-13644] - synced_folder/smb: Remove `nofail` mount option [GH-13645] VAGRANT-GO: - Removed with work archived to vagrant-go branch [GH-13622] ## 2.4.3 (November 12, 2024) IMPROVEMENTS: - command/cloud: Support HCP authentication [GH-13540] BUG FIXES: - core: Relax constraint on logger dependency [GH-13532] ## 2.4.2 (November 01, 2024) FEATURES: - provider/virtualbox: Add support for VirtualBox 7.1 [GH-13513] IMPROVEMENTS: - core: Always downcase type value when getting digest for checksum calculation [GH-13471] - core: Activate all runtime dependencies at startup [GH-13526] - guest/debian: Fix NFS install capability to prevent hang on install [GH-13411] - kernel_v2/config: Add warning for IPv6 address ending with :1 [GH-13362] - provider/docker: Properly match container ID when trailing output is present [GH-13475] - provider/docker: Support build with containerd storage [GH-13343] - provider/virtualbox: Allow paused state when booting vm [GH-13496] - provider/virtualbox: Handling warnings in output when detecting version [GH-13394] - synced_folder/nfs: Output mounting entry [GH-13383] - synced_folder/smb: Adjust ordering in mount entry output [GH-13383] BUG FIXES: - command/cloud: Fix provider upload [GH-13467] - host/bsd: Use nfsd update command instead of restart [GH-13490] - kernel_v2/config: Fix IP address check [GH-13494] - provider/docker: Prevent error if network configuration data is missing [GH-13337, GH-13373] - provider/docker: Fix docker-exec commands to pass kwargs correctly [GH-13488] - provider/docker: Fix docker to not rebuild image if it already exists [GH-13489] - provider/virtualbox: Prevent encoding errors within error translation [GH-13525] - provider/hyperv: Fix configure disks capability [GH-13346] - provisioner/ansible: Fix version detection [GH-13375] - provisioner/ansible: Support double digit versions [GH-13493] - provisioner/salt: Fix bootstrap script URLs [GH-13517] - synced_folder/nfs: Fix upstart detection [GH-13409] VAGRANT-GO: ## 2.4.1 (January 19, 2024) FEATURES: IMPROVEMENTS: - communicator/ssh: Support ECDSA type keys for insecure key replacement [GH-13327] - communicator/ssh: Inspect guest for supported key types [GH-13334] - core: Update Ruby constraint to allow Ruby 3.3 [GH-13335] - core/bundler: Force strict dependencies for default gems [GH-13336] - provisioner/ansible: Support pip installation for RHEL >= 8 [GH-13326] - util/keypair: Add support for ECDSA keys [GH-13327] BUG FIXES: - command/plugin: Fix plugin extension installation on Windows [GH-13328] - communicator/ssh: Fix private key writing on Windows [GH-13329] - core: Fix Vagrant SSL helper detection on macOS [GH-13277] - core: Fix box collection sorting [GH-#13320] - util/platform: Fix architecture mapping for Windows [GH-13278] VAGRANT-GO: ## 2.4.0 (October 16, 2023) FEATURES: - core: Add architecture support [GH-13239] IMPROVEMENTS: - communicator/ssh: Add key type detection on insecure key replacement [GH-13219] - core: Extract box files as sparse files [GH-13252] - keys: Add ed25519 insecure private key [GH-13219] - util/downloader: Perform best effort revocation checks on Windows [GH-13214] - util/keypair: Add support for generating ed25519 key pairs [GH-13219] BUG FIXES: - core: Fix extension installation path [GH-13215] - provider/virtualbox: Fix ipv6 static network configuration [GH-13241] VAGRANT-GO: - Add basic support for HCL based config [GH-13257] ## 2.3.7 (June 15, 2023) IMPROVEMENTS: - command/ssh: Enable deprecated key types and algorithms [GH-13179] - core: Update user error message on failed extension installs [GH-13207] - core: Support loading legacy providers in OpenSSL 3 [GH-13178] - provisioner/salt: Verify bootstrap-salt download [GH-13166] BUG FIXES: - communicator/ssh: Remove keyboard-interactive auth method [GH-13194] - provisioner/salt: Fix usage on Windows guests [GH-13086] VAGRANT-GO: - Update data layer implementation [GH-12904] - Update dependencies [GH-13201] ## 2.3.6 (May 19, 2023) BUG FIXES: - command/serve: Isolate proto constants to file for auto-loading [GH-13165] - core/util: Unlock file prior to deletion [GH-13159] - provider/docker: Attempt using docker command for bridge ip [GH-13153] - provider/virtualbox: Update preferred locale values for driver [GH-13160] ## 2.3.5 (May 15, 2023) BUG FIXES: - communicator/ssh: Use netssh builtin keep alive functionality [GH-13069] - communicator/ssh: Update connection settings when using a password to connect ssh [GH-13052] - core: Add a file mutex when downloading box files [GH-13057] - guest/arch: Support differentiating between Artix and Arch Linux [GH-13055] - host/windows: Get state of Windows feature "Microsoft-Hyper-V-Hypervisor" [GH-11933] - provider/docker: Ignore inactive docker containers when assigning ports [GH-13146] - provider/docker: Sync folders before preparing nfs settings [GH-13149] - provider/virtualbox: De-duplicate machine port forward info [GH-13056] - provider/virtualbox: Remove check for hyperv being enabled when verifying virtualbox is usable on windows [GH-13090] - provider/virtualbox: Validate LANG value when possible [GH-13150] - provider/hyperv: Check for hyper-v feature "EnhancedSessionTransportType" [GH-12280] - provisioner/ansible: Fix installing Ansible provisioner with version and pip [GH-13054] - synced_folders/rsync: allow rsync-auto to also ignore relative paths [GH-13066] NOTE: Vagrant installer packages were updated to Ruby 3 ## 2.3.4 (December 9, 2022) IMPROVEMENTS: - host/darwin: Isolate loading incompatible libraries to support EOL platforms [GH-13022] - provider/virtualbox: Detect network type when not provided [GH-13024] BUG FIXES: - host/windows: Add fix for Powershell 7.3.0 [GH-13006] - provider/virtualbox: Adjust hostnet DHCP configuration, ignore invalid devices [GH-13004] - provisioner/ansible: Fix install package names on older debian (and derivatives) versions [GH-13017] ## 2.3.3 (November 15, 2022) IMPROVEMENTS: - core: Bump net-ssh dependency to 7.0 and remove patches [GH-12979] - synced_folders/rsync: Include ssh `extra_args` value [GH-12973] BUG FIXES: - command/serve: Force root level namespace for Google constant [GH-12989] - guest/solaris: Fix insecure key authorized keys removal [GH-12740] - provider/virtualbox: Fix `:private_network` support for VirtualBox 7 on macOS [GH-12983] - provider/virtualbox: Prevent localization of command output [GH-12994] - provisioner/ansible: Update setup packages in debian capability [GH-12832] ## 2.3.2 (October 18, 2022) FEATURES: - provider/virtualbox: Add support for VirtualBox 7.0 [GH-12947] ## 2.3.1 (September 29, 2022) IMPROVEMENTS: - core: Raise error if required metadata.json box fields are not present [GH-12895] - core: Provider helpful error when box version format is invalid [GH-12911] BUG FIXES: - Fix flakiness when bringing up a machine that forwards ssh [GH-12909] - communicator/ssh: Fix `private_key_path` behavior when `keys_only` is disabled [GH-12885] - synced_folder/nfs: Update exports file creation [GH-12910] - util/downloader: Fix user agent [GH-12925] VAGRANT-GO: - Support secret interactive input [GH-12876] - Support terminal coloring [GH-12888] - Validate if requested provider is usable and append/prepend information to errors [GH-12898] - Raise error if required metadata.json box fields are not present [GH-12919] ## 2.3.0 (August 5, 2022) FEATURES: - core: Introduce vagrant-go [GH-12819] IMPROVEMENTS: - core: Set rsa-sha2 in kex algorithm set to enable in key exchange [GH-12584] - core/bundler: Improve Gem spec selection when resolving [GH-12567] - push/heroku: Display output from push [GH-12646] BUG FIXES: - host/darwin: Fix `NameError` for version capability [GH-12581] - push/ftp: Fix `VAGRANT_CWD` handling [GH-12645] - guests/redhat: Fix NFS shares on Rocky 9 guests [GH-12813] ## 2.2.19 (November 5, 2021) IMPROVEMENTS: - guest/suse: Add fallback shutdown for versions without systemd [GH-12489] - provider/virtualbox: Validate VirtualBox hostonly network range [GH-12564] BUG FIXES - guest/atomic: Update detection to prevent matching on non-atomic guests [GH-12575] - guest/coreos: Fix configure network capability [GH-12575] - guest/windows: Fix directory creation with rsync [GH-11880] - host/windows: Properly handle spaces in path to SSH key [GH-12398] - provisioner/chef: Update install checks [GH-12555] ## 2.2.18 (July 27, 2021) BUG FIXES: - core: Fix of plugin manager kwargs [GH-12452] - providers/docker: Pass in docker command opts as a map [GH-12449] - providers/hyperv: Fix network address detection [GH-12472] ## 2.2.17 (July 7, 2021) FEATURES: - guest/rocky: Add guest support for Rocky Linux [GH-12440] IMPROVEMENTS: - command/package: Add --info flag to package command [GH-12304] - guest/debian: Retry network setup on debain [GH-12421] - guest/suse: Use systemctl poweroff in the background instead of shutdown [GH-12439] - guest/windows: Get powershell path in %WINDIR%/System32 [GH-12436] - host/windows: Check Domain and Application Directory contexts for credentials when validating SMB creds [GH-12428] - providers/hyper-v: Fix IP detection when multiple devices are attached [GH-12232] - provisioner/ansible: Detects new versions of ansible-4.0.0+ [GH-12391] - provisioner/ansible: Strip whitespace from ansible version [GH-12420] - provisioner/salt: Always use upstream Salt bootstrap script on Windows [GH-12127] - provisioner/salt: Use more conservative TLS settings to work on older .NET versions [GH-12413] - provisioner/shell: Buffer output to display full lines [GH-12437] - vagrant: Updates to support Ruby 3.0 [GH-12427] BUG FIXES: - command/cloud: Fix authentication middleware to prevent breaking local paths [GH-12419] - communicator/ssh: Fix net-ssh patches for RSA signatures [GH-12415] - core: Add box directly with authed urls [GH-12278] ## 2.2.16 (April 29, 2021) IMPROVEMENTS: - guest/linux: Detect in process shutdown in reboot capability [GH-12302] - util/powershell: Support `pwsh` executable in new versions of powershell [GH-12335] BUG FIXES: - communicator/ssh: Properly handle authentication with RSA keys [GH-12298] - guest/fedora: Import guest detection module [GH-12275] - guest/linux: Fix SMB folder mount name capability call [GH-12281] - provider/docker: Properly handle updated buildkit build output [GH-12300] ## 2.2.15 (March 30, 2021) IMPROVEMENTS: - command/cloud: Remove access token URL parameter by default [GH-12234] - command/cloud: Add VAGRANT_SERVER_ACCESS_TOKEN_BY_URL to revert access token behavior [GH-12252] - core: Bump vagrant_cloud dependency to 3.0.3 [GH-12200] - core: Bump listen gem version and remove ruby_dep [GH-12148] - core: Bump vagrant_cloud dependency to 3.0.4 [GH-12242] - core/bundler: Update resolution handling when outside of installer and bundler [GH-12225] - core/plugin: Provide friendlier error messages on install fail when possible [GH-12225] - guest/openwrt: Add support for OpenWrt guests [GH-11791] - guest/freebsd: FreeBSD updated ansible to py37-ansible [GH-12201] - provider/virtualbox: Get default dhcp ip from a matching host ip [GH-12211] - util/downloader: Prevent redirect notification for default store [GH-12235] BUG FIXES: - command/cloud: Automatically disable direct uploads when file is too large [GH-12250] - core: Make shell script for loop shell agnostic [GH-12205] - core: Raise error if downloading box metadata fails [GH-12189] - core: Apply download options to metadata requests [GH-12177] - core: Don't try to find "" by prefix in the machine index [GH-12188] - core: Don't count not created machines as declined when destroying [GH-12186] - core: Bump bcrypt_pbkdf version [GH-12216] - core: Remove all space from checksums [GH-12168] - core/bundler: Do not include default gems as pinned constraints [GH-12253] - core/synced_folders: Extract os friendly mount name for vbox shared folders [GH-12184] - guest/alpine: Check if interface exists before shutting it down [GH-12181] - guest/nixos: Fix network config for recent NixOS releases [GH-12152] - guest/fedora: Detect fedora using os-releases id [GH-12230] ## 2.2.14 (November 20, 2020) IMPROVEMENTS: - host/windows: Update filesystem type matching on WSL2 [GH-12056] - provisioner/salt: Modernize Salt bootstrap script [GH-12135] BUG FIXES: - core: Track raw actions as they are applied to prevent multiple insertions [GH-12037] - core/bundler: Update solution file resolution to support prerelease matching [GH-12054] - guest/darwin: Mount vmware synced folders for big sur guests [GH-12053] ## 2.2.13 (November 06, 2020) BUG FIXES: - core/bundler: Adjust request sets properly with non-development prerelease [GH-12025] ## 2.2.12 (November 06, 2020) BUG FIXES: - core/bundler: Automatically enable prerelease dependency resolution [GH-12023] NOTE: This is a fix release to resolve an immediate issue with Vagrant plugin functionality ## 2.2.11 (November 05, 2020) IMPROVEMENTS: - command/cap: Add ability to specify target [GH-11965] - command/cloud: Add --force flag to `version release` command [GH-11912] - command/cloud: Updates to utilize the 3.0 version of vagrant_cloud [GH-11916] - core: Switch from unmaintained gem erubis to erubi [GH-11893] - core: Download Vagrant boxes using auth headers [GH-11835] - core: Remove dependency on mime gem [GH-11857] - core: Handle Errno::EALREADY exceptions on port check [GH-12008] - core: Fix missing hook/trigger insertion into action stack [GH-12014] - guest/linux: Make max reboot wait duration configurable [GH-12011] - guest/windows: Make max reboot wait duration configurable [GH-12011] - providers/virtualbox: Fix availability check of provider [GH-11936] - tests: Add integration tests for Docker provider [GH-11907] BUG FIXES: - core/synced_folders: Don't persist synced folders to fstab if guest is not reachable [GH-11900] - core: Don't try to recover machine without a uuid [GH-11863] - config/disks: Transform provider specific config to common form [GH-11939] - config/vm: Override synced-folder `:nfs` option [GH-11988] - contrib/zsh: Remove newline from beginning of completion script [GH-11963] - guests/arch: Install smbclient when setting up arch smb [GH-11923] - guest/linux: Check for /etc/fstab before trying to modify [GH-11897] - guest/linux: Create an /etc/fstab if does not exist [GH-11909] - guest/linux: Persist SMB mounts [GH-11846] - guest/debian: Set hostname in /etc/hosts as first step to changing hostname [GH-11885] - guest/rhel: Check for existence of network files before trying to update them [GH-11877] - guest/suse: Don't use hostnamectl to set hostname if not available on system [GH-11996] - tests: Remove rsync dependency from tests [GH-11889] ## 2.2.10 (August 24, 2020) FEATURES: - hyperv/disks: Add ability to manage virtual disks for guests [GH-11541] IMPROVEMENTS: - core: Allow provisioners to be run when a communicator is not available [GH-11579] - core: Add `autocomplete` command that allows for install of bash or zsh autocomplete scripts [GH-11523] - core: Update to childprocess gem to 4.0.0 [GH-11717] - core: Add action to wait for cloud-init to finish running [GH-11773] - core: Update to net-ssh to 6.0 and net-sftp to 3.0 [GH-11621] - core: Optimize port in use check for faster validation [GH-11810] - core: Support for Ruby 2.7 [GH-11814] - core: Add synced folder capabilities for mount options and default fstab modification behavior [GH-11797] - guest/arch: Use systemd-networkd to configure networking for guests [GH-11400] - guest/haiku: Rsync install for rsync synced folders [GH-11614] - guest/solaris11: Add guest capability shell_expand_guest_path [GH-11759] - host/darwin: Add ability to build ISO [GH-11694] - hosts/linux: Add ability to build ISO [GH-11750] - hosts/windows: Add ability to build ISO [GH-11750] - providers/hyperv: Add support for SecureBootTemplate setting on import [GH-11756] - providers/hyperv: Add support for EnhancedSessionTransportType [GH-11014] - virtualbox/disks: Add ability to manage virtual dvds for guests [GH-11613] BUG FIXES: - core: Ensure MapCommandOptions class is required [GH-11629] - core: Fix `:all` special value on triggers [GH-11688] - core: Ensure network addresses have a valid netmask [GH-11679] - core: Recover local machine metadata in global index [GH-11656] - core: Print CLI help message is ambiguous option provided [GH-11746] - core: Update how `/etc/hosts` gets updated for darwin, freebsd and openbsd [GH-11719] - core: Capture `[3J` escape sequence [GH-11807] - core: Treat empty box value as invalid [GH-11618] - core: Allow forwarding ports to unknown addresses [GH-11810] - core: Scrub credentials as whole words [GH-11837] - commands/destroy: Add gracefull option to switch beween gracefully or forcefully shutting down a vm [GH-11628] - communicator/ssh: Raise an error for a nil exit status [GH-11721] - communicator/winrm: Check for nil return from querying for forwarded ports [GH-11831] - config/vm: Add option `allow_hosts_modification` to allow/disable Vagrant editing the guests `/etc/hosts` file [GH-11565] - config/vm: Add config option `hostname` to `config.vm.network` [GH-11566] - config/vm: Don't ignore NFS synced folders on Windows hosts [GH-11631] - host: Use regular port check for loopback addresses [GH-11654] - host: Allow windows and linux hosts to detach from rdp process [GH-11732] - host/windows: Properly register SMB password validation capability [GH-11795] - guests: Allow setting of hostname according to `hostname` option for multiple guests [GH-11704] - guest/alpine: Allow setting of hostname according to `hostname` option [GH-11718] - guest/esxi: Be more permissive with permissions of ssh directory [GH-11587] - guest/linux: Add virtual box shared folders to guest fstab [GH-11570] - guest/suse: Allow setting of hostname according to `hostname` option [GH-11567] - providers/docker: Ensure new containers don't grab existing bound ports [GH-11602] - providers/hyperv: Fix check for secure boot [GH-11809] - providers/virtualbox: Fix inability to create disk with same name across multiple guests [GH-11767] - provisioners/docker: Allow to specify docker image version using the `run` option [GH-11806] - provisioners/file: Allow creating empty folders [GH-11805] - provisioners/shell: Ensure Windows shell provisioner gets the correct file extension [GH-11644] - util/powershell: Use correct powershell executable for privileged commands [GH-11787] ## 2.2.9 (May 07, 2020) BUG FIXES: - core/bundler: Properly handle plugin install with available specification [GH-11592] - provisioners/docker: Fix CentOS docker install and start service capabilities [GH-11581] - provisioners/podman: Seperate RHEL install from CentOS install [GH-11584] ## 2.2.8 (May 04, 2020) FEATURES: - virtualbox/disks: Add ability to manage virtual disks for guests [GH-11349] IMPROVEMENTS: - bin/vagrant: Automatically include global options within commands [GH-11473] - bin/vagrant: Suppress Ruby warnings when not running pre-release version [GH-11446] - communicator/ssh: Add support for configuring SSH connect timeout [GH-11533] - core: Update childprocess gem [GH-11487] - core: Add cli option `--no-tty` [GH-11414] - core: Overhaul call stack modifications implementation for hooks and triggers [GH-11455] - core/bundler: Cache plugin solution sets to speed up startup times [GH-11363] - config/vm: Add`box_download_options` config to specify extra download options for a box [GH-11560] - guest/alpine: Add ansible provisioner guest support [GH-11411] - guest/linux: Update systemd? check to use sudo [GH-11398] - guest/linux: Use systemd if available to halt and reboot system [GH-11407] - guests/linux: Mount smb folders with `mfsymlinks` option by default [GH-11503] - guest/redhat: Add support for SMB [GH-11463] - guest/windows: Rescue all regular exceptions during reboot wait [GH-11428] - providers/docker: Support catching container name when using podman [GH-11356] - provisioners/docker: Support Centos8 [GH-11462] - provisioners/podman: Add Podman as a provisioner [GH-11472] - provisioners/salt: Allow specifying python_version [GH-11436] BUG FIXES: - communicators/winssh: Fix issues with Windows SSH communicator [GH-11430] - core/bundler: Activate vagrant specification when not active [GH-11445] - core/bundler: Properly resolve sets when Vagrant is in prerelease [GH-11571] - core/downloader: Always set `-q` flag as first option [GH-11366] - core/hooks: Update dynamic action hook implementation to prevent looping [GH-11427] - core/synced_folders: Validate type option if set [GH-11359] - guests/debian: Choose netplan renderer based on network configuration and installed tools [GH-11498] - host/darwin: Quote directories in /etc/exports [GH-11441] - host/linux: Ensure `/etc/exports` does not contain duplicate records [GH-10591] - host/windows: Check all interfaces for port conflict when host_ip: "0.0.0.0" [GH-11454] - providers/docker: Fix issue where Vagrant fails to remove image if it is in use [GH-11355] - providers/docker: Fix issue with getting correct docker image id from build output [GH-11461] - providers/hyperv: Prevent error when identity reference cannot be translated [GH-11425] - provider/hyperv: Use service id for manipulating vm integration services [GH-11499] - providers/virtualbox: Parse `list dhcpservers` output on VirtualBox 6.1 [GH-11404] - providers/virtualbox: Raise an error if guest IP ends in .1 [GH-11500] - provisioners/shell: Ensure windows shell provisioners always get an extension [GH-11517] - util/io: Fix encoding conversion errors [GH-11571] ## 2.2.7 (January 27, 2020) IMPROVEMENTS: - guest/opensuse: Check for basename hostname prior to setting hostname [GH-11170] - host/linux: Check for modinfo in /sbin if it's not on PATH [GH-11178] - core: Show guest name in hostname error message [GH-11175] - provisioners/shell: Linux guests now support `reboot` option [GH-11194] - darwin/nfs: Put each NFS export on its own line [GH-11216] - contrib/bash: Add more completion flags to up command [GH-11223] - provider/virtualbox: Add VirtualBox provider support for version 6.1.x [GH-11250] - box/outdated: Allow to force check for box updates and ignore cached check [GH-11231] - guest/alpine: Update apk cache when installing rsync [GH-11220] - provider/virtualbox: Improve error message when machine folder is inaccessible [GH-11239] - provisioner/ansible_local: Add pip install method for arch guests [GH-11265] - communicators/winssh: Use Windows shell for `vagrant ssh -c` [GH-11258] BUG FIXES: - command/snapshot/save: Fix regression that prevented snapshot of all guests in environment [GH-11152] - core: Update UI to properly retain newlines when adding prefix [GH-11126] - core: Check if box update is available locally [GH-11188] - core: Ensure Vagrant::Errors are loaded in file_checksum util [GH-11183] - cloud/publish: Improve argument handling for missing arguments to command [GH-11184] - core: Get latest version for current provider during outdated check [GH-11192] - linux/nfs: avoid adding extra newlines to /etc/exports [GH-11201] - guest/darwin: Fix VMware synced folders on APFS [GH-11267] - guest/redhat: Ensure `nfs-server` is restarted when installing nfs client [GH-11212] - core: Do not validate checksums if options are empty string [GH-11211] - provider/docker: Enhance docker build method to match against buildkit output [GH-11205] - provisioner/ansible_local: Don't prompt for input when installing Ansible on Ubuntu and Debian [GH-11191] - provisioner/ansible_local: Ensure all guest caps accept all passed in arguments [GH-11265] - host/windows: Fix regression that prevented port collisions from being detected [GH-11244] - core/provisioner: Set top level provisioner name if set in a provisioner config [GH-11295] ## 2.2.6 (October 14, 2019) FEATURES: - core/provisioners: Introduce new Provisioner options: before and after [GH-11043] - guest/alpine: Integrate the vagrant-alpine plugin into Vagrant core [GH-10975] IMPROVEMENTS: - command/box/prune: Allow prompt skip while preserving actively in use boxes [GH-10908] - command/cloud: Support providing checksum information with boxes [GH-11101] - dev: Fixed Vagrantfile for Vagrant development [GH-11012] - guest/alt: Improve handling for using network tools when setting hostname [GH-11000] - guest/suse: Add ipv6 network config templates for SUSE based distributions [GH-11013] - guest/windows: Retry on connection timeout errors for the reboot capability [GH-11093] - host/bsd: Use host resolve path capability to modify local paths if required [GH-11108] - host/darwin: Add host resolve path capability to provide real paths for firmlinks [GH-11108] - provisioners/chef: Update pkg install flags for chef on FreeBSD guests [GH-11075] - provider/hyperv: Improve error message when VMMS is not running [GH-10978] - provider/virtualbox: Raise additional errors for incomplete virtualbox installation on usable check [GH-10938] - util/filechecksum: Add support for more checksum types [GH-11101] BUG FIXES: - command/rsync-auto: Fix path watcher bug so that all subdirectories are synced when changed [GH-11089] - command/snapshot/save: Ensure VM id is passed to list snapshots for hyper-v provider [GH-11097] - core: Ensure proper paths are shown in config loading exceptions [GH-11056] - guest/suse: Use hostnamectl instead of hostname to set the hostname under SUSE [GH-11100] - provider/docker: Fix default provider validation if password is used [GH-11053] - provider/docker: Fix Docker providers usable? check [GH-11068] - provisioner/ansible_local: Ensure pip_install_cmd is finalized to emptry string [GH-11098] - provisioner/file: Ensure relative path for file provisioner source is relative to guest machines cwd [GH-11099] - provider/docker: Ensure docker build_args option is properly set in docker compose config yaml [GH-11106] - guest/suse: Update nfs & service daemon names for suse based hosts and guests [GH-11076] - provider/docker: Determine ip address prefix workaround for docker public networks [GH-11111] - provider/docker: Only return interfaces where addr is not nil for networks [GH-11116] ## 2.2.5 (June 19, 2019) FEATURES: - providers/docker: Private and Public networking support [GH-10702] IMPROVEMENTS: - command/global-status: Provide machine-readable information [GH-10506] - command/snapshot: Separate snapshot names for guests when listing snapshots [GH-10828] - command/box/update: Ignore missing metadata files when updating all boxes [GH-10829] - core: Use consistent settings when unpacking boxes as root [GH-10707] - core: Write metadata.json file when packaging box [GH-10706] - core: Remove whitespace from id file on load [GH-10727] - core/bundler: Support resolution when installed within system [GH-10894] - guest/coreos: Update network configuration and hostname setting [GH-10752] - guest/freebsd: Add proper VirtualBox share folders support for FreeBSD guests [GH-10717] - guest/freebsd: Add unmount share folder for VirtualBox guests [GH-10761] - guest/freebsd: Simplify network interface listing when configuring networks [GH-10763] - providers/docker: Add usable? check to docker provider [GH-10890] - synced_folder/smb: Remove configuration information from synced folder data [GH-10811] BUG FIXES: - command/box/update: Ensure the right version is picked when updating specific boxes [GH-10810] - command/cloud: Properly set variable from CLI argument parsing for `username` field [GH-10726] - command/rsync_auto: Use relative paths to machines folder path for file path Listener [GH-10902] - communicator/ssh: Remove net/sftp loading to prevent loading errors [GH-10745] - contrib/bash: Search for running_vm_list only in `machines` folder [GH-10841] - core/bundler: Properly parse multiple constants when installing plugins [GH-10896] - core/environment: Support plugin configuration within box Vagrantfiles [GH-10889] - core/triggers: Fix typo in UI output [GH-10748] - core/triggers: Properly exit with abort option [GH-10824] - core/triggers: Ensure guest names are string when filtering trigger configs [GH-10854] - core/triggers: Abort after all running processes have completed when parallel is enabled [GH-10891] - guest/void: Fix NFS capability detection [GH-10713] - guest/bsd: Properly set BSD options order for /etc/exports [GH-10909] - host/windows: Fix rubygems error when host has directory named `c` [GH-10803] - provider/virtualbox: Ensure non-existent machines do not attempt to list snapshots [GH-10784] - provider/docker: Properly set docker-compose config file with volume names [GH-10820] - provisioner/ansible: Fix pip installer hardcoded curl get_pip.py piped to python [GH-10625] - provisioner/chef: Update chef install check for guests [GH-10917] - synced_folders/rsync: Remove rsync__excludes from command if array is empty [GH-10901] ## 2.2.4 (February 27, 2019) FEATURES: - core/triggers: Introduce new option `:type` for actions, hooks, and commands [GH-10615] IMPROVEMENTS: - communicator/ssh: Update `#upload` behavior to work properly with new sshd path checks [GH-10698] - communicator/winrm: Update `#upload` behavior to match ssh communicator upload behavior [GH-10698] - guest/windows: Add reboot output to guest capability [GH-10638] - provisioner/file: Refactor path modification rules and allow communicator to handle details [GH-10698] BUG FIXES: - core: Fix format finalization of plugins in Vagrantfile [GH-10664] - core: Fix SIGINT behavior and prevent backtrace [GH-10666] - core: Change remaining box_client_cert refs to box_download_client_cert [GH-10622] - core: Move over AddAuthentication middleware and hooks out of deprecated class [GH-10686] - guest/debian: Properly set DHCP for systemd-networkd ips [GH-10586] - guest/solaris11: Create interface if required before configuration [GH-10595] - installers/appimage: Use ld path with appimage libs on suffix [GH-10647] - providers/docker: Expand paths when comparing synced folders on reload [GH-10645] - providers/virtualbox: Fix import paths on Windows with VirtualBox 6 [GH-10629] - synced_folders/rsync: Properly clean up tmp folder created during rsync [GH-10690] ## 2.2.3 (January 9, 2019) FEATURES: - host/void: Add host support for void linux [GH-10012] IMPROVEMENTS: - command/rsync-auto: Prevent crash on post-rsync command failure [GH-10515] - command/snapshot: Raise error for bad subcommand [GH-10470] - command/package: Ensure temp dir for package command is cleaned up [GH-10479] - command/powershell: Support running elevated commands [GH-10528] - communicator/ssh: Add `config` and `remote_user` options [GH-10496] - core: Display version update on stderr instead of stdout [GH-10482] - core: Add experimental feature flag [GH-10485] - core: Show box version during box outdated check [GH-10573] - guest/windows: Modify elevated username only on username failure [GH-10488] - host/windows: Prevent SMB setup commands from becoming too long [GH-10489] - host/windows: Automatically answer yes when pruning SMB shares [GH-10524] - provisioners/file: Show source and destination locations with file provisioner [GH-10570] - provisioners/salt: Validate that `install_type` is set if `version` is specified [GH-10474] - provisioners/salt: Update default install version [GH-10537] - provisioners/shell: Add `reboot` option for rebooting supported guest [GH-10532] - synced_folders/rsync: Support using rsync `--chown` option [GH-10529] - util/guest_inspection: Validate hostnamectl command works when detected [GH-10512] - util/platform: Use wslpath command for customized root on WSL [GH-10574] BUG FIXES: - command/cloud publish: Ensure box file exists before path expanding [GH-10468] - command/cloud publish: Catch InvalidVersion errors from vagrant_cloud client [GH-10513] - command/snapshot: Retain consistent provisioning behavior across all commands [GH-10490] - command/validate: Bypass install checks for validating configs with the `--ignore-provider` flag [GH-10467] - communicator/ssh: Fix garbage output detection [GH-10571] - guest/alt: Fix network configuration errors [GH-10527] - guest/coreos: Fix grep command for network interface of CoreOS guest [GH-10554] - guest/freebsd: Fix defaultrouter rcvar in static network template [GH-10469] - guest/redhat: Fix network configuration errors [GH-10527] - providers/virtualbox: Adjust version requirement for NIC warning [GH-10486] - util/powershell: Use correct Base64 encoding for encoded commands [GH-10487] ## 2.2.2 (November 27, 2018) BUG FIXES: - providers/virtualbox: Update default_nic_type implementation and add warning [GH-10450] ## 2.2.1 (November 15, 2018) FEATURES: - core/plugins: Add reset! method to communicator [GH-10399] - providers/virtualbox: Add support for VirtualBox 6.0 [GH-10379] IMPROVEMENTS: - command/validate: Allow validation of config while ignoring provider [GH-10351] - communicators/ssh: Prevent overly verbose output waiting for connection [GH-10321] - communicators/ssh: Support ed25519 keys [GH-10365] - communicators/ssh: Add reset! implementation [GH-10399] - communicators/winrm: Add reset! implementation [GH-10399] - core: Limit number of automatic box update checks [GH-10359] - host/windows: Remove PATH check in WSL detection [GH-10313] - providers/hyperv: Disable automatic checkpoints before deletion [GH-10406] - providers/virtualbox: Add `automount` flag if specified with synced_folder [GH-10326] - providers/virtualbox: Refactor host only network settings [GH-7699] - providers/virtualbox: Support setting default NIC type for network adapters [GH-10383] - providers/virtualbox: Update ssh_port helper to handle multiple matches [GH-10409] - provisioners/shell: Add :reset option to allow communicator reset [GH-10399] - synced_folders/smb: Allow for 'default' smb_username in prompt if set [GH-10319] - util/network_ip: Simplify `network_address` helper [GH-7693] - util/platform: Prevent hard failure during hyper-v enabled check [GH-10332] BUG FIXES: - command/login: Only show deprecation warning when command is invoked [GH-10374] - core: Fallback to Vagrantfile defined box information [GH-10368] - core/bundler: Update source ordering to properly resolve with new RubyGems [GH-10364] - core/triggers: Only split inline script if host is non-Windows [GH-10405] - communicator/winrm: Prepend computer name to username when running elevated commands [GH-10387] - guest/debian: Fix halting issue when setting hostname by restarting networking on guest [GH-10301, GH-10330] - guest/linux: Fix vagrant user access to docker after install [GH-10399] - guest/windows: Add reboot capability to fix hostname race condition [GH-10347] - guest/windows: Allow for reading key paths with spaces [GH-10389] - host/windows: Fix powershell to properly handle paths with spaces [GH-10390] - providers/docker: Deterministic host VM synced folder location for Docker VM [GH-10311] - providers/hyperv: Fix network vlan configuration script [GH-10366] - providers/hyperv: Properly output error message on failed guest import [GH-10404] - providers/hyperv: Fix typo in network configuration detection script [GH-10410] ## 2.2.0 (October 16, 2018) FEATURES: - command/cloud: Introduce `vagrant cloud` subcommand to Vagrant [GH-10148] - command/upload: Add command for uploading files to guest [GH-10263] - command/winrm: Add command for executing guest commands via WinRM [GH-10263] - command/winrm-config: Add command for providing WinRM configuration [GH-10263] IMPROVEMENTS: - core: Ensure file paths are identical when checking for cwd [GH-10220] - core: Add config option `ignore_box_vagrantfile` for ignoring vagrantfile inside box [GH-10242] - core/triggers: Add abort option to core triggers [GH-10232] - core/triggers: Introduce `ruby` option for trigger [GH-10267] - contrib/bash: Add completion for snapshot names for vagrant snapshot restore|delete [GH-9054] - providers/docker: Build docker from git repo [GH-10221] - providers/hyperv: Update Hyper-V admin check and allow override via ENV variable [GH-10275] - providers/virtualbox: Allow base_mac to be optional [GH-10255] - provisioners/salt: bootstrap-salt.sh: use -s with curl [GH-9432] - provisioners/salt: remove leading space with bootstrap_options [GH-9431] BUG FIXES: - core/environment: Provide rgloader for local plugin installations [GH-10279] - contrib/sudoers/osx: Fix missing comma and add remove export alias [GH-10235] - guest/redhat: Update restart logic in redhat change_host_name cap [GH-10223] - guest/windows: Allow special characters in SMB password field [GH-10219] - providers/hyperv: Only use AutomaticCheckpointsEnabled when available [GH-10264] - providers/hyperv: Only use CheckpointType when available [GH-10265] - provisioners/ansible: Fix remote directory creation [GH-10259, GH-10258] - provisioners/puppet: Properly set env variables for puppet provisioner on windows [GH-10218] - provisioners/salt: Properly set salt pillar variables for windows guests [GH-10215] - synced_folders/rsync: Ensure unique tmp dirs for ControlPath with rsync [GH-10291] ## 2.1.5 (September 12, 2018) IMPROVEMENTS: - core: Add `Vagrant.version?` helper method [GH-10191] - core: Scrub sensitive values from logger output [GH-10200] - core: Prevent multiple evaluations of Vagrantfile [GH-10199] - command/init: Support VAGRANT_DEFAULT_TEMPLATE env var [GH-10171] - command/powershell: Improve doc help string and fix winrm locales error [GH-10189] - contrib/bash: autocomplete running VM names for destroy subcommand [GH-10168] - guest/debian: Use `sudo` to determine if systemd is in use for hardened systems [GH-10198] - guest/openbsd: Add IPv6 network template for OpenBSD machines [GH-8912] - provisioners/salt: Allow non-windows hosts to pass along version [GH-10194] BUG FIXES: - core: Fix Vagrant.has_plugin? behavior before plugins are initialized [GH-10165] - core: Check verify_host_key for falsey or :never values when generating ssh config [GH-10182] - guest/linux: Filter out empty strings and loopback interfaces when constructing list of network interfaces [GH-10092] - provider/hyper-v: Check for automatic checkpoint support before configuring [GH-10181] ## 2.1.4 (August 30, 2018) BUG FIXES: - core: Fix local plugin installation prompt answer parsing [GH-10154] - core: Reset internal environment after plugin loading [GH-10155] - host/windows: Fix SMB list parsing when extra fields are included [GH-10156] - provisioners/ansible_local: Fix umask setting permission bug [GH-10140] ## 2.1.3 (August 29, 2018) FEATURES: - core: Support for project specific plugins [GH-10037] IMPROVEMENTS: - command/reload: Add `--force` flag to reload command [GH-10123] - communicator/winrm: Display warning if vagrant-winrm plugin is detected [GH-10076] - contrib/bash: Replace -VAGRANTSLASH- with literal slash in completion [GH-9987] - core: Show installed version of Vagrant when displaying version check [GH-9968] - core: Retain information of original box backing active guest [GH-10083] - core: Only write box info if provider supports box objects [GH-10126] - core: Update net-ssh dependency constraint to ~> 5.0.0 [GH-10066] - core/triggers: Catch and allow for non-standard exit codes with triggers `run` options [GH-10005] - core/triggers: Allow for spaces in `path` for trigger run option [GH-10118] - guest/debian: Isolate network interface configuration to individual files for systemd [GH-9889] - guest/redhat: Use libnfs-utils package if available [GH-9878] - provider/docker: Support Docker volume consistency for synced folders [GH-9811] - provider/hyperv: Disable synced folders on non-DrvFs file systems by default [GH-10001] - util/downloader: Support custom suffix on user agent string [GH-9966] - util/downloader: Prevent false positive matches on Location header [GH-10041] - util/subprocess: Force system library paths for executables external to AppImage [GH-10078] BUG FIXES: - core: Disable Vagrantfile loading with plugin commands [GH-10030] - core: Ensure the SecureRandom library is loaded for the trigger class [GH-10063] - core/triggers: Allow trigger run args option to be a single string [GH-10116] - util/powershell: Properly `join` commands from passed in array [GH-10115] - guest/solaris: Add back guest detection check for Solaris derived guests [GH-10081] - guest/windows: Be more explicit when invoking cmd.exe with mount_volume script [GH-9976] - host/linux: Fix sudo usage in NFS capability when modifying exports file [GH-10084] - host/windows: Remove localization dependency from SMB list generation [GH-10043] - provider/docker: Convert windows paths for volume mounts on docker driver [GH-10100] - provider/hyperv: Fix checkpoint configuration and properly disable automatic checkpoints by default [GH-9999] - provider/hyperv: Remove localization dependency from access check [GH-10000] - provider/hyperv: Enable ExposeVirtualizationExtensions only when available [GH-10079] - provider/virtualbox: Skip link-local when fixing IPv6 route [GH-9639, GH-10077] - push/ftp: Custom error when attempting to push too many files [GH-9952] - util/downloader: Prevent errors when Location header contains relative path [GH-10017] - util/guest_inspection: Prevent nmcli check from hanging when pty is enabled [GH-9926] - util/platform: Always force string type conversion on path [GH-9998] ## 2.1.2 (June 26, 2018) IMPROVEMENTS: - commands/suspend: Introduce flag for suspending all machines [GH-9829] - commands/global-status: Improve message about removing stale entries [GH-9856] - provider/hyperv: Attempt to determine import failure cause [GH-9936] - provider/hyperv: Update implementation. Include support for modifications on reload [GH-9872] - provider/hyperv: Validate maxmemory configuration setting [GH-9932] - provider/hyperv: Enable provider within WSL [GH-9943] - provider/hyperv: Add Hyper-V accessibility check on data directory path [GH-9944] - provisioners/ansible_local: Improve installation from PPA on Ubuntu guests. The compatibility is maintained only for active long-term support (LTS) versions, i.e. Ubuntu 12.04 (Precise Pangolin) is no longer supported. [GH-9879] BUG FIXES: - communicator/ssh: Update ssh private key file permission handling on Windows [GH-9923, GH-9900] - core: Display plugin commands in help [GH-9808] - core: Ensure guestpath or name is set with synced_folder option and dont set guestpath if not provided [GH-9692] - guest/debian: Fix netplan generation when using DHCP [GH-9855] - guest/debain: Update priority of network configuration file when using networkd [GH-9867] - guest/ubuntu: Update netplan config generation to detect NetworkManager [GH-9824] - guest/ubuntu: Fix failing Ansible installation from PPA on Bionic Beaver (18.04 LTS) [GH-9796] - host/windows: Prevent processing of last SMB line when using net share [GH-9917] - provisioner/chef: Prevent node_name set on configuration with chef_apply [GH-9916] - provisioner/salt: Remove usage of masterless? config attribute [GH-9833] ## 2.1.1 (May 7, 2018) IMPROVEMENTS: - guest/linux: Support builtin vboxsf module for shared folders [GH-9800] - host/windows: Update SMB capability to work without Get-SmbShare cmdlet [GH-9785] BUG FIXES: - core/triggers: Initialize internal trigger object for machine before initializing provider [GH-9784] - core/triggers: Ensure internal trigger fire does not get called if plugin installed [GH-9799] - provider/hyperv: Call import script with switchid instead of switchname [GH-9781] ## 2.1.0 (May 3, 2018) FEATURES: - core: Integrate vagrant-triggers plugin functionality into core Vagrant [GH-9713] IMPROVEMENTS: - core: Improve messaging around not finding requested provider [GH-9735] - core: Disable exception reports by default [GH-9738] - core: Continue on if vagrant fails to parse metadata box for update [GH-9760] - hosts/linux: Support RDP capability within WSL [GH-9758] - hosts/windows: Add SMB default mount options capability and set default version to 2.0 [GH-9734] - provider/hyperv: Include neighbor check for MAC on guest IP detection [GH-9737] - provider/virtualbox: Do not require VirtualBox availability within WSL [GH-9759] - provisioner/chef_zero: Support arrays for data_bags_path [GH-9669] - util/downloader: Don't raise error if response is HTTP 416 [GH-9729] - util/platform: Update Hyper-V enabled check [GH-9746] BUG FIXES: - communicators/ssh: Log error and proceed on Windows private key permissions [GH-9769] - middleware/authentication: Prevent URL modification when no changes are required [GH-9730] - middleware/authentication: Ignore URLs which cannot be parsed [GH-9739] - provider/hyperv: Reference switches by ID instead of name [GH-9747] - provider/docker: Use Util::SafeExec if docker-exec is run with `-t` option [GH-9761] - provisioner/chef: Trim drive letter from path on Windows [GH-9766] - provisioner/puppet: Properly finalize structured_facts config option [GH-9720] - util/platform: Fix original WSL to Windows path for "root" directory [GH-9696] ## 2.0.4 (April 20, 2018) FEATURES: - core: Vagrant aliases [GH-9504] IMPROVEMENTS: - communicators/ssh: Update file permissions when generating new key pairs [GH-9676] - core: Make resolv-replace usage opt-in instead of opt-out [GH-9644] - core: Suppress error messages from checkpoint runs [GH-9645] - guests/coreos: Identify operating systems closely related to CoreOS [GH-9600] - guests/debian: Adjust network configuration file prefix to 50- [GH-9646] - guests/photon: Less specific string grep to fix PhotonOS 2.0 detection [GH-9528] - guests/windows: Fix slow timeout when updating windows hostname [GH-9578] - hosts/windows: Make powershell version detection timeout configurable [GH-9506] - providers/virtualbox: Improve network collision error message [GH-9685] - provisioner/chef_solo: Improve Windows drive letter removal hack for remote paths[GH-9490] - provisioner/chef_zero: File path expand all chef_zero config path options [GH-9690] - provisioner/puppet: Puppet structured facts toyaml on provisioner [GH-9670] - provisioner/salt: Add master_json_config & minion_json_config options [GH-9420] - util/platform: Warn on ArgumentError exceptions from encoding [GH-9506] BUG FIXES: - commands/package: Fix uninitialized constant error [GH-9654] - communicators/winrm: Fix command filter to properly parse commands [GH-9673] - hosts/windows: Properly respect the VAGRANT_PREFER_SYSTEM_BIN environment variable [GH-9503] - hosts/windows: Fix virtualbox shared folders path for windows guests [GH-8099] - guests/freebsd: Fix typo in command that manages configuring networks [GH-9705] - util/checkpoint_client: Respect VAGRANT_CHECKPOINT_DISABLE environment variable [GH-9659] - util/platform: Use `--version` instead of `version` for WSL validation [GH-9674] ## 2.0.3 (March 15, 2018) IMPROVEMENTS: - guests/solaris: More explicit Solaris 11 and inherit SmartOS from Solaris [GH-9398] - hosts/windows: Add support for latest WSL release [GH-9525, GH-9300] - plugins/login: Update middleware to re-map hosts and warn on custom server [GH-9499] - providers/hyper-v: Exit if Hyper-V is enabled and VirtualBox provider is used [GH-9456] - provisioners/salt: Change to a temporary directory before downloading script files [GH-9351] - sycned_folders/nfs: Default udp to false when using version 4 [GH-8828] - util/downloader: Notify on host redirect [GH-9344] BUG FIXES: - core: Use provider override when specifying box_version [GH-9502] - guests/debian: Renew DHCP lease on hostname change [GH-9405] - guests/debian: Point hostname to 127.0.1.1 in /etc/hosts [GH-9404] - guests/debian: Update systemd? check for guest inspection [GH-9459] - guests/debian: Use ip route in dhcp template [GH-8730] - guests/gentoo: Disable if/netplugd when setting up a static ip on a gentoo guest using openrc [GH-9261] - guests/openbsd: Atomically apply new hostname.if(5) [GH-9265] - hosts/windows: Fix halt problem when determining powershell version on old powershells [GH-9470] - hosts/windows: Convert to windows path if on WSL during vbox export [GH-9518] - providers/virtualbox: Fix hostonly matching not respecting :name argument [GH-9302] - util/credential_scrubber: Ignore empty strings [GH-9472, GH-9462] ## 2.0.2 (January 29, 2018) FEATURES: - core: Provide mechanism for removing sensitive data from output [GH-9276] - core: Relax Ruby constraints to include 2.5 [GH-9363] - core: Hide sensitive values in output [GH-9369] - command/init: Support custom Vagrantfile templates [GH-9202] - guests: Add support for the Haiku operating system [GH-7805, GH-9245] - synced_folders/smb: Add support for macOS hosts [GH-9294] - vagrant-spec: Update vagrant-spec to include Windows platforms and updated linux boxes [GH-9183] IMPROVEMENTS: - config/ssh: Deprecate :paranoid in favor of :verify_host_key [GH-9341] - core: Add optional timestamp prefix on log output [GH-9269] - core: Print more helpful error message for NameEror exceptions in Vagrantfiles [GH-9252] - core: Update checkpoint implementation to announce updates and support notifications [GH-9380] - core: Use Ruby's Resolv by default [GH-9394] - docs: Include virtualbox 5.2.x as supported in docs [GH-9237] - docs: Improve how to pipe debug log on powershell [GH-9330] - guests/amazon: Improve guest detection [GH-9307] - guests/debian: Update guest configure networks [GH-9338] - guests/dragonflybsd: Base guest on FreeBSD to inherit more functionality [GH-9205] - guests/linux: Improve NFS service name detection and interactions [GH-9274] - guests/linux: Support mount option overrides for SMB mounts [GH-9366] - guests/linux: Use `ip` for reading guest address if available [GH-9315] - guests/solaris: Improve guest detection for alternatives [GH-9295] - hosts/windows: Check credentials during SMB prepare [GH-9365] - providers/hyper-v: Ensure Hyper-V cmdlets are fully qualified [GH-8863] - middleware/authentication: Add app.vagrantup.com to allowed hosts [GH-9145] - provisioners/shell: Support hiding environment variable values in output [GH-9367] - providers/virtualbox: Add a clean error message for invalid IP addresses [GH-9275] - providers/virtualbox: Introduce flag for SharedFoldersEnableSymlinksCreate setting [GH-9354] - providers/virtualbox: Provide warning for SharedFoldersEnableSymlinksCreate setting [GH-9389] - provisioners/salt: Fixes timeout issue in salt bootstrapping for windows [GH-8992] - synced_folders/smb: Update Windows implementation [GH-9294] - util/ssh: Attempt to locate local ssh client before attempting installer provided [GH-9400] BUG FIXES: - commands/box: Show all box providers with `update outdated --global` [GH-9347] - commands/destroy: Exit 0 if vagrant destroy finds no running vms [GH-9251] - commands/package: Fix --output path with specified folder [GH-9131] - guests/suse: Do not use full name when setting hostname [GH-9212] - providers/hyper-v: Fix enable virtualization extensions on import [GH-9255] - provisioners/ansible(both): Fix broken 'ask_sudo_pass' option [GH-9173] ## 2.0.1 (November 2, 2017) FEATURES: - core: Introduce Ruby 2.4 to Vagrant [GH-9102] - providers/virtualbox: Virtualbox 5.2 support [GH-8955] IMPROVEMENTS: - command/destroy: Introduce parallel destroy for certain providers [GH-9127] - communicators/winrm: Include APIPA check within ready check [GH-8997] - core: Clear POSIXLY_CORRECT when using optparse [GH-8685] - docs: Add auto_start_action and auto_stop_action to docs. [GH-9029] - docs: Fix typo in box format doc [GH-9100] - provisioners/chef: Handle chef provisioner reboot request [GH-8874] - providers/salt: Support Windows Salt Minions greater than 2016.x.x [GH-8926] - provisioners/salt: Add wget to bootstrap_salt options when fetching installer file [GH-9112] - provisioners/shell: Use ui.detail for displaying output [GH-8983] - util/downloader: Use CURL_CA_BUNDLE environment variable [GH-9135] BUG FIXES: - communicators/ssh: Retry on Errno::EPIPE exceptions [GH-9065] - core: Rescue more exceptions when checking if port is open [GH-8517] - guests/solaris11: Inherit from Solaris guest and keep solaris11 specific methods [GH-9034] - guests/windows: Split out cygwin path helper for msys2/cygwin paths and ensure cygpath exists [GH-8972] - guests/windows: Specify expected shell when executing on guest (fixes einssh communicator usage) [GH-9012] - guests/windows: Include WinSSH Communicator when using insert_public_key [GH-9105] - hosts/windows: Check for vagrant.exe when validating versions within WSL [GH-9107, GH-8962] - providers/docker: Isolate windows check within executor to handle running through VM [GH-8921] - providers/hyper-v: Properly invoke Auto stop action [GH-9000] - provisioners/puppet: Fix winssh communicator support in puppet provisioner [GH-9014] - virtualbox/synced_folders: Allow synced folders to contain spaces in the guest path [GH-8995] ## 2.0.0 (September 7, 2017) IMPROVEMENTS: - commands/login: Add support for two-factor authentication [GH-8935] - commands/ssh-config: Properly display windows path if invoked from msys2 or cygwin [GH-8915] - guests/alt: Add support for ALT Linux [GH-8746] - guests/kali: Fix file permissions on guest plugin ruby files [GH-8950] - hosts/linux: Provide common systemd detection for services interaction, fix NFS host interactions [GH-8938] - providers/salt: Remove duplicate stdout, stderr output from salt [GH-8767] - providers/salt: Introduce salt_call_args and salt_args option for salt provisioner [GH-8927] - providers/virtualbox: Improving resilience of some VirtualBox commands [GH-8951] - provisioners/ansible(both): Add the compatibility_mode option, with auto-detection enabled by default [GH-8913, GH-6570] - provisioners/ansible: Add the version option to the host-based provisioner [GH-8913, GH-8914] - provisioners/ansible(both): Add the become and become_user options with deprecation of sudo and sudo_user options [GH-8913, GH-6570] - provisioners/ansible: Add the ask_become_pass option with deprecation of the ask_sudo_pass option [GH-8913, GH-6570] BUG FIXES: - guests/shell_expand_guest_path : Properly expand guest paths that include relative path alias [GH-8918] - hosts/linux: Remove duplicate export folders before writing /etc/exports [GH-8945] - provisioners/ansible(both): Add single quotes to the inventory host variables, only when necessary [GH-8597] - provisioners/ansible(both): Add the "all:vars" section to the inventory when defined in `groups` option [GH-7730] - provisioners/ansible_local: Extra variables are no longer truncated when a dollar ($) character is present [GH-7735] - provisioners/file: Align file provisioner functionality on all platforms [GH-8939] - util/ssh: Properly quote key path for IdentityFile option to allow for spaces [GH-8924] BREAKING CHANGES: - Both Ansible provisioners are now capable of automatically setting the compatibility_mode that best fits with the Ansible version in use. You may encounter some compatibility issues when upgrading. If you were using Ansible 2.x and referring to the _ssh-prefixed variables present in the generated inventory (e.g. `ansible_ssh_host`). In this case, you can fix your Vagrant setup by setting compatibility_mode = "1.8", or by migrating to the new variable names (e.g. ansible_host). ## 1.9.8 (August 23, 2017) IMPROVEMENTS: - bash: Add box prune to contrib bash completion [GH-8806] - commands/login: Ask for description of Vagrant Cloud token [GH-8876] - commands/validate: Improve functionality of the validate command [GH-8889]n - core: Updated Vagrants rspec gem to 3.5.0 [GH-8850] - core: Validate powershell availability and version before use [GH-8839] - core: Introduce extra_args setting for ssh configs [GH-8895] - docs: Align contrib/sudoers file for ubuntu linux with docs [GH-8842] - provider/hyperv: Prefer IPv4 guest address [GH-8831, GH-8759] - provisioners/chef: Add config option omnibus_url for chef provisioners [GH-8682] - provisioners/chef: Improve exception handling around missing folder paths [GH-8775] BUG FIXES: - box/update: Add force flag for box upgrade command [GH-8871] - commands/rsync-auto: Ensure relative dirs are still rsync'd if defined [GH-8781] - commands/up: Disable install providers when using global id on vagrant up [GH-8910] - communicators/winssh: Fix public key insertion to retain ACL [GH-8790] - core: Update util/ssh to use `-o` for identity files [GH-8786] - guests/freebsd: Fix regex for listing network devices on some FreeBSD boxes. [GH-8760] - hosts/windows: Prevent control characters in version check for WSL [GH-8902, GH-8901] - providers/docker: Split String type links into Array when using compose [GH-8837, GH-8821] - providers/docker: Expand relative volume paths correctly [GH-8838, GH-8822] - providers/docker: Error when compose option enabled with force_host_vm [GH-8911] - provisioners/ansible: Update to use `-o` for identity files [GH-8786] - provisioners/file: Ensure remote folder exists prior to scp file or folder [GH-8880] - provisioners/salt: Fix error case when github is unreachable for installer [GH-8864] - provisioners/shell: Allow frozen string scripts [GH-8875] - provisioners/puppet: Remove `--manifestdir` flag from puppet apply in provisioner [GH-8797] - synced_folders/rsync: Correctly format IPv6 host [GH-8840, GH-8809] ## 1.9.7 (July 7, 2017) FEATURES: - core: Add support for preferred providers [GH-8558] IMPROVEMENTS: - guests/bsd: Invoke `tee` with explicit path [GH-8740] - guests/smartos: Guest updates for host name and nfs capabilities [GH-8695] - guests/windows: Add public key capabilities for WinSSH communicator [GH-8761] - hosts/windows: Log command exec encoding failures and use original string on failure [GH-8820] - providers/virtualbox: Filter machine IPs when preparing NFS settings [GH-8819] BUG FIXES: - communicators/winssh: Make script upload directory configurable [GH-8761] - core: Update cygwin detection to prevent PATH related errors [GH-8749, GH-6788] - core: Fix URI parsing of box names to prevent errors [GH-8762, GH-8758] - provider/docker: Only rsync-auto current working dir with docker provider [GH-8756] ## 1.9.6 (June 28, 2017) IMPROVEMENTS: - commands/snapshot: Enforce unique snapshot names and introduce `--force` flag [GH-7810] - commands/ssh: Introduce tty flag for `vagrant ssh -c` [GH-6827] - core: Warn about vagrant CWD changes for a machine [GH-3921] - core: Allow Compression and DSAAuthentication ssh flags to be configurable [GH-8693] - core/box: Warn if user sets box as url [GH-7118] - core/bundler: Enforce stict constraints on vendored libraries [GH-8692] - guests/kali: Add support for guest [GH-8553] - guests/smartos: Update halt capability and add public key insert and remove capabilities [GH-8618] - provisioners/ansible: Fix SSH keys only behavior to be consistent with Vagrant [GH-8467] - providers/docker: Add post install provisioner for docker setup [GH-8722] - snapshot/delete: Improve error message when given snapshot doesn't exist [GH-8653] - snapshot/list: Raise exception if provider does not support snapshots [GH-8619] - snapshot/restore: Improve error message when given snapshot doesn't exist [GH-8653] - snapshot/save: Raise exception if provider does not support snapshots [GH-8619] BUG FIXES: - communicators/ssh: Move `none` cipher to end of default cipher list in Net::SSH [GH-8661] - core: Add unique identifier to provisioner objects [GH-8680] - core: Stop config loader from loading dupe config if home and project dir are equal [GH-8707] - core/bundler: Impose constraints on update and allow system plugins to properly update [GH-8729] - guests/linux: Strip whitespace from GID [GH-8666, GH-8664] - guests/solaris: Do not use UNC style path for shared folders from windows hosts [GH-7723] - guests/windows: Fix directory creation when using rsync for synced folders [GH-8588] - hosts/windows: Force common encoding when running system commands [GH-8725] - providers/docker: Fix check for docker-compose [GH-8659, GH-8660] - providers/docker: Fix SSH under docker provider [GH-8706] - providers/hyperv: Fix box import [GH-8678, GH-8677] - provisioners/ansible_local: Catch pip_args in FreeBSD's and SUSE's ansible_install [GH-8676] - provisioners/salt: Fix minion ID configuration [GH-7865, GH-7454] - snapshot/restore: Exit 1 if vm has not been created when command is invoked [GH-8653] ## 1.9.5 (May 15, 2017) FEATURES: - hosts/windows: Support running within WSL [GH-8570, GH-8582] IMPROVEMENTS: - communicators/ssh: Retry on aborted connections [GH-8526, GH-8520] - communicators/winssh: Enabling shared folders and networking setup [GH-8567] - core: Remove nokogiri dependency and constraint [GH-8571] - guests: Do not modify existing /etc/hosts content [GH-8506, GH-7794] - guests/redhat: Update network configuration capability to properly handle NM [GH-8531] - hosts/windows: Check for elevated shell for Hyper-V [GH-8548, GH-8510] - hosts/windows: Fix invalid share names on Windows guests from Windows hosts [GH-8433] - providers: Return errors from docker/hyperv on ssh when not available [GH-8565, GH-8508] - providers/docker: Add support for driving provider with docker-compose [GH-8576] BUG FIXES: - guests/debian: Fix use_dhcp_assigned_default_route [GH-8577, GH-8575] - provisioners/shell: Fix Windows batch file provisioning [GH-8539, GH-8535] - providers/docker: Fall back to old style for SSH info lookup [GH-8566, GH-8552] - providers/hyperv: Fix import script [GH-8529] - providers/hyperv: Use string comparison for conditional checks in import scripts [GH-8568, GH-8444] ## 1.9.4 (April 24, 2017) FEATURES: - command/validate: Add Vagrantfile validation command [GH-8264, GH-8151] - communicators/winssh: Add WinSSH communicator for Win32-OpenSSH [GH-8485] - provider/hyperv: Support integration services configuration [GH-8379, GH-8378] IMPROVEMENTS: - core: Update internal dependencies [GH-8329, GH-8456] - core/bundler: Warn when plugin require fails instead of generating hard failure [GH-8400, GH-8392] - core/bundler: Error when configured plugin sources are unavailable [GH-8442] - guests/elementary: Add support for new guest "Elementary OS" [GH-8472] - guests/esxi: Add public_key capability [GH-8310] - guests/freebsd: Add chef_install and chef_installed? capabilities [GH-8443] - guests/gentoo: Add support for systemd in network configuration [GH-8407, GH-8406] - guests/windows: Support mounting synced folders via SSH on windows [GH-7425, GH-6220] - hosts/windows: Improve user permission detection [GH-7797] - provider/docker: Improve IP and port detection [GH-7840, GH-7651] - provider/docker: Do not force docker host VM on Darwin or Windows [GH-8437, GH-7895] - provisioners/ansible_local: Add `pip_args` option to define additional parameters when installing Ansible via pip [GH-8170, GH-8405] - provisioners/ansible_local: Add `:pip_args_only` install mode to allow full custom pip installations [GH-8405] - provisioners/salt: Update minion version installed to 2016.11.3 [GH-8448] BUG FIXES: - command/box: Remove extraneous sort from box list prior to display [GH-8422] - command/box: Properly handle local paths with spaces for box add [GH-8503, GH-6825] - command/up: Prevent other provider installation when explicitly defined [GH-8393, GH-8389] - communicators/ssh: Do not yield empty output data [GH-8495, GH-8259] - core: Provide fallback and retry when 0.0.0.0 is unavailable during port check [GH-8399, GH-8395] - core: Support port checker methods that do not expect inclusion of host_ip [GH-8497, GH-8423] - core/bundler: Check if source is local path and prevent addition to remote sources [GH-8401] - core/ui: Prevent deadlock detection errors [GH-8414, GH-8125] - guests/debian: Remove hardcoded device name in interface template [GH-8336, GH-7960] - guests/linux: Fix SMB mount capability [GH-8410, GH-8404] - hosts/windows: Fix issues with Windows encoding [GH-8385, GH-8380, GH-8212, GH-8207, GH-7516] - hosts/windows: Fix UNC path generation when UNC path is provided [GH-8504] - provisioners/salt: Allow Salt version to match 2 digit month [GH-8428] - provisioners/shell: Properly handle remote paths on Windows that include spaces [GH-8498, GH-7234] ## 1.9.3 (March 21, 2017) IMPROVEMENTS: - command/plugin: Remove requirement for paths with no spaces [GH-7967] - core: Support host_ip for forwarded ports [GH-7035, GH-8350] - core: Include disk space hint in box install failure message [GH-8089] - core/bundler: Allow vagrant constraint matching in prerelease mode [GH-8341] - provisioner/docker: Include /bin/docker as valid path [GH-8390] - provider/hyperv: Support enabling Hyper-V nested virtualization [GH-8325, GH-7738] BUG FIXES: - communicator/winrm: Prevent inaccurate WinRM address [GH-7983, GH-8073] - contrib/bash: Handle path spaces in bash completion [GH-8337] - core: Fix box sorting on find and list [GH-7956, GH-8334] - core/bundler: Force path as preferred source on install [GH-8327] - core/provision: Update "never" behavior to match documentation [GH-8366, GH-8016] - plugins/push: Isolate deprecation to Atlas strategy only - plugins/synced_folders: Give UID/GID precedence if found within mount options [GH-8122, GH-8064, GH-7859] ## 1.9.2 (February 27, 2017) FEATURES: - providers/hyperv: Support packaging of Hyper-V boxes [GH-7867] - util/command_deprecation: Add utility module for command deprecation [GH-8300] - util/subprocess: Add #stop and #running? methods [GH-8270] IMPROVEMENTS: - commands/expunge: Display default value on prompt and validate input [GH-8192, GH-8171] - communicator/winrm: Refactor WinRM communicator to use latest WinRM gems and V2 API [GH-8102] - core: Scrub URL credentials from output when adding boxes [GH-8194, GH-8117] - providers/hyperv: Prefer VMCX over XML configuration when VMCX is supported [GH-8119] BUG FIXES: - command/init: Include box version when using minimal option [GH-8283, GH-8282] - command/package: Fix SecureRandom constant error [GH-8159] - communicator/ssh: Remove any STDERR output prior to command execution [GH-8291, GH-8288] - core/bundler: Prevent pristine warning messages [GH-8191, GH-8190, GH-8147] - core/bundler: Fix local installations of pre-release plugins [GH-8252, GH-8253] - core/bundler: Prefer user defined source when installing plugins [GH-8273, GH-8210] - core/environment: Prevent persisting original environment variable if name is empty [GH-8198, GH-8126] - core/environment: Fix gems_path location [GH-8248] - core/environment: Properly expand dotfile path [GH-8196, GH-8108] - guests/arch: Fix configuring multiple network interfaces [GH-8165] - guests/linux: Fix guest detection for names with spaces [GH-8092] - guests/redhat: Fix network interface configuration [GH-8148] DEPRECATIONS: - command/push: Disable push command [GH-8300] ## 1.9.1 (December 7, 2016) IMPROVEMENTS: - core: Disable Vagrantfile loading when running plugin commands [GH-8066] - guests/redhat: Detect and restart NetworkManager service if in use [GH-8052, GH-7994] BUG FIXES: - core: Detect load failures within install solution sets and retry [GH-8068] - core: Prevent interactive shell on plugin uninstall [GH-8086, GH-8087] - core: Remove bundler usage from Util::Env [GH-8090, GH-8094] - guests/linux: Prevent stderr output on init version check for synced folders [GH-8051] ## 1.9.0 (November 28, 2016) FEATURES: - commands/box: Add `prune` subcommand for removing outdated boxes [GH-7978] - core: Remove Bundler integration for handling internal plugins [GH-7793, GH-8000, GH-8011, GH-8031] - providers/hyperv: Add support for Hyper-V binary configuration format [GH-7854, GH-7706, GH-6102] - provisioners/shell: Support MD5/SHA1 checksum validation of remote scripts [GH-7985, GH-6323] IMPROVEMENTS: - commands/plugin: Retain name sorted output when listing plugins [GH-8028] - communicator/ssh: Support custom environment variable export template [GH-7976, GH-6747] - provisioners/ansible(both): Add `config_file` option to point the location of an `ansible.cfg` file via ANSIBLE_CONFIG environment variable [GH-7195, GH-7918] - synced_folders: Support custom naming and disable auto-mount [GH-7980, GH-6836] BUG FIXES: - guests/linux: Do not match interfaces with special characters when sorting [GH-7989, GH-7988] - provisioner/salt: Fix Hash construction for constant [GH-7986, GH-7981] ## 1.8.7 (November 4, 2016) IMPROVEMENTS: - guests/linux: Place ethernet devices at start of network devices list [GH-7848] - guests/linux: Provide more consistent guest detection [GH-7887, GH-7827] - guests/openbsd: Validate guest rsync installation success [GH-7929, GH-7898] - guests/redhat: Include Virtuozzo Linux 7 within flavor identification [GH-7818] - guests/windows: Allow vagrant to start Windows Nano without provisioning [GH-7831] - provisioners/ansible_local: Change the Ansible binary detection mechanism [GH-7536] - provisioners/ansible(both): Add the `playbook_command` option [GH-7881] - provisioners/puppet: Support custom environment variables [GH-7931, GH-7252, GH-2270] - util/safe_exec: Use subprocess for safe_exec on Windows [GH-7802] - util/subprocess: Allow closing STDIN [GH-7778] BUG FIXES: - communicators/winrm: Prevent connection leakage [GH-7712] - core: Prevent duplicate provider priorities [GH-7756] - core: Allow Numeric type for box version [GH-7874, GH-6960] - core: Provide friendly error when user environment is too large [GH-7889, GH-7857] - guests: Remove `set -e` usage for better shell compatibility [GH-7921, GH-7739] - guests/linux: Fix incorrectly configured private network [GH-7844, GH-7848] - guests/linux: Properly order network interfaces [GH-7866, GH-7876, GH-7858, GH-7876] - guests/linux: Only emit upstart event if initctl is available [GH-7813] - guests/netbsd: Fix rsync installation [GH-7922, GH-7901] - guests/photon: Fix networking setup [GH-7808, GH-7873] - guests/redhat: Properly configure network and restart service [GH-7751] - guests/redhat: Prevent NetworkManager from managing devices on initial start [GH-7926] - hosts/linux: Fix race condition in writing /etc/exports file for NFS configuration [GH-7947, GH-7938] - Thanks to Aron Griffis (@agriffis) for identifying this issue - plugins/rsync: Escape exclude paths [GH-7928, GH-7910] - providers/docker: Remove --interactive flag when pty is true [GH-7688] - provisioners/ansible_local: Use enquoted path for file/directory existence checks - provisioners/salt: Synchronize configuration defaults with documentation [GH-7907, GH-6624] - pushes/atlas: Fix atlas push on Windows platform [GH-6938, GH-7802] ## 1.8.6 (September 27, 2016) IMPROVEMENTS: - Add detection for DragonFly BSD [GH-7701] - Implement auto_start and auto_stop actions for Hyper-V [GH-7647] - communicators/ssh: Remove any content prepended to STDOUT [GH-7676, GH-7613] BUG FIXES: - commands/package: Provide machine data directory for base box package [GH-5070, GH-7725] - core: Fix windows path formatting [GH-6598] - core: Fixes for ssh-agent interactions [GH-7703, GH-7621, GH-7398] - core: Support VAGRANT_DOTFILE_PATH relative to the Vagrantfile [GH-7623] - guests: Prevent ssh disconnect errors on halt command [GH-7675] - guests/bsd: Remove Darwin matching [GH-7701] - guests/linux: Fix SSH key permissions [GH-7610, GH-7611] - guests/linux: Always sort discovered network interfaces [GH-7705, GH-7668] - guests/linux: Fixes for user and group ID lookups for virtualbox shared folders [GH-7616, GH-7662, GH-7720] - guests/openbsd: Add custom halt capability [GH-7701] - guests/ubuntu: Fix detection on older guests [GH-7632, GH-7524, GH-7625] - hosts/arch: Detect NFS server by service name on arch [GH-7630, GH-7629] - hosts/darwin: Fix generated RDP configuration file [GH-7698] - provisioners/ansible: Add support for `ssh.proxy_command` setting [GH-7752] - synced_folders/nfs: Display warning when configured for NFSv4 and UDP [GH-7740] - synced_folders/rsync: Properly ignore excluded files within synced directory from `chown` command. [GH-5256, GH-7726] ## 1.8.5 (July 18, 2016) FEATURES: - core: Provide a way to globally disable box update checks with the environment variable `VAGRANT_BOX_UPDATE_CHECK_DISABLE`. Setting this to any non-empty value will instruct Vagrant to not look for box updates when running `vagrant up`. Setting this environment variable has no effect on the `vagrant box` commands. IMPROVEMENTS: - guests/arch: Support installing synced folder clients [GH-7519] - guests/darwin: Allow ipv6 static networks [GH-7491] - providers/virtualbox: Add support for 5.1 [GH-7574] BUG FIXES: - core: Bump listen gem and Ruby version to improve rsync performance [GH-7453, GH-7441] - core: Check process stdout when detecting if a hyperv admin [GH-7465, GH-7467] - core: Ensure removal of temporary directory when box download fails [GH-7496, GH-7499] - core: Fix regression for installing plugins from path [GH-7505, GH-7493] - core: Skip checking conflicts on disabled ports [GH-7587] - core: Idempotent write-out for state file [GH-7550] - core/guests: Create common BSD guest for shared logic - core/guests: Ignore empty output from `/sbin/ip` [GH-7539, GH-7537, GH-7533, GH-7605] - synced_folders/nfs: Shellescape rsync paths [GH-7540, GH-7605] - synced_folders/nfs: Ensure retries take place [GH-6360, GH-7605] - synced_folders/rsync: Shellescape rsync paths [GH-7580, GH-6690, GH-7579, GH-7605] - synced_folders/rsync: Translate Windows paths [GH-7012, GH-6702, GH-6568, GH-7046] - guests/bsd: Consolidate core logic for mounting NFS folders [GH-7480, GH-7474, GH-7466] - guests/bsd: Consolidate core logic for public key management [GH-7481] - guests/bsd: Consolidate core logic for halting [GH-7484] - guests/centos: Use `ip` instead of `ifconfig` to detect network interfaces [GH-7460] - guests/debian: Ensure newline when inserting public key [GH-7456] - guests/linux: Ensure NFS retries during mounting [GH-7492] - guests/redhat: Use `/sbin/ip` to list and configure networks for compatability with older versions of CentOS [GH-7482] - guests/redhat: Ensure newline when inserting public key [GH-7598, GH-7605] - guests/ubuntu: Use /etc/os-release to detect [GH-7524] - guests/ubuntu: Use short hostname [GH-7488, GH-7605] - providers/hyperv: Fix version check and catch statement [GH-7447, GH-7487] ## 1.8.4 (June 13, 2016) BUG FIXES: - core: Fix bundler plugin issue and version constraint [GH-7418, GH-7415] - providers/virtualbox: Use 8 network interfaces (due to Windows limitation) [GH-7417, GH-7419] - provisioners/ansible(both): Honor "galaxy_roles_path" option when running ansible-playbook [GH-7269, GH-7420] - provisioners/ansible_local: Add quotes around "ansible-galaxy" arguments [GH-7420] IMPROVEMENTS: - guests/redhat: Add CloudLinux detection [GH-7428, GH-7427] ## 1.8.3 (June 10, 2016) BREAKING CHANGES: - The `winrm` communicator now shares the same upload behavior as the `ssh` communicator. This change should have no impact to most vagrant operations but may break behavior when uploading directories to an existing destination target. The `file` provisioner should be the only builtin provisioner affected by this change. When uploading a directory and the destination directory exists on the endpoint, the source base directory will be created below the destination directory on the endpoint and the source directory contents will be unzipped to that location. Prior to this release, the contents of the source directory would be unzipped to an existing destination directory without creating the source base directory. This new behavior is more consistent with SCP and other well known shell copy commands. - The Chef provisioner's `channel` default value has changed from "current" to "stable". The "current" channel includes nightly releases and should be opt-in only. Note that users wishing to download the Chef Development Kit will need to opt into the "current" channel until Chef Software promotes into the "stable" channel. - The Arch Linux host capability for NFS removed support for rc.d in favor or systemd which has been present since 2012. Please see GH-7181 for more information. FEATURES: - provider/docker: Allow non-linux users to opt-out of the host VM to run Docker containers by setting `config.force_host_vm = false` in the Vagrantfile. This is especially useful for customers who wish to use the beta builds for Mac and Windows, dlite, or a custom provider. [GH-7277, GH-7298, 8c11b53] - provider/docker: New command: `docker-exec` allows attaching to an already-running container. [GH-7377, GH-6566, GH-5193, GH-4904, GH-4057, GH-4179, GH-4903] IMPROVEMENTS: - core/downloader: increase box resume download limit to 24h [GH-7352, GH-7272] - core/package: run validations prior to packaging [GH-7353, GH-7351] - core/action: make `start` ("vagrant up") run provisioners [GH-4467, GH-4421] - commands/all: Make it clear that machine IDs can be specified [GH-7356, GH-7228] - commands/init: Add support for specifying the box version [GH-7363, GH-5004] - commands/login: Print a warning with both the environment variable and local login token are present [GH-7206, GH-7219] - communicators/winrm: Upgrade to latest WinRM gems [GH-6922] - provisioners/ansible_local: Allow to install Ansible from pip, with version selection capability [GH-6654, GH-7167] - provisioners/ansible_local: Use `provisioning_path` as working directory for `ansible-galaxy` execution - provisioners/ansible(both provisioners): Add basic config validators/converters on `raw_arguments` and `raw_ssh_args` options [GH-7103] - provisioners/chef: Add the ability to install on SUSE [GH-6806] - provisioners/chef: Support legacy solo mode [GH-7327] - provisioners/docker: Restart container if newer image is available [GH-7358, GH-6620] - hosts/arch: Remove sysvinit and assume systemd [GH-7181] - hosts/linux: Do not use a pager with systemctl commands [GH-7270] - hosts/darwin: Add `extra_args` support for RDP [GH-5523, GH-6602] - hosts/windows: Use SafeExec to capture history in Powershell [GH-6749] - guests/amazon: Add detection [GH-7395, GH-7254] - guests/freebsd: Add quotes around hostname [GH-6867] - guests/fedora: Add support for ipv6 static networks [GH-7275, GH-7276] - guests/tinycore: Add support for shared folders [GH-6977, GH-6968] - guests/trisquel: Add initial support [GH-6842, GH-6843] - guests/windows: Add support for automatic login (no password prompting) [GH-5670] - core: Add `--no-delete` and provisioning flags to snapshot restore/pop [GH-6879] - providers/docker: Allow TCP and UDP ports on the same number [GH-7365, GH-5527] - providers/hyperv: Add support for differencing disk [GH-7090] - providers/hyperv: Add support for snapshots [GH-7110] - providers/hyperv: Reinstate compatibility with PS 4 [GH-7108] - providers/virtualbox: Add linked clone support for Virtualbox 1.4 [GH-7050] - synced_folders/nfs: Read static and dynamic IPs [GH-7290, GH-7289] BUG FIXES: - core: Bump nokogiri version to fix windows bug [GH-6766, GH-6848] - core: Revert a change made to the output of the identify file [GH-6962, GH-6929, GH-6589] - core: Fix login command behind a proxy [GH-6898, GH-6899] - core: Fix support for regular expressions on multi-machine `up` [GH-6908, GH-6909] - core: Allow boxes to use pre-release versions [GH-6892, GH-6893] - core: Rescue `Errno:ENOTCONN` waiting for port to be open [GH-7182, GH-7184] - core: Properly authenticate metadata box URLs [GH-6776, GH-7158] - core: Do not run provisioners if already run on resume [GH-7059, GH-6787] - core: Implement better tracking of tempfiles and tmpdirs to identify file leaks [GH-7355] - core: Allow SSH forwarding on Windows [GH-7287, GH-7202] - core: Allow customizing `keys_only` SSH option [GH-7360, GH-4275] - core: Allow customizing `paranoid` SSH option [GH-7360, GH-4275] - command/box_update: Do not update the same box twice [GH-6042, GH-7379] - command/init: Remove unnecessary `sudo` from generated Vagrantfile [GH-7369, GH-7295] - docs & core: Be consistent about the "2" in the Vagrantfile version [GH-6961, GH-6963] - guests/all: Refactor guest capabilities to run in a single command - **please see GH-7393 for the complete list of changes!** - guests/arch: Restart network after configuration [GH-7120, GH-7119] - guests/debian: Do not return an error if ifdown fails [GH-7159, GH-7155, GH-6871] - guests/freebsd: Use `pkg` to install rsync [GH-6760] - guests/freebsd: Use `netif` to configure networks [GH-5852, GH-7093] - guests/coreos: Detect all interface names [GH-6608, GH-6610] - providers/hyperv: Only specify Hyper-V if the parameter is support [GH-7101, GH-7098] - providers/virtualbox: Set maximum network adapters to 36 [GH-7293, GH-7286] - providers/virtualbox: Do not fail when master VM from linked clone is missing [GH-7126, GH-6742] - providers/virtualbox: Use scoped overrides in preparing NFS [GH-7387, GH-7386] - provisioners/ansible: Fix a race condition in the concurrent generations of the ansible inventory file, while running `vagrant up --parallel` [GH-6526, GH-7190] - provisioners/ansible_local: Don't quote the Ansible arguments defined in the `raw_arguments` option [GH-7103] - provisioners/ansible_local: Format json `extra_vars` with double quotes [GH-6726, GH-7103] - provisioners/ansible_local: Fix errors in absolute paths to playbook or galaxy resources when running on a Windows host [GH-6740, GH-6757] - provisioners/ansible_local: Change the way to verify `ansible-galaxy` presence, to avoid a non-zero status code with Ansible 2.0 [GH-6793] - provisioners/ansible(both provisioners): The Ansible configuration files detection is only executed by the `provision` action [GH-6763, GH-6984] - provisioners/chef: Do not use double sudo when installing [GGH-6805, GH-6804] - provisioners/chef: Change the default channel to "stable" (previously it was "current") [GH-7001, GH-6979] - provisioners/chef: Default node_name to hostname if present [GH-7063, GH-7153] - provisioners/docker: Fix -no-trunc command option [GH-7085] - provisioners/docker: Allow provisioning when container name is specified [GH-7074, GH-7086] - provisioners/puppet: Use `where.exe` to locate puppet binary [GH-6912, GH-6876] - provisioners/salt: Move masterless config to apply to all platforms [GH-7207, Gh-6924, GH-6915] - pushes/ftp: Create parent directories when uploading [GH-7154, GH-6316] - synced_folders/smb: Do not interpolate configuration file [GH-6906] ## 1.8.1 (December 21, 2015) BUG FIXES: - core: Don't create ".bundle" directory in pwd [GH-6717] - core: Fix exception on installing VirtualBox [GH-6713] - core: Do not convert standalone drive letters such as "D:" to UNC paths [GH-6598] - core: Fix a crash in parsing the config in some cases with network configurations [GH-6730] - core: Clean up temporarily files created by bundler [GH-7354, GH-6301, GH-3469, GH-6231] - commands/up: Smarter logic about what provider to install, avoiding situations where VirtualBox was installed over the correct provider [GH-6731] - guests/debian: Fix Docker install [GH-6722] - provisioners/chef: convert chef version to a string before comparing for the command builder [GH-6709, GH-6711] - provisioners/shell: convert env var values to strings [GH-6714] ## 1.8.0 (December 21, 2015) FEATURES: - **New Command: `vagrant powershell`**: For machines that support it, this will open a PowerShell prompt. - **New Command: `vagrant port`**: For machines that support it, this will display the list of forwarded ports from the guest to the host. - **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] - New provisioner: `ansible_local` to execute Ansible from the guest machine. [GH-2103] 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 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. - provisioners/salt: the "config_dir" option has been removed. It has no effect in Vagrant 1.8. [GH-6073] IMPROVEMENTS: - core: allow removal of all box versions with `--all` flag [GH-3462] - core: prune entries from global status on non-existent cwd [GH-6535] - core: networking: allow specifying a DHCP IP [GH-6325] - core: run provisioner cleanup tasks before powering off the VM [GH-6553] - core: only run provisioner cleanup tasks if they're implemented [GH-6603] This improves UX, but wasn't a bug before. - command/plugin: Add `--plugin-clean-sources` flag to reset plugin install sources, primarily for corp firewalls. [GH-4738] - command/rsync-auto: SSH connection is cached for faster sync times [GH-6399] - command/up: provisioners are run on suspend resume [GH-5815] - communicators/ssh: allow specifying host environment variables to forward to guests [GH-4132, GH-6562] - communicators/winrm: Configurable execution time limit [GH-6213] - providers/virtualbox: cache version lookup, which caused significant slowdown on some Windows hosts [GH-6552] - providers/virtualbox: add `public_address` capability for virtualbox [GH-6583, GH-5978] - provisioners/chef: perform cleanup tasks on the guest instead of the host - provisioners/chef: automatically generate a node_name if one was not given [GH-6555] - provisioners/chef: install Chef automatically on Windows [GH-6557] - provisioners/chef: allow the user to specify the Chef product (such as the Chef Development Kit) [GH-6557] - provisioners/chef: allow data_bags_path to be an array [GH-5988, GH-6561] - provisioners/shell: Support interactive mode for elevated PowerShell scripts [GH-6185] - provisioners/shell: add `env` option [GH-6588, GH-6516] - provisioners/ansible+ansible_local: add support for ansible-galaxy [GH-2718] - provisioners/ansible+ansible_local: add support for group and host variables in the generated inventory [GH-6619] - provisioners/ansible+ansible_local: add support for alphanumeric patterns for groups in the generated inventory [GH-3539] - provisioners/ansible: add support for WinRM settings [GH-5086] - 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] - pushes/local-exec: add support for specifying script args [GH-6661, GH-6660] - guests/slackware: add support for networking [GH-6514] BUG FIXES: - core: Ctrl-C weirdness fixed where it would exit parent process before Vagrant finished cleaning up [GH-6085] - core: DHCP network configurations don't warn on IP addresses ending in ".1" [GH-6150] - core: only append `access_token` when it does not exist in the URL [GH-6395, GH-6534] - core: use the correct private key when packaging a box [GH-6406] - core: fix crash when using invalid box checksum type [GH-6327] - core: don't check for metadata if the download URL is not HTTP [GH-6540] - core: don't make custom dotfile path if there is no Vagrantfile [GH-6542] - core: more robust check for admin privs on Windows [GH-5616] - core: properly detect when HTTP server doesn't support byte ranges and retry from scratch [GH-4479] - core: line numbers show properly in Vagrantfile syntax errors on Windows [GH-6445] - core: catch errors setting env vars on Windows [GH-6017] - core: remove cached synced folders when they're removed from the Vagrantfile [GH-6567] - core: use case-insensitive comparison for box checksum validations [GH-6648, GH-6650] - commands/box: add command with `~` paths on Windows works [GH-5747] - commands/box: the update command supports CA settings [GH-4473] - commands/box: removing all versions and providers of a box will properly clean all directories in `~/.vagrant.d/boxes` [GH-3570] - commands/box: outdated global won't halt on metadata download failure [GH-6453] - commands/login: respect environment variables in `vagrant login` command [GH-6590, GH-6422] - commands/package: when re-packaging a packaged box, preserve the generated SSH key [GH-5780] - commands/plugin: retry plugin install automatically a few times to avoid network issues [GH-6097] - commands/rdp: prefer `xfreerdp` if it is available on Linux [GH-6475] - commands/up: the `--provision-with` flag works with provisioner names [GH-5981] - communicator/ssh: fix potential crash case with PTY [GH-6225] - communicator/ssh: escape IdentityFile path [GH-6428, GH-6589] - communicator/winrm: respect `boot_timeout` setting [GH-6229] - communicator/winrm: execute scheduled tasks immediately on Windows XP since elevation isn't required [GH-6195] - communicator/winrm: Decouple default port forwarding rules for "winrm" and "winrm-ssl" [GH-6581] - communicator/winrm: Hide progress bars from PowerShell v5 [GH-6309] - guests/arch: enable network device after setting it up [GH-5737] - guests/darwin: advanced networking works with more NICs [GH-6386] - guests/debian: graceful shutdown works properly with newer releases [GH-5986] - guests/fedora: Preserve `localhost` entry when changing hostname [GH-6203] - guests/fedora: Use dnf if it is available [GH-6288] - guests/linux: when replacing a public SSH key, use POSIX-compliant sed flags [GH-6565] - guests/suse: DHCP network interfaces properly configured [GH-6502] - hosts/slackware: Better detection of NFS [GH-6367] - providers/hyper-v: support generation 2 VMs [GH-6372] - providers/hyper-v: support VMs with more than one NIC [GH-4346] - providers/hyper-v: check if user is in the Hyper-V admin group if they're not a Windows admin [GH-6662] - providers/virtualbox: ignore "Unknown" status bridge interfaces [GH-6061] - providers/virtualbox: only fix ipv6 interfaces that are in use [GH-6586, GH-6552] - provisioners/ansible: use quotes for the `ansible_ssh_private_key_file` value in the generated inventory [GH-6209] - provisioners/ansible: use quotes when passing the private key files via OpenSSH `-i` command line arguments [GH-6671] - provisioners/ansible: don't show the `ansible-playbook` command when verbose option is an empty string - provisioners/chef: fix `nodes_path` for Chef Zero [GH-6025, GH-6049] - provisioners/chef: do not error when the `node_name` is unset [GH-6005, GH-6064, GH-6541] - provisioners/chef: only force the formatter on Chef 11 or higher [GH-6278, GH-6556] - provisioners/chef: require `nodes_path` to be set for Chef Zero [GH-6110, GH-6559] - provisioners/puppet: apply provisioner uses correct default manifests with environments. [GH-5987] - provisioners/puppet: remove broken backticks [GH-6404] - provisioners/puppet: find Puppet binary properly on Windows [GH-6259] - provisioners/puppet-server: works with Puppet Collection 1 [GH-6389] - provisioners/salt: call correct executables on Windows [GH-5999] - provisioners/salt: log level and colorize works for masterless [GH-6474] - push/local-exec: use subprocess on windows when fork does not exist [GH-5307, GH-6563] - push/heroku: use current branch [GH-6554] - synced\_folders/rsync: on Windows, replace all paths with Cygwin paths since all rsync implementations require this [GH-6160] - synced\_folders/smb: use credentials files to allow for more characters in password [GH-4230] PLUGIN AUTHOR CHANGES: - installer: Upgrade to Ruby 2.2.3 ## 1.7.4 (July 17, 2015) BUG FIXES: - communicators/winrm: catch timeout errors [GH-5971] - 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] - hosts/linux: NFS will sudo copying back to `/etc/exports` [GH-5957] - providers/docker: Add `pull` setting, default to false [GH-5932] - providers/virtualbox: remove UNC path conversion on Windows since it caused mounting regressions [GH-5933] - provisioners/puppet: Windows Puppet 4 paths work correctly [GH-5967] - provisioners/puppet: Fix config merging errors [GH-5958] - provisioners/salt: fix "dummy config" error on bootstrap [GH-5936] ## 1.7.3 (July 10, 2015) FEATURES: - **New guest: `atomic`* - Project Atomic is supported as a guest - providers/virtualbox: add support for 5.0 [GH-5647] IMPROVEMENTS: - core: add password authentication to rdp_info hash [GH-4726] - core: improve error message when packaging fails [GH-5399] - core: improve message when adding a box from a file path [GH-5395] - core: add support for network gateways [GH-5721] - core: allow redirecting stdout and stderr in the UI [GH-5433] - core: update version of winrm-fs to 0.2.0 [GH-5738] - core: add option to enabled trusted http(s) redirects [GH-4422] - core: capture additional information such as line numbers during Vagrantfile loading [GH-4711, GH-5769] - core: add .color? to UI objects to see if they support color [GH-5771] - core: ignore hidden directories when searching for boxes [GH-5748, GH-5785] - core: use `config.ssh.sudo_command` to customize the sudo command format [GH-5573] - core: add `Vagrant.original_env` for Vagrant and plugins to restore or inspect the original environment when Vagrant is being run from the installer [GH-5910] - guests/darwin: support inserting generated key [GH-5204] - guests/darwin: support mounting SMB shares [GH-5750] - guests/fedora: support Fedora 21 [GH-5277] - guests/fedora: add capabilities for nfs and flavor [GH-5770, GH-4847] - guests/linux: specify user's domain as separate parameter [GH-3620, GH-5512] - guests/redhat: support Scientific Linux 7 [GH-5303] - guests/photon: initial support [GH-5612] - guests/solaris,solaris11: support inserting generated key [GH-5182] [GH-5290] - providers/docker: images are pulled prior to starting [GH-5249] - provisioners/ansible: store the first ssh private key in the auto-generated inventory [GH-5765] - provisioners/chef: add capability for checking if Chef is installed on Windows [GH-5669] - provisioners/docker: restart containers if arguments have changed [GH-3055, GH-5924] - provisioners/puppet: add support for Puppet 4 and configuration options [GH-5601] - provisioners/puppet: add support for `synced_folder_args` in apply [GH-5359] - provisioners/salt: add configurable `config_dir` [GH-3138] - provisioners/salt: add support for masterless configuration [GH-3235] - provisioners/salt: provider path to missing file in errors [GH-5637] - provisioners/salt: add ability to run salt orchestrations [GH-4371] - provisioners/salt: update to 2015.5.2 [GH-4152, GH-5437] - provisioners/salt: support specifying version to install [GH-5892] - provisioners/shell: add :name attribute to shell provisioner [GH-5607] - providers/docker: supports file downloads with the file provisioner [GH-5651] - providers/docker: support named Dockerfile [GH-5480] - providers/docker: don't remove image on reload so that build cache can be used fully [GH-5905] - providers/hyperv: select a Hyper-V switch based on a `network_name` [GH-5207] - providers/hyperv: allow configuring VladID [GH-5539] - providers/virtualbox: regexp supported for bridge configuration [GH-5320] - providers/virtualbox: handle a list of bridged NICs [GH-5691] - synced_folders/rsync: allow showing rsync output in debug mode [GH-4867] - synced_folders/rsync: set `rsync__rsync_path` to specify the remote command used to execute rsync [GH-3966] BUG FIXES: - core: push configurations are validated with global configs [GH-5130] - core: remove executable permissions on internal file [GH-5220] - core: check name and version in `has_plugin?` [GH-5218] - core: do not create duplicates when defining two private network addresses [GH-5325] - core: update ssh to check for Plink [GH-5604] - core: do not report plugins as installed when plugins are disabled [GH-5698, GH-5430] - core: Only take files when packaging a box to avoid duplicates [GH-5658, GH-5657] - core: escape curl urls and authentication [GH-5677] - core: fix crash if a value is missing for CLI arguments [GH-5550] - core: retry SSH key generation for transient RSA errors [GH-5056] - core: `ssh.private_key_path` will override the insecure key [GH-5632] - core: restore the original environment when shelling out to subprocesses outside of the installer [GH-5912] - core/cli: fix box checksum validation [GH-4665, GH-5221] - core/windows: allow Windows UNC paths to allow more than 256 characters [GH-4815] - command/rsync-auto: don't crash if rsync command fails [GH-4991] - communicators/winrm: improve error handling significantly and improve the error messages shown to be more human-friendly. [GH-4943] - communicators/winrm: remove plaintext passwords from files after provisioner is complete [GH-5818] - hosts/nfs: allow colons (`:`) in NFS IDs [GH-5222] - guests/darwin: remove dots from LocalHostName [GH-5558] - guests/debian: Halt works properly on Debian 8. [GH-5369] - guests/fedora: recognize future fedora releases [GH-5730] - guests/fedora: reload iface connection by NetworkManager [GH-5709] - guests/fedora: do not use biosdevname if it is not installed [GH-5707] - guests/freebsd: provide an argument to the backup file [GH-5516, GH-5517] - guests/funtoo: fix incorrect path in configure networks [GH-4812] - guests/linux: fix edge case exception where no home directory is available on guest [GH-5846] - guests/linux: copy NFS exports to tmpdir to do edits to guarantee permissions are available [GH-5773] - guests/openbsd: output newline after inserted public key [GH-5881] - guests/tinycore: fix change hostname functionality [GH-5623] - guests/ubuntu: use `hostnamectl` to set hostname on Ubuntu Vivid [GH-5753] - guests/windows: Create rsync folder prior to rsync-ing. [GH-5282] - guests/windows: Changing hostname requires reboot again since the non-reboot code path was crashing Windows server. [GH-5261] - guests/windows: ignore virtual NICs [GH-5478] - hosts/windows: More accurately get host IP address in VPNs. [GH-5349] - plugins/login: allow users to login with a token [GH-5145] - providers/docker: Build image from `/var/lib/docker` for more disk space on some systems. [GH-5302] - providers/docker: Fix crash that could occur in some scenarios when the host VM path changed. - providers/docker: Fix crash that could occur on container destroy with VirtualBox shared folders [GH-5143] - providers/hyperv: allow users to configure memory, cpu count, and vmname [GH-5183] - providers/hyperv: import respects secure boot. [GH-5209] - providers/hyperv: only set EFI secure boot for gen 2 machines [GH-5538] - providers/virtualbox: read netmask from dhcpservers [GH-5233] - providers/virtualbox: Fix exception when VirtualBox version is empty. [GH-5308] - providers/virtualbox: Fix exception when VBoxManage.exe can't be run on Windows [GH-1483] - providers/virtualbox: Error if another user is running after a VM is created to avoid issue with VirtualBox "losing" the VM [GH-5895] - providers/virtualbox: The "name" setting on private networks will choose an existing hostonly network [GH-5389] - provisioners/ansible: fix SSH settings to support more than 5 ssh keys [GH-5017] - provisioners/ansible: increase ansible connection timeout to 30 seconds [GH-5018] - provisioners/ansible: disable color if Vagrant is not colored [GH-5531, GH-5532] - provisioners/ansible: only show ansible-playbook command when `verbose` option is enabled [GH-5803] - provisioners/ansible: fix a race condition in the inventory file generation [GH-5551] - provisioners/docker: use `service` to restart Docker instead of upstart [GH-5245, GH-5577] - provisioners/docker: Only add docker user to group if exists. [GH-5315] - provisioners/docker: Use https for repo [GH-5749] - provisioners/docker: `apt-get update` before installing linux kernel images to get the correct version [GH-5860] - provisioners/chef: Fix shared folders missing error [GH-5199] - provisioners/chef: Use `command -v` to check for binary instead of `which` since that doesn't exist on some systems. [GH-5170] - provisioners/chef-zero: support more chef-zero/local mode attributes [GH-5339] - provisioners/chef: use windows-specific paths in Chef provisioners [GH-5913] - provisioners/docker: use docker.com instead of docker.io [GH-5216] - provisioners/docker: use `--restart` instead of `-r` on daemon [GH-4477] - provisioners/file: validation of source is relative to Vagrantfile [GH-5252] - pushes/atlas: send additional box metadata [GH-5283] - pushes/local-exec: fix "text file busy" error for inline [GH-5695] - pushes/ftp: improve check for remote directory existence [GH-5549] - synced\_folders/rsync: add `IdentitiesOnly=yes` to the rsync command. [GH-5175] - synced\_folders/smb: use correct `password` option [GH-5805] - synced\_folders/smb: prever IPv4 over IPv6 address to mount [GH-5798] - virtualbox/config: fix misleading error message for private_network [GH-5536, GH-5418] ## 1.7.2 (January 6, 2015) BREAKING CHANGES: - If you depended on the paths that Chef/Puppet provisioners use to store cookbooks (ex. "/tmp/vagrant-chef-1"), these will no longer be correct. Without this change, Chef/Puppet didn't work at all with `vagrant provision`. We expect this to affect only a minor number of people, since it's not something that was ever documented or recommended by Vagrant, or even meant to be supported. FEATURES: - provisioners/salt: add support for grains [GH-4895] IMPROVEMENTS: - commands/reload,up: `--provision-with` implies `--provision` [GH-5085] BUG FIXES: - core: private boxes still referencing vagrantcloud.com will have their vagrant login access token properly appended - core: push plugin configuration is properly validated - core: restore box packaging functionality - commands/package: fix crash - commands/push: push lookups are by user-defined name, not push strategy name [GH-4975] - commands/push: validate the configuration - communicators/winrm: detect parse errors in PowerShell and error - guests/arch: fix network configuration due to poor line breaks. [GH-4964] - guests/solaris: Merge configurations properly so configs can be set in default Vagrantfiles. [GH-5092] - installer: SSL cert bundle contains 1024-bit keys, fixing SSL verification for a lot of sites. - installer: vagrant executable properly `cygpaths` the SSL bundle path for Cygwin - installer: Nokogiri (XML lib used by Vagrant and dependencies) linker dependencies fixed, fixing load issues on some platforms - providers/docker: Symlinks in shared folders work. [GH-5093] - providers/hyperv: VM start errors turn into proper Vagrant errors. [GH-5101] - provisioners/chef: fix missing shared folder error [GH-4988] - provisioners/chef: remove Chef version check from solo.rb generation and make `roles_path` populate correctly - provisioners/chef: fix bad invocation of `with_clean_env` [GH-5021] - pushes/atlas: support more verbose logging - pushes/ftp: expand file paths relative to the Vagrantfile - pushes/ftp: improved debugging output - pushes/ftp: create parent directories if they do not exist on the remote server ## 1.7.1 (December 11, 2014) IMPROVEMENTS: - provisioners/ansible: Use Docker proxy if needed. [GH-4906] BUG FIXES: - providers/docker: Add support of SSH agent forwarding. [GH-4905] ## 1.7.0 (December 9, 2014) BREAKING CHANGES: - provisioners/ansible: `raw_arguments` has now highest priority - provisioners/ansible: only the `ssh` connection transport is supported (`paramiko` can be enabled with `raw_arguments` at your own risks) FEATURES: - **Vagrant Push**: Vagrant can now deploy! `vagrant push` is a single command to deploy your application. Deploy to Heroku, FTP, or HashiCorp's commercial product Atlas. New push strategies can be added with plugins. - **Named provisioners**: Provisioners can now be named. This name is used for output as well as `--provision-with` for better control. - Default provider logic improved: Providers in `config.vm.provider` blocks in your Vagrantfile now have higher priority than plugins. Earlier providers are chosen before later ones. [GH-3812] - If the default insecure keypair is used, Vagrant will automatically replace it with a randomly generated keypair on first `vagrant up`. [GH-2608] - Vagrant Login is now part of Vagrant core - Chef Zero provisioner: Use Chef 11's "local" mode to run recipes against an in-memory Chef Server - Chef Apply provisioner: Specify inline Chef recipes and recipe snippets using the Chef Apply provisioner IMPROVEMENTS: - core: `has_plugin?` function now takes a second argument which is a version constraint requirement. [GH-4650] - core: ".vagrantplugins" file in the same folder as your Vagrantfile will be loaded for defining inline plugins. [GH-3775] - commands/plugin: Plugin list machine-readable output contains the plugin name as the target for versions and other info. [GH-4506] - env/with_cleanenv: New helper for plugin developers to use when shelling out to another Ruby environment - guests/arch: Support predictable network interface naming. [GH-4468] - guests/suse: Support NFS client install, rsync setup. [GH-4492] - guests/tinycore: Support changing host names. [GH-4469] - guests/tinycore: Support DHCP-based networks. [GH-4710] - guests/windows: Hostname can be set without reboot. [GH-4687] - providers/docker: Build output is now shown. [GH-3739] - providers/docker: Can now start containers from private repositories more easily. Vagrant will login for you if you specify auth. [GH-4042] - providers/docker: `stop_timeout` can be used to modify the `docker stop` timeout. [GH-4504] - provisioners/chef: Automatically install Chef when using a Chef provisioner. - provisioners/ansible: Always show Ansible command executed when Vagrant log level is debug (even if ansible.verbose is false) - synced\_folders/nfs: Won't use `sudo` to write to /etc/exports if there are write privileges. [GH-2643] - synced\_folders/smb: Credentials from one SMB will be copied to the rest. [GH-4675] BUG FIXES: - core: Fix cases where sometimes SSH connection would hang. - core: On a graceful halt, force halt if capability "insert public key" is missing. [GH-4684] - core: Don't share `/vagrant` if any "." folder is shared. [GH-4675] - core: Fix SSH private key permissions more aggressively. [GH-4670] - core: Custom Vagrant Cloud server URL now respected in more cases. - core: On downloads, don't continue downloads if the remote server doesn't support byte ranges. [GH-4479] - core: Box downloads recognize more complex content types that include "application/json" [GH-4525] - core: If all sub-machines are `autostart: false`, don't start any. [GH-4552] - core: Update global-status state in more cases. [GH-4513] - core: Only delete machine state if the machine is not created in initialize - commands/box: `--cert` flag works properly. [GH-4691] - command/docker-logs: Won't crash if container is removed. [GH-3990] - command/docker-run: Synced folders will be attached properly. [GH-3873] - command/rsync: Sync to Docker containers properly. [GH-4066] - guests/darwin: Hostname sets bonjour name and local host name. [GH-4535] - guests/freebsd: NFS mounting can specify the version. [GH-4518] - guests/linux: More descriptive error message if SMB mount fails. [GH-4641] - guests/rhel: Hostname setting on 7.x series works properly. [GH-4527] - guests/rhel: Installing NFS client works properly on 7.x [GH-4499] - guests/solaris11: Static IP address preserved after restart. [GH-4621] - guests/ubuntu: Detect with `lsb_release` instead of `/etc/issue`. [GH-4565] - hosts/windows: RDP client shouldn't map all drives by default. [GH-4534] - providers/docker: Create args works. [GH-4526] - providers/docker: Nicer error if package is called. [GH-4595] - providers/docker: Host IP restriction is forwarded through. [GH-4505] - providers/docker: Protocol is now honored in direct `ports settings. - providers/docker: Images built using `build_dir` will more robustly capture the final image. [GH-4598] - providers/docker: NFS synced folders now work. [GH-4344] - providers/docker: Read the created container ID more robustly. - providers/docker: `vagrant share` uses correct IP of proxy VM if it exists. [GH-4342] - providers/docker: `vagrant_vagrantfile` expands home directory. [GH-4000] - providers/docker: Fix issue where multiple identical proxy VMs would be created. [GH-3963] - providers/docker: Multiple links with the same name work. [GH-4571] - providers/virtualbox: Show a human-friendly error if VirtualBox didn't clean up an existing VM. [GH-4681] - providers/virtualbox: Detect case when VirtualBox reports 0.0.0.0 as IP address and don't allow it. [GH-4671] - providers/virtualbox: Show more descriptive error if VirtualBox is reporting an empty version. [GH-4657] - provisioners/ansible: Force `ssh` (OpenSSH) connection by default [GH-3396] - provisioners/ansible: Don't use or modify `~/.ssh/known_hosts` file by default, similarly to native vagrant commands [GH-3900] - provisioners/ansible: Use intermediate Docker host when needed. [GH-4071] - provisioners/docker: Get GPG key over SSL. [GH-4597] - provisioners/docker: Search for docker binary in multiple places. [GH-4580] - provisioners/salt: Highstate works properly with a master. [GH-4471] - provisioners/shell: Retry getting SSH info a few times. [GH-3924] - provisioners/shell: PowerShell scripts can have args. [GH-4548] - synced\_folders/nfs: Don't modify NFS exports file if no exports. [GH-4619] - synced\_folders/nfs: Prune exports for file path IDs. [GH-3815] PLUGIN AUTHOR CHANGES: - `Machine#action` can be called with the option `lock: false` to not acquire a machine lock. - `Machine#reload` will now properly trigger the `machine_id_changed` callback on providers. ## 1.6.5 (September 4, 2014) BUG FIXES: - core: forward SSH even if WinRM is used. [GH-4437] - communicator/ssh: Fix crash when pty is enabled with SSH. [GH-4452] - guests/redhat: Detect various RedHat flavors. [GH-4462] - guests/redhat: Fix typo causing crash in configuring networks. [GH-4438] - guests/redhat: Fix typo causing hostnames to not set. [GH-4443] - providers/virtualbox: NFS works when using DHCP private network. [GH-4433] - provisioners/salt: Fix error when removing non-existent bootstrap script on Windows. [GH-4614] ## 1.6.4 (September 2, 2014) BACKWARDS INCOMPATIBILITIES: - commands/docker-run: Started containers are now deleted after run. Specify the new `--no-rm` flag to retain the original behavior. [GH-4327] - providers/virtualbox: Host IO cache is no longer enabled by default since it causes stale file issues. Please enable manually if you require this. [GH-3934] IMPROVEMENTS: - core: Added `config.vm.box_server_url` setting to point at a Vagrant Cloud instance. [GH-4282] - core: File checksumming performance has been improved by at least 100%. Memory requirements have gone down by half. [GH-4090] - commands/docker-run: Add the `--no-rm` flag. Containers are deleted by default. [GH-4327] - commands/plugin: Better error output is shown when plugin installation fails. - commands/reload: show post up message [GH-4168] - commands/rsync-auto: Add `--poll` flag. [GH-4392] - communicators/winrm: Show stdout/stderr if command fails. [GH-4094] - guests/nixos: Added better NFS support. [GH-3983] - providers/hyperv: Accept VHD disk format. [GH-4208] - providers/hyperv: Support generation 2 VMs. [GH-4324] - provisioners/docker: More verbose output. [GH-4377] - provisioners/salt: Get proper exit codes to detect failed runs. [GH-4304] BUG FIXES: - core: Downloading box files should resume in more cases since the temporary file is preserved in more cases. [GH-4301] - core: Windows is not detected as NixOS in some cases. [GH-4302] - core: Fix encoding issues with Windows. There are still some outlying but this fixes a few. [GH-4159] - core: Fix crash case when destroying with an invalid provisioner. [GH-4281] - core: Box names with colons work on Windows. [GH-4100] - core: Cleanup all temp files. [GH-4103] - core: User curlrc is not loaded, preventing strange download issues. [GH-4328] - core: VM names may no longer contain brackets, since they cause issues with some providers. [GH-4319] - core: Use "-f" to `rm` files in case pty is true. [GH-4410] - core: SSH key doesn't have to be owned by our user if we're running as root. [GH-4387] - core: "vagrant provision" will cause "vagrant up" to properly not reprovision. [GH-4393] - commands/box/add: "Content-Type" header is now case-insensitive when looking for metadata type. [GH-4369] - commands/docker-run: Named docker containers no longer conflict. [GH-4294] - commands/package: base package won't crash with exception [GH-4017] - commands/rsync-auto: Destroyed machines won't raise exceptions. [GH-4031] - commands/ssh: Extra args are passed through to Docker container. [GH-4378] - communicators/ssh: Nicer error if remote unexpectedly disconnects. [GH-4038] - communicators/ssh: Clean error when max sessions is hit. [GH-4044] - communicators/ssh: Fix many issues around PTY-enabled output parsing. [GH-4408] - communicators/winrm: Support `mkdir` [GH-4271] - communicators/winrm: Properly escape double quotes. [GH-4309] - communicators/winrm: Detect failed commands that aren't CLIs. [GH-4383] - guests/centos: Fix issues when NFS client is installed by restarting NFS [GH-4088] - guests/debian: Deleting default route on DHCP networks can fail. [GH-4262] - guests/fedora: Fix networks on Fedora 20 with libvirt. [GH-4104] - guests/freebsd: Rsync install for rsync synced folders work on FreeBSD 10. [GH-4008] - guests/freebsd: Configure vtnet devices properly [GH-4307] - guests/linux: Show more verbose error when shared folder mount fails. [GH-4403] - guests/redhat: NFS setup should use systemd for RH7+ [GH-4228] - guests/redhat: Detect RHEL 7 (and CentOS) and install Docker properly. [GH-4402] - guests/redhat: Configuring networks on EL7 works. [GH-4195] - guests/redhat: Setting hostname on EL7 works. [GH-4352] - guests/smartos: Use `pfexec` for rsync. [GH-4274] - guests/windows: Reboot after hostname change. [GH-3987] - hosts/arch: NFS works with latest versions. [GH-4224] - hosts/freebsd: NFS exports are proper syntax. [GH-4143] - hosts/gentoo: NFS works with latest versions. [GH-4418] - hosts/windows: RDP command works without crash. [GH-3962] - providers/docker: Port on its own will choose random host port. [GH-3991] - providers/docker: The proxy VM Vagrantfile can be in the same directory as the main Vagrantfile. [GH-4065] - providers/virtualbox: Increase network device limit to 36. [GH-4206] - providers/virtualbox: Error if can't detect VM name. [GH-4047] - provisioners/cfengine: Fix default Yum repo URL. [GH-4335] - provisioners/chef: Chef client cleanup should work. [GH-4099] - provisioners/puppet: Manifest file can be a directory. [GH-4169] - provisioners/puppet: Properly escape facter variables for PowerShell on Windows guests. [GH-3959] - provisioners/puppet: When provisioning fails, don't repeat all of stdout/stderr. [GH-4303] - provisioners/salt: Update salt minion version on Windows. [GH-3932] - provisioners/shell: If args is an array and contains numbers, it no longer crashes. [GH-4234] - provisioners/shell: If fails, the output/stderr isn't repeated again. [GH-4087] ## 1.6.3 (May 29, 2014) FEATURES: - **New Guest:** NixOS - Supports changing host names and setting networks. [GH-3830] IMPROVEMENTS: - core: A CA path can be specified in the Vagrantfile, not just a file, when using a custom CA. [GH-3848] - commands/box/add: `--capath` flag added for custom CA path. [GH-3848] - commands/halt: Halt in reverse order of up, like destroy. [GH-3790] - hosts/linux: Uses rdesktop to RDP into machines if available. [GH-3845] - providers/docker: Support for UDP forwarded ports. [GH-3886] - provisioners/salt: Works on Windows guests. [GH-3825] BUG FIXES: - core: Provider plugins more easily are compatible with global-status and should show less stale data. [GH-3808] - core: When setting a synced folder, it will assume it is not disabled unless explicitly specified. [GH-3783] - core: Ignore UDP forwarded ports for collision detection. [GH-3859] - commands/package: Package with `--base` for VirtualBox doesn't crash. [GH-3827] - guests/solaris11: Fix issue with public network and DHCP on newer Solaris releases. [GH-3874] - guests/windows: Private networks with static IPs work when there is more than one. [GH-3818] - guests/windows: Don't look up a forwarded port for WinRM if we're not accessing the local host. [GH-3861] - guests/windows: Fix errors with arg lists that are too long over WinRM in some cases. [GH-3816] - guests/windows: Powershell exits with proper exit code, fixing - issues where non-zero exit codes weren't properly detected. [GH-3922] - hosts/windows: Don't execute mstsc using PowerShell since it doesn't exit properly. [GH-3837] - hosts/windows: For RDP, don't remove the Tempfile right away. [GH-3875] - providers/docker: Never do graceful shutdown, always use `docker stop`. [GH-3798] - providers/docker: Better error messaging when SSH is not ready direct to container. [GH-3763] - providers/docker: Don't port map SSH port if container doesn't support SSH. [GH-3857] - providers/docker: Proper SSH info if using native driver. [GH-3799] - providers/docker: Verify host VM has SSH ready. [GH-3838] - providers/virtualbox: On Windows, check `VBOX_MSI_INSTALL_PATH` for VBoxManage path as well. [GH-3852] - provisioners/puppet: Fix setting facter vars with Windows guests. [GH-3776] - provisioners/puppet: On Windows, run in elevated prompt. [GH-3903] - guests/darwin: Respect mount options for NFS. [GH-3791] - guests/freebsd: Properly register the rsync_pre capability - guests/windows: Certain executed provisioners won't leave output and exit status behind. [GH-3729] - synced\_folders/rsync: `rsync__chown` can be set to `false` to disable recursive chown after sync. [GH-3810] - synced\_folders/rsync: Use a proper msys path if not in Cygwin. [GH-3804] - synced\_folders/rsync: Don't append args infinitely, clear out arg list on each run. [GH-3864] PLUGIN AUTHOR CHANGES: - Providers can now implement the `rdp_info` provider capability to get proper info for `vagrant rdp` to function. ## 1.6.2 (May 12, 2014) IMPROVEMENTS: - core: Automatically forward WinRM port if communicator is WinRM. [GH-3685] - command/rdp: Args after "--" are passed directly through to the RDP client. [GH-3686] - providers/docker: `build_args` config to specify extra args for `docker build`. [GH-3684] - providers/docker: Can specify options for the build dir synced folder when a host VM is in use. [GH-3727] - synced\_folders/nfs: Can tell Vagrant not to handle exporting by setting `nfs_export: false` [GH-3636] BUG FIXES: - core: Hostnames can be one character. [GH-3713] - core: Don't lock machines on SSH actions. [GH-3664] - core: Fixed crash when adding a box from Vagrant Cloud that was the same name as a real directory. [GH-3732] - core: Parallelization is more stable, doesn't crash due to to bad locks. [GH-3735] - commands/package: Don't double included files in package. [GH-3637] - guests/linux: Rsync chown ignores symlinks. [GH-3744] - provisioners/shell: Fix shell provisioner config validation when the `binary` option is set to false [GH-3712] - providers/docker: default proxy VM won't use HGFS [GH-3687] - providers/docker: fix container linking [GH-3719] - providers/docker: Port settings expose to host properly. [GH-3723] - provisioners/puppet: Separate module paths with ';' on Windows. [GH-3731] - synced\_folders\rsync: Copy symlinks as real files. [GH-3734] - synced\_folders/rsync: Remove non-portable '-v' flag from chown. [GH-3743] ## 1.6.1 (May 7, 2014) IMPROVEMENTS: - **New guest: Linux Mint** is now properly detected. [GH-3648] BUG FIXES: - core: Global control works from directories that don't have a Vagrantfile. - core: Plugins that define config methods that collide with Ruby Kernel/Object - methods are merged properly. [GH-3670] - commands/docker-run: `--help` works. [GH-3698] - commands/package: `--base` works without crashing for VirtualBox. - commands/reload: If `--provision` is specified, force provisioning. [GH-3657] - guests/redhat: Fix networking issues with CentOS. [GH-3649] - guests/windows: Human error if WinRM not in use to configure networks. [GH-3651] - guests/windows: Puppet exit code 2 doesn't cause Windows to raise an error. [GH-3677] - providers/docker: Show proper error message when on Linux. [GH-3654] - providers/docker: Proxy VM works properly even if default provider environmental variable set to "docker" [GH-3662] - providers/docker: Put sync folders in `/var/lib/docker` because it usually has disk space. [GH-3680] - synced\_folders/rsync: Create the directory before syncing. ## 1.6.0 (May 6, 2014) BACKWARDS INCOMPATIBILITIES: - Deprecated: `halt_timeout` and `halt_check_interval` settings for SmartOS, Solaris, and Solaris11 guests. These will be fully removed in 1.7. A warning will be shown if they're in use in 1.6. FEATURES: - **New guest: Windows**. Vagrant now fully supports Windows as a guest VM. WinRM can be used for communication (or SSH), and the shell provisioner, Chef, and Puppet all work with Windows VMs. - **New command: global-status**. This command shows the state of every created Vagrant environment on the system for that logged in user. - **New command: rdp**. This command connects to the running machine via the Remote Desktop Protocol. - **New command: version**. This outputs the currently installed version as well as the latest version of Vagrant available. - **New provider: Docker**. This provider will back your development environments with Docker containers. If you're not on Linux, it will automatically spin up a VM for you on any provider. You can even specify a specific Vagrantfile to use as the Docker container host. - Control Vagrant environments from any directory. Using the UUIDs given in `vagrant global-status`, you can issue commands from anywhere on your machine, not just that environment's directory. Example: `vagrant destroy UUID` from anywhere. - Can now specify a `post_up_message` in your Vagrantfile that is shown after a `vagrant up`. This is useful for putting some instructions of how to use the development environment. - Can configure provisioners to run "once" or "always" (defaults to "once"), so that subsequent `vagrant up` or `reload` calls will always run a provisioner. [GH-2421] - Multi-machine environments can specify an "autostart" option (default to true). `vagrant up` starts all machines that have enabled autostart. - Vagrant is smarter about choosing a default provider. If `VAGRANT_DEFAULT_PROVIDER` is set, it still takes priority, but otherwise Vagrant chooses a "best" provider. IMPROVEMENTS: - core: Vagrant locks machine access to one Vagrant process at a time. This will protect against two simultaneous `up` actions happening on the same environment. - core: Boxes can be compressed with LZMA now as well. - commands/box/remove: Warns if the box appears to be in use by an environment. Can be forced with `--force`. - commands/destroy: Exit codes changes. 0 means everything succeeded. 1 means everything was declined. 2 means some were declined. [GH-811] - commands/destroy: Doesn't require box to exist anymore. [GH-1629] - commands/init: force flag. [GH-3564] - commands/init: flag for minimal Vagrantfile creation (no comments). [GH-3611] - commands/rsync-auto: Picks up and syncs provisioner folders if provisioners are backed by rsync. - commands/rsync-auto: Detects when new synced folders were added and warns user they won't be synced until `vagrant reload`. - commands/ssh-config: Works without a target in multi-machine envs [GH-2844] - guests/freebsd: Support for virtio interfaces. [GH-3082] - guests/openbsd: Support for virtio interfaces. [GH-3082] - guests/redhat: Networking works for upcoming RHEL7 release. [GH-3643] - providers/hyperv: Implement `vagrant ssh -c` support. [GH-3615] - provisioners/ansible: Support for Ansible Vault. [GH-3338] - provisioners/ansible: Show Ansible command executed. [GH-3628] - provisioners/salt: Colorize option. [GH-3603] - provisioners/salt: Ability to specify log level. [GH-3603] - synced\_folders: nfs: Improve sudo commands used to make them sudoers friendly. Examples in docs. [GH-3638] BUG FIXES: - core: Adding a box from a network share on Windows works again. [GH-3279] - commands/plugin/install: Specific versions are now locked in. - commands/plugin/install: If insecure RubyGems.org is specified as a source, use that. [GH-3610] - commands/rsync-auto: Interrupt exits properly. [GH-3552] - commands/rsync-auto: Run properly on Windows. [GH-3547] - communicators/ssh: Detect if `config.ssh.shell` is invalid. [GH-3040] - guests/debian: Can set hostname if hosts doesn't contain an entry already for 127.0.1.1 [GH-3271] - guests/linux: For `read_ip_address` capability, set `LANG=en` so it works on international systems. [GH-3029] - providers/virtualbox: VirtualBox detection works properly again on Windows when the `VBOX_INSTALL_PATH` has multiple elements. [GH-3549] - providers/virtualbox: Forcing MAC address on private network works properly again. [GH-3588] - provisioners/chef-solo: Fix Chef version checking to work with prerelease versions. [GH-3604] - provisioners/salt: Always copy keys and configs on provision. [GH-3536] - provisioners/salt: Install args should always be present with bootstrap. - provisioners/salt: Overwrite keys properly on subsequent provisions [GH-3575] - provisioners/salt: Bootstrap uses raw GitHub URL rather than subdomain. [GH-3583] - synced\_folders/nfs: Acquires a process-level lock so exports don't collide with Vagrant running in parallel. - synced\_folders/nfs: Implement usability check so that hosts that don't support NFS get an error earlier. [GH-3625] - synced\_folders/rsync: Add UserKnownHostsFile option to not complain. [GH-3511] - synced\_folders/rsync: Proxy command is used properly if set. [GH-3553] - synced\_folders/rsync: Owner/group settings are respected. [GH-3544] - synced\_folders/smb: Passwords with symbols work. [GH-3642] PLUGIN AUTHOR CHANGES: - **New host capability:** "rdp\_client". This capability gets the RDP connection info and must launch the RDP client on the system. - core: The "Call" middleware now merges the resulting middleware stack into the current stack, rather than running it as a separate stack. The result is that ordering is preserved. - core: The "Message" middleware now takes a "post" option that will output the message on the return-side of the middleware stack. - core: Forwarded port collision repair works when Vagrant is run in parallel with other Vagrant processes. [GH-2966] - provider: Providers can now specify that boxes are optional. This lets you use the provider without a `config.vm.box`. Useful for providers like AWS or Docker. - provider: A new class-level `usable?` method can be implemented on the provider implementation. This returns or raises an error when the provider is not usable (i.e. VirtualBox isn't installed for VirtualBox) - synced\_folders: New "disable" method for removing synced folders from a running machine. ## 1.5.4 (April 21, 2014) IMPROVEMENTS: - commands/box/list: Doesn't parse Vagrantfile. [GH-3502] - providers/hyperv: Implement the provision command. [GH-3494] BUG FIXES: - core: Allow overriding of the default SSH port. [GH-3474] - commands/box/remove: Make output nicer. [GH-3470] - commands/box/update: Show currently installed version. [GH-3467] - command/rsync-auto: Works properly on Windows. - guests/coreos: Fix test for Docker daemon running. - guests/linux: Fix test for Docker provisioner whether Docker is running. - guests/linux: Fix regression where rsync owner/group stopped working. [GH-3485] - provisioners/docker: Fix issue where we weren't waiting for Docker to properly start before issuing commands. [GH-3482] - provisioners/shell: Better validation of master config path, results in no more stack traces at runtime. [GH-3505] ## 1.5.3 (April 14, 2014) IMPROVEMENTS: - core: 1.5 upgrade code gives users a chance to quit. [GH-3212] - commands/rsync-auto: An initial sync is done before watching folders. [GH-3327] - commands/rsync-auto: Exit immediately if there are no paths to watch. [GH-3446] - provisioners/ansible: custom vars/hosts files can be added in .vagrant/provisioners/ansible/inventory/ directory [GH-3436] BUG FIXES: - core: Randomize some filenames internally to improve the parallelism of Vagrant. [GH-3386] - core: Don't error if network problems cause box update check to fail [GH-3391] - core: `vagrant` on Windows cmd.exe doesn't always exit with exit code zero. [GH-3420] - core: Adding a box from a network share has nice error on Windows. [GH-3279] - core: Setting an ID on a provisioner now works. [GH-3424] - core: All synced folder paths containing symlinks are fully expanded before sharing. [GH-3444] - core: Windows no longer sees "process not started" errors rarely. - commands/box/repackage: Works again. [GH-3372] - commands/box/update: Update should check for updates from latest version. [GH-3452] - commands/package: Nice error if includes contain symlinks. [GH-3200] - commands/rsync-auto: Don't crash if the machine can't be communicated to. [GH-3419] - communicators/ssh: Throttle connection attempt warnings if the warnings are the same. [GH-3442] - guests/coreos: Docker provisioner works. [GH-3425] - guests/fedora: Fix hostname setting. [GH-3382] - guests/fedora: Support predictable network interface names for public/private networks. [GH-3207] - guests/linux: Rsync folders have proper group if owner not set. [GH-3223] - guests/linux: If SMB folder mounting fails, the password will no longer be shown in plaintext in the output. [GH-3203] - guests/linux: SMB mount works with passwords with symbols. [GH-3202] - providers/hyperv: Check for PowerShell features. [GH-3398] - provisioners/docker: Don't automatically generate container name with a forward slash. [GH-3216] - provisioners/shell: Empty shell scripts don't cause errors. [GH-3423] - synced\_folders/smb: Only set the chmod properly by default on Windows if it isn't already set. [GH-3394] - synced\_folders/smb: Sharing folders with odd characters like parens works properly now. [GH-3405] ## 1.5.2 (April 2, 2014) IMPROVEMENTS: - **New guest:** SmartOS - core: Change wording from "error" to "warning" on SSH retry output to convey actual meaning. - commands/plugin: Listing plugins now has machine-readable output. [GH-3293] - guests/omnios: Mount NFS capability [GH-3282] - synced\_folders/smb: Verify PowerShell v3 or later is running. [GH-3257] BUG FIXES: - core: Vagrant won't collide with newer versions of Bundler [GH-3193] - core: Allow provisioner plugins to not have a config class. [GH-3272] - core: Removing a specific box version that doesn't exist doesn't crash Vagrant. [GH-3364] - core: SSH commands are forced to be ASCII. - core: private networks with DHCP type work if type parameter is a string and not a symbol. [GH-3349] - core: Converting to cygwin path works for folders with spaces. [GH-3304] - core: Can add boxes with spaces in their path. [GH-3306] - core: Prerelease plugins installed are locked to that prerelease version. [GH-3301] - core: Better error message when adding a box with a malformed version. [GH-3332] - core: Fix a rare issue where vagrant up would complain it couldn't check version of a box that doesn't exist. [GH-3326] - core: Box version constraint can't be specified with old-style box. [GH-3260] - commands/box: Show versions when listing. [GH-3316] - commands/box: Outdated check can list local boxes that are newer. [GH-3321] - commands/status: Machine readable output contains the target. [GH-3218] - guests/arch: Reload udev rules after network change. [GH-3322] - guests/debian: Changing host name works properly. [GH-3283] - guests/suse: Shutdown works correctly on SLES [GH-2775] - hosts/linux: Don't hardcode `exportfs` path. Now searches the PATH. [GH-3292] - providers/hyperv: Resume command works properly. [GH-3336] - providers/virtualbox: Add missing translation for stopping status. [GH-3368] - providers/virtualbox: Host-only networks set cableconnected property to "yes" [GH-3365] - provisioners/docker: Use proper flags for 0.9. [GH-3356] - synced\_folders/rsync: Set chmod flag by default on Windows. [GH-3256] - synced\_folders/smb: IDs of synced folders are hashed to work better with VMware. [GH-3219] - synced\_folders/smb: Properly remove existing folders with the same name. [GH-3354] - synced\_folders/smb: Passwords with symbols now work. [GH-3242] - synced\_folders/smb: Exporting works for non-english locale Windows machines. [GH-3251] ## 1.5.1 (March 13, 2014) IMPROVEMENTS: - guests/tinycore: Will now auto-install rsync. - synced\_folders/rsync: rsync-auto will not watch filesystem for excluded paths. [GH-3159] BUG FIXES: - core: V1 Vagrantfiles can upgrade provisioners properly. [GH-3092] - core: Rare EINVAL errors on box adding are gone. [GH-3094] - core: Upgrading the home directory for Vagrant 1.5 uses the Vagrant temp dir. [GH-3095] - core: Assume a box isn't metadata if it exceeds 20 MB. [GH-3107] - core: Asking for input works even in consoles that don't support hiding input. [GH-3119] - core: Adding a box by path in Cygwin on Windows works. [GH-3132] - core: PowerShell scripts work when they're in a directory with spaces. [GH-3100] - core: If you add a box path that doesn't exist, error earlier. [GH-3091] - core: Validation on forwarded ports to make sure they're between 0 and 65535. [GH-3187] - core: Downloads with user/password use the curl `-u` flag. [GH-3183] - core: `vagrant help` no longer loads the Vagrantfile. [GH-3180] - guests/darwin: Fix an exception when configuring networks. [GH-3143] - guests/linux: Only chown folders/files in rsync if they don't have the proper owner. [GH-3186] - hosts/linux: Unusual sed delimiter to avoid conflicts. [GH-3167] - providers/virtualbox: Make more internal interactions with VBoxManage retryable to avoid spurious VirtualBox errors. [GH-2831] - providers/virtualbox: Import progress works again on Windows. - provisioners/ansible: Request SSH info within the provision method, when we know its available. [GH-3111] - synced\_folders/rsync: owner/group settings work. [GH-3163] ## 1.5.0 (March 10, 2014) BREAKING CHANGES: - provisioners/ansible: the machine name (taken from Vagrantfile) is now set as default limit to ensure that vagrant provision steps only affect the expected machine. DEPRECATIONS: - provisioners/chef-solo: The "nfs" setting has been replaced by `synced_folder_type`. The "nfs" setting will be removed in the next version. - provisioners/puppet: The "nfs" setting has been replaced by `synced_folder_type`. The "nfs" setting will be removed in the next version. FEATURES: - **New provider:** Hyper-V. If you're on a Windows machine with Hyper-V enabled, Vagrant can now manage Hyper-V virtual machines out of the box. - **New guest:** Funtoo (change host name and networks supported) - **New guest:** NetBSD - **New guest:** TinyCore Linux. This allows features such as networking, halting, rsync and more work with Boot2Docker. - **New synced folder type:** rsync - Does a one-time one-directional sync to the guest machine. New commands `vagrant rsync` and `vagrant rsync-auto` can resync the folders. - **New synced folder type:** SMB- Allows bi-directional folder syncing using SMB on Windows hosts with any guest. - Password-based SSH authentication. This lets you use almost any off-the-shelf virtual machine image with Vagrant. Additionally, Vagrant will automatically insert a keypair into the machine. - Plugin versions can now be constrained to a range of versions. Example: `vagrant plugin install foo --plugin-version "> 1.0, < 1.1"` - Host-specific operations now use a "host capabilities" system much like guests have used "guest capabilities" for a few releases now. This allows plugin developers to create pluggable host-specific capabilities and makes further integrating Vagrant with new operating systems even easier. - You can now override provisioners within sub-VM configuration and provider overrides. See documentation for more info. [GH-1113] - providers/virtualbox: Provider-specific configuration `cpus` can be used to set the number of CPUs on the VM [GH-2800] - provisioners/docker: Can now build images using `docker build`. [GH-2615] IMPROVEMENTS: - core: Added "error-exit" type to machine-readable output which contains error information that caused a non-zero exit status. [GH-2999] - command/destroy: confirmation will re-ask question if bad input. [GH-3027] - guests/solaris: More accurate Solaris >= 11, < 11 detection. [GH-2824] - provisioners/ansible: Generates a single inventory file, rather than one per machine. See docs for more info. [GH-2991] - provisioners/ansible: SSH forwarding support. [GH-2952] - provisioners/ansible: Multiple SSH keys can now be attempted [GH-2952] - provisioners/ansible: Disable SSH host key checking by default, which improves the experience. We believe this is a sane default for ephemeral dev machines. - provisioners/chef-solo: New config `synced_folder_type` replaces the `nfs` option. This can be used to set the synced folders the provisioner needs to any type. [GH-2709] - provisioners/chef-solo: `roles_paths` can now be an array of paths in Chef 11.8.0 and newer. [GH-2975] - provisioners/docker: Can start a container without daemonization. - provisioners/docker: Started containers are given names. [GH-3051] - provisioners/puppet: New config `synced_folder_type` replaces the `nfs` option. This can be used to set the synced folders the provisioner needs to any type. [GH-2709] - commands/plugin: `vagrant plugin update` will now update all installed plugins, respecting any constraints set. - commands/plugin: `vagrant plugin uninstall` can now uninstall multiple plugins. - commands/plugin: `vagrant plugin install` can now install multiple plugins. - hosts/redhat: Recognize Korora OS. [GH-2869] - synced\_folders/nfs: If the guest supports it, NFS clients will be automatically installed in the guest. BUG FIXES: - core: If an exception was raised while attempting to connect to SSH for the first time, it would get swallowed. It is properly raised now. - core: Plugin installation does not fail if your local gemrc file has syntax errors. - core: Plugins that fork within certain actions will no longer hang indefinitely. [GH-2756] - core: Windows checks home directory permissions more correctly to warn of potential issues. - core: Synced folders set to the default synced folder explicitly won't be deleted. [GH-2873] - core: Static IPs can end in ".1". A warning is now shown. [GH-2914] - core: Adding boxes that have directories in them works on Windows. - core: Vagrant will not think provisioning is already done if the VM is manually deleted outside of Vagrant. - core: Box file checksums of large files works properly on Windows. [GH-3045] - commands/box: Box add `--force` works with `--provider` flag. [GH-2757] - commands/box: Listing boxes with machine-readable output crash is gone. - commands/plugin: Plugin installation will fail if dependencies conflict, rather than at runtime. - commands/ssh: When using `-c` on Windows, no more TTY errors. - commands/ssh-config: ProxyCommand is included in output if it is set. [GH-2950] - guests/coreos: Restart etcd after configuring networks. [GH-2852] - guests/linux: Don't chown VirtualBox synced folders if mounting as readonly. [GH-2442] - guests/redhat: Set hostname to FQDN, per the documentation for RedHat. [GH-2792] - hosts/bsd: Don't invoke shell for NFS sudo calls. [GH-2808] - hosts/bsd: Sort NFS exports to avoid false validation errors. [GH-2927] - hosts/bsd: No more checkexports NFS errors if you're sharing the same directory. [GH-3023] - hosts/gentoo: Look for systemctl in `/usr/bin` [GH-2858] - hosts/linux: Properly escape regular expression to prune NFS exports, allowing VMware to work properly. [GH-2934] - hosts/opensuse: Start NFS server properly. [GH-2923] - providers/virtualbox: Enabling internal networks by just setting "true" works properly. [GH-2751] - providers/virtualbox: Make more internal interactions with VBoxManage retryable to avoid spurious VirtualBox errors. [GH-2831] - providers/virtualbox: Config validation catches invalid keys. [GH-2843] - providers/virtualbox: Fix network adapter configuration issue if using provider-specific config. [GH-2854] - providers/virtualbox: Bridge network adapters always have their "cable connected" properly. [GH-2906] - provisioners/chef: When chowning folders, don't follow symlinks. - provisioners/chef: Encrypted data bag secrets also in Chef solo are now uploaded to the provisioning path to avoid perm issues. [GH-2845] - provisioners/chef: Encrypted data bag secret is removed from the machine before and after provisioning also with Chef client. [GH-2845] - provisioners/chef: Set `encrypted_data_bag_secret` on the VM to `nil` if the secret is not specified. [GH-2984] - provisioners/chef: Fix loading of the custom configure file. [GH-876] - provisioners/docker: Only add SSH user to docker group if the user isn't already in it. [GH-2838] - provisioners/docker: Configuring autostart works properly with the newest versions of Docker. [GH-2874] - provisioners/puppet: Append default module path to the module paths always. [GH-2677] - provisioners/salt: Setting pillar data doesn't require `deep_merge` plugin anymore. [GH-2348] - provisioners/salt: Options can now set install type and install args. [GH-2766] - provisioners/salt: Fix case when salt would say "options only allowed before install arguments" [GH-3005] - provisioners/shell: Error if script is encoded incorrectly. [GH-3000] - synced\_folders/nfs: NFS entries are pruned on every `vagrant up`, if there are any to prune. [GH-2738] ## 1.4.3 (January 2, 2014) BUG FIXES: - providers/virtualbox: `vagrant package` works properly again. [GH-2739] ## 1.4.2 (December 31, 2013) IMPROVEMENTS: - guests/linux: emit upstart event when NFS folders are mounted. [GH-2705] - provisioners/chef-solo: Encrypted data bag secret is removed from the machine after provisioning. [GH-2712] BUG FIXES: - core: Ctrl-C no longer raises "trap context" exception. - core: The version for `Vagrant.configure` can now be an int. [GH-2689] - core: `Vagrant.has_plugin?` tries to use plugin's gem name before registered plugin name [GH-2617] - core: Fix exception if an EOFError was somehow raised by Ruby while checking a box checksum. [GH-2716] - core: Better error message if your plugin state file becomes corrupt somehow. [GH-2694] - core: Box add will fail early if the box already exists. [GH-2621] - hosts/bsd: Only run `nfsd checkexports` if there is an exports file. [GH-2714] - commands/plugin: Fix exception that could happen rarely when installing a plugin. - providers/virtualbox: Error when packaging if the package already exists _before_ the export is done. [GH-2380] - providers/virtualbox: NFS with static IP works even if VirtualBox guest additions aren't installed (regression). [GH-2674] - synced\_folders/nfs: sudo will only ask for password one at a time when using a parallel provider [GH-2680] ## 1.4.1 (December 18, 2013) IMPROVEMENTS: - hosts/bsd: check NFS exports file for issues prior to exporting - provisioners/ansible: Add ability to use Ansible groups in generated inventory [GH-2606] - provisioners/docker: Add support for using the provisioner with RedHat based guests [GH-2649] - provisioners/docker: Remove "Docker" prefix from Client and Installer classes [GH-2641] BUG FIXES: - core: box removal of a V1 box works - core: `vagrant ssh -c` commands are now executed in the context of a login shell (regression). [GH-2636] - core: specifying `-t` or `-T` to `vagrant ssh -c` as extra args will properly enable/disable a TTY for OpenSSH. [GH-2618] - commands/init: Error if can't write Vagrantfile to directory. [GH-2660] - guests/debian: fix `use_dhcp_assigned_default_route` to work properly. [GH-2648] - guests/debian,ubuntu: fix change\_host\_name for FQDNs with trailing dots [GH-2610] - guests/freebsd: configuring networks in the guest works properly [GH-2620] - guests/redhat: fix configure networks bringing down interfaces that don't exist. [GH-2614] - providers/virtualbox: Don't override NFS exports for all VMs when coming up. [GH-2645] - provisioners/ansible: Array arguments work for raw options [GH-2667] - provisioners/chef-client: Fix node/client deletion when node\_name is not set. [GH-2345] - provisioners/chef-solo: Force remove files to avoid cases where a prompt would be shown to users. [GH-2669] - provisioners/puppet: Don't prepend default module path for Puppet in case Puppet is managing its own paths. [GH-2677] ## 1.4.0 (December 9, 2013) FEATURES: - New provisioner: Docker. Install Docker, pull containers, and run containers easier than ever. - Machine readable output. Vagrant now has machine-friendly output by using the `--machine-readable` flag. - New plugin type: synced folder implementation. This allows new ways of syncing folders to be added as plugins to Vagrant. - The `Vagrant.require_version` function can be used at the top of a Vagrantfile to enforce a minimum/maximum Vagrant version. - Adding boxes via `vagrant box add` and the Vagrantfile both support providing checksums of the box files. - The `--debug` flag can be specified on any command now to get debug-level log output to ease reporting bugs. - You can now specify a memory using `vb.memory` setting with VirtualBox. - Plugin developers can now hook into `environment_plugins_loaded`, which is executed after plugins are loaded but before Vagrantfiles are parsed. - VirtualBox internal networks are now supported. [GH-2020] IMPROVEMENTS: - core: Support resumable downloads [GH-57] - core: owner/group of shared folders can be specified by integers. [GH-2390] - core: the VAGRANT\_NO\_COLOR environmental variable may be used to enable `--no-color` mode globally. [GH-2261] - core: box URL and add date is tracked and shown if `-i` flag is specified for `vagrant box list` [GH-2327] - core: Multiple SSH keys can be specified with `config.ssh.private_key_path` [GH-907] - core: `config.vm.box_url` can be an array of URLs. [GH-1958] - commands/box/add: Can now specify a custom CA cert for verifying certs from a custom CA. [GH-2337] - commands/box/add: Can now specify a client cert when downloading a box. [GH-1889] - commands/init: Add `--output` option for specifying output path, or "-" for stdin. [GH-1364] - commands/provision: Add `--no-parallel` option to disable provider parallelization if the provider supports it. [GH-2404] - commands/ssh: SSH compression is enabled by default. [GH-2456] - commands/ssh: Inline commands specified with "-c" are now executed using OpenSSH rather than pure-Ruby SSH. It is MUCH faster, and stdin works! - communicators/ssh: new configuration `config.ssh.pty` is a boolean for whether you want ot use a PTY for provisioning. - guests/linux: emit upstart event `vagrant-mounted` if upstart is available. [GH-2502] - guests/pld: support changing hostname [GH-2543] - providers/virtualbox: Enable symlinks for VirtualBox 4.1. [GH-2414] - providers/virtualbox: default VM name now includes milliseconds with a random number to try to avoid conflicts in CI environments. [GH-2482] - providers/virtualbox: customizations via VBoxManage are retried, avoiding VirtualBox flakiness [GH-2483] - providers/virtualbox: NFS works with DHCP host-only networks now. [GH-2560] - provisioners/ansible: allow files for extra vars [GH-2366] - provisioners/puppet: client cert and private key can now be specified for the puppet server provisioner. [GH-902] - provisioners/puppet: the manifests path can be in the VM. [GH-1805] - provisioners/shell: Added `keep_color` option to not automatically color output based on stdout/stderr. [GH-2505] - provisioners/shell: Arguments can now be an array of args. [GH-1949] - synced\_folders/nfs: Specify `nfs_udp` to false to disable UDP based NFS folders. [GH-2304] BUG FIXES: - core: Make sure machine IDs are always strings. [GH-2434] - core: 100% CPU spike when waiting for SSH is fixed. [GH-2401] - core: Command lookup works on systems where PATH is not valid UTF-8 [GH-2514] - core: Human-friendly error if box metadata.json becomes corrupted. [GH-2305] - core: Don't load Vagrantfile on `vagrant plugin` commands, allowing Vagrantfiles that use plugins to work. [GH-2388] - core: global flags are ignored past the "--" on the CLI. [GH-2491] - core: provisioning will properly happen if `up` failed. [GH-2488] - guests/freebsd: Mounting NFS folders works. [GH-2400] - guests/freebsd: Uses `sh` by default for shell. [GH-2485] - guests/linux: upstart events listening for `vagrant-mounted` won't wait for jobs to complete, fixing issues with blocking during vagrant up [GH-2564] - guests/redhat: `DHCP_HOSTNAME` is set to the hostname, not the FQDN. [GH-2441] - guests/redhat: Down interface before messing up configuration file for networking. [GH-1577] - guests/ubuntu: "localhost" is preserved when changing hostnames. [GH-2383] - hosts/bsd: Don't set mapall if maproot is set in NFS. [GH-2448] - hosts/gentoo: Support systemd for NFS startup. [GH-2382] - providers/virtualbox: Don't start new VM if VirtualBox has transient failure during `up` from suspended. [GH-2479] - provisioners/chef: Chef client encrypted data bag secrets are now uploaded to the provisioning path to avoid perm issues. [GH-1246] - provisioners/chef: Create/chown the cache and backup folders. [GH-2281] - provisioners/chef: Verify environment paths exist in config validation step. [GH-2381] - provisioners/puppet: Multiple puppet definitions in a Vagrantfile work correctly. - provisioners/salt: Bootstrap on FreeBSD systems work. [GH-2525] - provisioners/salt: Extra args for bootstrap are put in the proper location. [GH-2558] ## 1.3.5 (October 15, 2013) FEATURES: - VirtualBox 4.3 is now supported. [GH-2374] - ESXi is now a supported guest OS. [GH-2347] IMPROVEMENTS: - guests/redhat: Oracle Linux is now supported. [GH-2329] - provisioners/salt: Support running overstate. [GH-2313] BUG FIXES: - core: Fix some places where "no error message" errors were being reported when in fact there were errors. [GH-2328] - core: Disallow hyphens or periods for starting hostnames. [GH-2358] - guests/ubuntu: Setting hostname works properly. [GH-2334] - providers/virtualbox: Retryable VBoxManage commands are properly retried. [GH-2365] - provisioners/ansible: Verbosity won't be blank by default. [GH-2320] - provisioners/chef: Fix exception raised during Chef client node cleanup. [GH-2345] - provisioners/salt: Correct master seed file name. [GH-2359] ## 1.3.4 (October 2, 2013) FEATURES: - provisioners/shell: Specify the `binary` option as true and Vagrant won't automatically replace Windows line endings with Unix ones. [GH-2235] IMPROVEMENTS: - guests/suse: Support installing CFEngine. [GH-2273] BUG FIXES: - core: Don't output `\e[0K` anymore on Windows. [GH-2246] - core: Only modify `DYLD_LIBRARY_PATH` on Mac when executing commands in the installer context. [GH-2231] - core: Clear `DYLD_LIBRARY_PATH` on Mac if the subprocess is executing a setuid or setgid script. [GH-2243] - core: Defined action hook names can be strings now. They are converted to symbols internally. - guests/debian: FQDN is properly set when setting the hostname. [GH-2254] - guests/linux: Fix poor chown command for mounting VirtualBox folders. - guests/linux: Don't raise exception right away if mounting fails, allow retries. [GH-2234] - guests/redhat: Changing hostname changes DHCP_HOSTNAME. [GH-2267] - hosts/arch: Vagrant won't crash on Arch anymore. [GH-2233] - provisioners/ansible: Extra vars are converted to strings. [GH-2244] - provisioners/ansible: Output will show up on a task-by-task basis. [GH-2194] - provisioners/chef: Propagate disabling color if Vagrant has no color enabled. [GH-2246] - provisioners/chef: Delete from chef server exception fixed. [GH-2300] - provisioners/puppet: Work with restrictive umask. [GH-2241] - provisioners/salt: Remove bootstrap definition file on each run in order to avoid permissions issues. [GH-2290] ## 1.3.3 (September 18, 2013) BUG FIXES: - core: Fix issues with dynamic linker not finding symbols on OS X. [GH-2219] - core: Properly clean up machine directories on destroy. [GH-2223] - core: Add a timeout to waiting for SSH connection and server headers on SSH. [GH-2226] ## 1.3.2 (September 17, 2013) IMPROVEMENTS: - provisioners/ansible: Support more verbosity levels, better documentation. [GH-2153] - provisioners/ansible: Add `host_key_checking` configuration. [GH-2203] BUG FIXES: - core: Report the proper invalid state when waiting for the guest machine to be ready - core: `Guest#capability?` now works with strings as well - core: Fix NoMethodError in the new `Vagrant.has_plugin?` method [GH-2189] - core: Convert forwarded port parameters to integers. [GH-2173] - core: Don't spike CPU to 100% while waiting for machine to boot. [GH-2163] - core: Increase timeout for individual SSH connection to 60 seconds. [GH-2163] - core: Call realpath after creating directory so NFS directory creation works. [GH-2196] - core: Don't try to be clever about deleting the machine state directory anymore. Manually done in destroy actions. [GH-2201] - core: Find the root Vagrantfile only if Vagrantfile is a file, not a directory. [GH-2216] - guests/linux: Try `id -g` in addition to `getent` for mounting VirtualBox shared folders [GH-2197] - hosts/arch: NFS exporting works properly, no exceptions. [GH-2161] - hosts/bsd: Use only `sudo` for writing NFS exports. This lets NFS exports work if you have sudo privs but not `su`. [GH-2191] - hosts/fedora: Fix host detection encoding issues. [GH-1977] - hosts/linux: Fix NFS export problems with `no_subtree_check`. [GH-2156] - installer/mac: Vagrant works properly when a library conflicts from homebrew. [GH-2188] - installer/mac: deb/rpm packages now have an epoch of 1 so that new installers don't appear older. [GH-2179] - provisioners/ansible: Default output level is now verbose again. [GH-2194] - providers/virtualbox: Fix an issue where destroy middlewares weren't being properly called. [GH-2200] ## 1.3.1 (September 6, 2013) BUG FIXES: - core: Fix various issues where using the same options hash in a Vagrantfile can cause errors. - core: `VAGRANT_VAGRANTFILE` env var only applies to the project Vagrantfile name. [GH-2130] - core: Fix an issue where the data directory would be deleted too quickly in a multi-VM environment. - core: Handle the case where we get an EACCES cleaning up the .vagrant directory. - core: Fix exception on upgrade warnings from V1 to V2. [GH-2142] - guests/coreos: Proper IP detection. [GH-2146] - hosts/linux: NFS exporting works properly again. [GH-2137] - provisioners/chef: Work even with restrictive umask on user. [GH-2121] - provisioners/chef: Fix environment validation to be less restrictive. - provisioners/puppet: No more "shared folders cannot be found" error. [GH-2134] - provisioners/puppet: Work with restrictive umask on user by testing for folders with sudo. [GH-2121] ## 1.3.0 (September 5, 2013) BACKWARDS INCOMPATIBILITY: - `config.ssh.max_tries` is gone. Instead of maximum tries, Vagrant now uses a simple overall timeout value `config.vm.boot_timeout` to wait for the machine to boot up. - `config.vm.graceful_halt_retry_*` settings are gone. Instead, a single timeout is now used to wait for a graceful halt to work, specified by `config.vm.graceful_halt_timeout`. - The ':extra' flag to shared folders for specifying arbitrary mount options has been replaced with the `:mount_options` flag, which is now an array of mount options. - `vagrant up` will now only run provisioning by default the first time it is run. Subsequent `reload` or `up` will need to explicitly specify the `--provision` flag to provision. [GH-1776] FEATURES: - New command: `vagrant plugin update` to update specific installed plugins. - New provisioner: File provisioner. [GH-2112] - New provisioner: Salt provisioner. [GH-1626] - New guest: Mac OS X guest support. [GH-1914] - New guest: CoreOS guest support. Change host names and configure networks on CoreOS. [GH-2022] - New guest: Solaris 11 guest support. [GH-2052] - Support for environments in the Chef-solo provisioner. [GH-1915] - Provisioners can now define "cleanup" tasks that are executed on `vagrant destroy`. [GH-1302] - Chef Client provisioner will now clean up the node/client using `knife` if configured to do so. - `vagrant up` has a `--no-destroy-on-error` flag that will not destroy the VM if a fatal error occurs. [GH-2011] - NFS: Arbitrary mount options can be specified using the `mount_options` option on synced folders. [GH-1029] - NFS: Arbitrary export options can be specified using `bsd__nfs_options` and `linux__nfs_options`. [GH-1029] - Static IP can now be set on public networks. [GH-1745] - Add `Vagrant.has_plugin?` method for use in Vagrantfile to check if a plugin is installed. [GH-1736] - Support for remote shell provisioning scripts [GH-1787] IMPROVEMENTS: - core: add `--color` to any Vagrant command to FORCE color output. [GH-2027] - core: "config.vm.host_name" works again, just an alias to hostname. - core: Reboots via SSH are now handled gracefully (without exception). - core: Mark `disabled` as true on forwarded port to disable. [GH-1922] - core: NFS exports are now namespaced by user ID, so pruning NFS won't remove exports from other users. [GH-1511] - core: "vagrant -v" no longer loads the Vagrantfile - commands/box/remove: Fix stack trace that happens if no provider is specified. [GH-2100] - commands/plugin/install: Post install message of a plugin will be shown if available. [GH-1986] - commands/status: cosmetic improvement to better align names and statuses [GH-2016] - communicators/ssh: Support a proxy_command. [GH-1537] - guests/openbsd: support configuring networks, changing host name, and mounting NFS. [GH-2086] - guests/suse: Supports private/public networks. [GH-1689] - hosts/fedora: Support RHEL as a host. [GH-2088] - providers/virtualbox: "post-boot" customizations will run directly after boot, and before waiting for SSH. [GH-2048] - provisioners/ansible: Many more configuration options. [GH-1697] - provisioners/ansible: Ansible `inventory_path` can be a directory now. [GH-2035] - provisioners/ansible: Extra verbose option by setting `config.verbose` to `extra`. [GH-1979] - provisioners/ansible: `inventory_path` will be auto-generated if not specified. [GH-1907] - provisioners/puppet: Add `nfs` option to puppet provisioner. [GH-1308] - provisioners/shell: Set the `privileged` option to false to run without sudo. [GH-1370] BUG FIXES: - core: Clean up ".vagrant" folder more effectively. - core: strip newlines off of ID file values [GH-2024] - core: Multiple forwarded ports with different protocols but the same host port can be specified. [GH-2059] - core: `:nic_type` option for private networks is respected. [GH-1704] - commands/up: provision-with validates the provisioners given. [GH-1957] - guests/arch: use systemd way of setting host names. [GH-2041] - guests/debian: Force bring up eth0. Fixes hangs on setting hostname. [GH-2026] - guests/ubuntu: upstart events are properly emitted again. [GH-1717] - hosts/bsd: Nicer error if can't read NFS exports. [GH-2038] - hosts/fedora: properly detect later CentOS versions. [GH-2008] - providers/virtualbox: VirtualBox 4.2 now supports up to 36 network adapters. [GH-1886] - provisioners/ansible: Execute ansible with a cwd equal to the path where the Vagrantfile is. [GH-2051] - provisioners/all: invalid config keys will be properly reported. [GH-2117] - provisioners/ansible: No longer report failure on every run. [GH-2007] - provisioners/ansible: Properly handle extra vars with spaces. [GH-1984] - provisioners/chef: Formatter option works properly. [GH-2058] - provisioners/chef: Create/chown the provisioning folder before reading contents. [GH-2121] - provisioners/puppet: mount synced folders as root to avoid weirdness - provisioners/puppet: Run from the correct working directory. [GH-1967] with Puppet. [GH-2015] - providers/virtualbox: Use `getent` to get the group ID instead of `id` in case the name doesn't have a user. [GH-1801] - providers/virtualbox: Will only set the default name of the VM on initial `up`. [GH-1817] ## 1.2.7 (July 28, 2013) BUG FIXES: - On Windows, properly convert synced folder host path to a string so that separator replacement works properly. - Use `--color=false` for no color in Puppet to support older versions properly. [GH-2000] - Make sure the hostname configuration is a string. [GH-1999] - cURL downloads now contain a user agent which fixes some issues with downloading Vagrant through proxies. [GH-2003] - `vagrant plugin install` will now always properly show the actual installed gem name. [GH-1834] ## 1.2.6 (July 26, 2013) BUG FIXES: - Box collections with multiple formats work properly by converting the supported formats to symbols. [GH-1990] ## 1.2.5 (July 26, 2013) FEATURES: - `vagrant help ` now works. [GH-1578] - Added `config.vm.box_download_insecure` to allow the box_url setting to point to an https site that won't be validated. [GH-1712] - VirtualBox VBoxManage customizations can now be specified to run pre-boot (the default and existing functionality, pre-import, or post-boot. [GH-1247] - VirtualBox no longer destroys unused network interfaces by default. This didn't work across multi-user systems and required admin privileges on Windows, so it has been disabled by default. It can be enabled using the VirtualBox provider-specific `destroy_unused_network_interfaces` configuration by setting it to true. [GH-1324] IMPROVEMENTS: - Remote commands that fail will now show the stdout/stderr of the command that failed. [GH-1203] - Puppet will run without color if the UI is not colored. [GH-1344] - Chef supports the "formatter" configuration for setting the formatter. [GH-1250] - VAGRANT_DOTFILE_PATH environmental variable reintroduces the functionality removed in 1.1 from "config.dotfile_name" [GH-1524] - Vagrant will show an error if VirtualBox 4.2.14 is running. - Added provider to BoxNotFound error message. [GH-1692] - If Ansible fails to run properly, show an error message. [GH-1699] - Adding a box with the `--provider` flag will now allow a box for any of that provider's supported formats. - NFS mounts enable UDP by default, resulting in higher performance. (Because mount is over local network, packet loss is not an issue) [GH-1706] BUG FIXES: - `box_url` now handles the case where the provider doesn't perfectly match the provider in use, but the provider supports it. [GH-1752] - Fix uninitialized constant error when configuring Arch Linux network. [GH-1734] - Debian/Ubuntu change hostname works properly if eth0 is configured with hot-plugging. [GH-1929] - NFS exports with improper casing on Mac OS X work properly. [GH-1202] - Shared folders overriding '/vagrant' in multi-VM environments no longer all just use the last value. [GH-1935] - NFS export fsid's are now 32-bit integers, rather than UUIDs. This lets NFS exports work with Linux kernels older than 2.6.20. [GH-1127] - NFS export allows access from all private networks on the VM. [GH-1204] - Default VirtualBox VM name now contains the machine name as defined in the Vagrantfile, helping differentiate multi-VM. [GH-1281] - NFS works properly on CentOS hosts. [GH-1394] - Solaris guests actually shut down properly. [GH-1506] - All provisioners only output newlines when the provisioner sends a newline. This results in the output looking a lot nicer. - Sharing folders works properly if ".profile" contains an echo. [GH-1677] - `vagrant ssh-config` IdentityFile is only wrapped in quotes if it contains a space. [GH-1682] - Shared folder target path can be a Windows path. [GH-1688] - Forwarded ports don't auto-correct by default, and will raise an error properly if they collide. [GH-1701] - Retry SSH on ENETUNREACH error. [GH-1732] - NFS is silently ignored on Windows. [GH-1748] - Validation so that private network static IP does not end in ".1" [GH-1750] - With forward agent enabled and sudo being used, Vagrant will automatically discover and set `SSH_AUTH_SOCK` remotely so that forward agent works properly despite misconfigured sudoers. [GH-1307] - Synced folder paths on Windows containing '\' are replaced with '/' internally so that they work properly. - Unused config objects are finalized properly. [GH-1877] - Private networks work with Fedora guests once again. [GH-1738] - Default internal encoding of strings in Vagrant is now UTF-8, allowing detection of Fedora to work again (which contained a UTF-8 string). [GH-1977] ## 1.2.4 (July 16, 2013) FEATURES: - Chef solo and client provisioning now support a `custom_config_path` setting that accepts a path to a Ruby file to load as part of Chef configuration, allowing you to override any setting available. [GH-876] - CFEngine provisioner: you can now specify the package name to install, so CFEngine enterprise is supported. [GH-1920] IMPROVEMENTS: - `vagrant box remove` works with only the name of the box if that box exists only backed by one provider. [GH-1032] - `vagrant destroy` returns exit status 1 if any of the confirmations are declined. [GH-923] - Forwarded ports can specify a host IP and guest IP to bind to. [GH-1121] - You can now set the "ip" of a private network that uses DHCP. This will change the subnet and such that the DHCP server uses. - Add `file_cache_path` support for chef_solo. [GH-1897] BUG FIXES: - VBoxManage or any other executable missing from PATH properly reported. Regression from 1.2.2. [GH-1928] - Boxes downloaded as part of `vagrant up` are now done so _prior_ to config validation. This allows Vagrantfiles to references files that may be in the box itself. [GH-1061] - Chef removes dna.json and encrypted data bag secret file prior to uploading. [GH-1111] - NFS synced folders exporting sub-directories of other exported folders now works properly. [GH-785] - NFS shared folders properly dereference symlinks so that the real path is used, avoiding mount errors [GH-1101] - SSH channel is closed after the exit status is received, potentially eliminating any SSH hangs. [GH-603] - Fix regression where VirtualBox detection wasn't working anymore. [GH-1918] - NFS shared folders with single quotes in their name now work properly. [GH-1166] - Debian/Ubuntu request DHCP renewal when hostname changes, which will fix issues with FQDN detecting. [GH-1929] - SSH adds the "DSAAuthentication=yes" option in case that is disabled on the user's system. [GH-1900] ## 1.2.3 (July 9, 2013) FEATURES: - Puppet provisioner now supports Hiera by specifying a `hiera_config_path`. - Added a `working_directory` configuration option to the Puppet apply provisioner so you can specify the working directory when `puppet` is called, making it friendly to Hiera data and such. [GH-1670] - Ability to specify the host IP to bind forwarded ports to. [GH-1785] IMPROVEMENTS: - Setting hostnames works properly on OmniOS. [GH-1672] - Better VBoxManage error detection on Windows systems. This avoids some major issues where Vagrant would sometimes "lose" your VM. [GH-1669] - Better detection of missing VirtualBox kernel drivers on Linux systems. [GH-1671] - More precise detection of Ubuntu/Debian guests so that running Vagrant within an LXC container works properly now. - Allow strings in addition to symbols to more places in V1 configuration as well as V2 configuration. - Add `ARPCHECK=0` to RedHat OS family network configuration. [GH-1815] - Add SSH agent forwarding sample to initial Vagrantfile. [GH-1808] - VirtualBox: Only configure networks if there are any to configure. This allows linux's that don't implement this capability to work with Vagrant. [GH-1796] - Default SSH forwarded port now binds to 127.0.0.1 so only local connections are allowed. [GH-1785] - Use `netctl` for Arch Linux network configuration. [GH-1760] - Improve fedora host detection regular expression. [GH-1913] - SSH shows a proper error on EHOSTUNREACH. [GH-1911] BUG FIXES: - Ignore "guest not ready" errors when attempting to graceful halt and carry on checks whether the halt succeeded. [GH-1679] - Handle the case where a roles path for Chef solo isn't properly defined. [GH-1665] - Finding V1 boxes now works properly again to avoid "box not found" errors. [GH-1691] - Setting hostname on SLES 11 works again. [GH-1781] - `config.vm.guest` properly forces guests again. [GH-1800] - The `read_ip_address` capability for linux properly reads the IP of only the first network interface. [GH-1799] - Validate that an IP is given for a private network. [GH-1788] - Fix uninitialized constant error for Gentoo plugin. [GH-1698] ## 1.2.2 (April 23, 2013) FEATURES: - New `DestroyConfirm` built-in middleware for providers so they can more easily conform to the `destroy` action. IMPROVEMENTS: - No longer an error if the Chef run list is empty. It is now a warning. [GH-1620] - Better locking around handling the `box_url` parameter for parallel providers. - Solaris guest is now properly detected on SmartOS, OmniOS, etc. [GH-1639] - Guest addition version detection is more robust, attempting other routes to get the version, and also retrying a few times. [GH-1575] BUG FIXES: - `vagrant package --base` works again. [GH-1615] - Box overrides specified in provider config overrides no longer fail to detect the box. [GH-1617] - In a multi-machine environment, a box not found won't be downloaded multiple times. [GH-1467] - `vagrant box add` with a file path now works correctly on Windows when a drive letter is specified. - DOS line endings are converted to Unix line endings for the shell provisioner automatically. [GH-1495] ## 1.2.1 (April 17, 2013) FEATURES: - Add a `--[no-]parallel` flag to `vagrant up` to enable/disable parallelism. Vagrant will parallelize by default. IMPROVEMENTS: - Get rid of arbitrary 4 second sleep when connecting via SSH. The issue it was attempting to work around may be gone now. BUG FIXES: - Chef solo run list properly set. [GH-1608] - Follow 30x redirects when downloading boxes. [GH-1607] - Chef client config defaults are done properly. [GH-1609] - VirtualBox mounts shared folders with the proper owner/group. [GH-1611] - Use the Mozilla CA cert bundle for cURL so SSL validation works properly. ## 1.2.0 (April 16, 2013) BACKWARDS INCOMPATIBILITIES: - WINDOWS USERS: Vagrant now defaults to using the 'USERPROFILE' environmental variable for the home directory if it is set. This means that the default location for the Vagrant home directory is now `%USERPROFILE%/.vagrant.d`. On Cygwin, this will cause existing Cygwin users to "lose" their boxes. To work around this, either set `VAGRANT_HOME` to your Cygwin ".vagrant.d" folder or move your ".vagrant.d" folder to `USERPROFILE`. The latter is recommended for long-term support. - The constant `Vagrant::Environment::VAGRANT_HOME` was removed in favor of `Vagrant::Environment#default_vagrant_home`. FEATURES: - Providers can now parallelize! If they explicitly support it, Vagrant will run "up" and other commands in parallel. For providers such AWS, this means that your instances will come up in parallel. VirtualBox does not support this mode. - Box downloads are now done via `curl` rather than Ruby's built-in HTTP library. This results in massive speedups, support for SSL verification, FTP downloads, and more. - `config.vm.provider` now takes an optional second parameter to the block, allowing you to override any configuration value. These overrides are applied last, and therefore override any other configuration value. Note that while this feature is available, the "Vagrant way" is instead to use box manifests to ensure that the "box" for every provider matches, so these sorts of overrides are unnecessary. - A new "guest capabilities" system to replace the old "guest" system. This new abstraction allows plugins to define "capabilities" that certain guest operating systems can implement. This allows greater flexibility in doing guest-specific behavior. - Ansible provisioner support. [GH-1465] - Providers can now support multiple box formats by specifying the `box_format:` option. - CFEngine provisioner support. - `config.ssh.default` settings introduced to set SSH defaults that providers can still override. [GH-1479] IMPROVEMENTS: - Full Windows support in cmd.exe, PowerShell, Cygwin, and MingW based environments. - By adding the "disabled" boolean flag to synced folders you can disable them altogether. [GH-1004] - Specify the default provider with the `VAGRANT_DEFAULT_PROVIDER` environmental variable. [GH-1478] - Invalid settings are now caught and shown in a user-friendly way. [GH-1484] - Detect PuTTY Link SSH client on Windows and show an error. [GH-1518] - `vagrant ssh` in Cygwin won't output DOS path file warnings. - Add `--rtcuseutc on` as a sane default for VirtualBox. [GH-912] - SSH will send keep-alive packets every 5 seconds by default to keep connections alive. Can be disabled with `config.ssh.keep_alive`. [GH-516] - Show a message on `vagrant up` if the machine is already running. [GH-1558] - "Running provisioner" output now shoes the provisioner shortcut name, rather than the less-than-helpful class name. - Shared folders with the same guest path will overwrite each other. No more shared folder IDs. - Shell provisioner outputs script it is running. [GH-1568] - Automatically merge forwarded ports that share the same host port. BUG FIXES: - The `:mac` option for host-only networks is respected. [GH-1536] - Don't preserve modified time when untarring boxes. [GH-1539] - Forwarded port auto-correct will not auto-correct to a port that is also in use. - Cygwin will always output color by default. Specify `--no-color` to override this. - Assume Cygwin has a TTY for asking for input. [GH-1430] - Expand Cygwin paths to Windows paths for calls to VBoxManage and for VirtualBox shared folders. - Output the proper clear line text for shells in Cygwin when reporting dynamic progress. - When using `Builder` instances for hooks, the builders will be merged for the proper before/after chain. [GH-1555] - Use the Vagrant temporary directory again for temporary files since they can be quite large and were messing with tmpfs. [GH-1442] - Fix issue parsing extra SSH args in `vagrant ssh` in multi-machine environments. [GH-1545] - Networks come back up properly on RedHat systems after reboot. [GH-921] - `config.ssh` settings override all detected SSH settings (regression). [GH-1479] - `ssh-config` won't raise an exception if the VirtualBox machine is not created. [GH-1562] - Multiple machines defined in the same Vagrantfile with the same name will properly merge. - More robust hostname checking for RedHat. [GH-1566] - Cookbook path existence for Chef is no longer an error, so that things like librarian and berkshelf plugins work properly. [GH-1570] - Chef solo provisioner uses proper SSH username instead of hardcoded config. [GH-1576] - Shell provisioner takes ownership of uploaded files properly so that they can also be manually executed later. [GH-1576] ## 1.1.6 (April 3, 2013) BUG FIXES: - Fix SSH re-use connection logic to drop connection if an error occurs. ## 1.1.5 (April 2, 2013) IMPROVEMENTS: - More robust SSH connection close detection. - Don't load `vagrant plugin` installed plugins when in a Bundler environment. This happens during plugin development. This will make Vagrant errors much quieter when developing plugins. - Vagrant will detect Bundler environments, make assumptions that you're developing plugins, and will quiet its error output a bit. - More comprehensive synced folder configuration validation. - VBoxManage errors now show the output from the command so that users can potentially know what is wrong. BUG FIXES: - Proper error message if invalid provisioner is used. [GH-1515] - Don't error on graceful halt if machine just shut down very quickly. [GH-1505] - Error message if private key for SSH isn't owned by the proper user. [GH-1503] - Don't error too early when `config.vm.box` is not properly set. - Show a human-friendly error if VBoxManage is not found (exit status 126). [GH-934] - Action hook prepend/append will only prepend or append once. - Retry SSH on Errno::EACCES. - Show an error if an invalid network type is used. - Don't share Chef solo folder if it doesn't exist on host. ## 1.1.4 (March 25, 2013) BUG FIXES: - Default forwarded port adapter for VirtualBox should be 1. ## 1.1.3 (March 25, 2013) IMPROVEMENTS: - Puppet apply provisioner now retains the default module path even while specifying custom module paths. [GH-1207] - Re-added DHCP support for host-only networks. [GH-1466] - Ability to specify a plugin version, plugin sources, and pre-release versions using `--plugin-version`, `--plugin-source`, and `--plugin-prerelease`. [GH-1461] - Move VirtualBox guest addition checks to after the machine boots. [GH-1179] - Removed `Vagrant::TestHelpers` because it doesn't really work anymore. - Add PLX linux guest support. [GH-1490] BUG FIXES: - Attempt to re-establish SSH connection on `Net::SSH::Disconnect` - Allow any value that can convert to a string for `Vagrant.plugin` - Chef solo `recipe_url` works properly again. [GH-1467] - Port collision detection works properly in VirtualBox with auto-corrected ports. [GH-1472] - Fix obscure error when temp directory is world writable when adding boxes. - Improved error handling around network interface detection for VirtualBox [GH-1480] ## 1.1.2 (March 18, 2013) BUG FIXES: - When not specifying a cookbooks_path for chef-solo, an error won't be shown if "cookbooks" folder is missing. - Fix typo for exception when no host-only network with NFS. [GH-1448] - Use UNSET_VALUE/nil with args on shell provisioner by default since `[]` was too truthy. [GH-1447] ## 1.1.1 (March 18, 2013) IMPROVEMENTS: - Don't load plugins on any `vagrant plugin` command, so that errors are avoided. [GH-1418] - An error will be shown if you forward a port to the same host port multiple times. - Automatically convert network, provider, and provisioner names to symbols internally in case people define them as strings. - Using newer versions of net-ssh and net-scp. [GH-1436] BUG FIXES: - Quote keys to StringBlockEditor so keys with spaces, parens, and so on work properly. - When there is no route to host for SSH, re-establish a new connection. - `vagrant package` once again works, no more nil error. [GH-1423] - Human friendly error when "metadata.json" is missing in a box. - Don't use the full path to the manifest file with the Puppet provisioner because it exposes a bug with Puppet path lookup on VMware. - Fix bug in VirtualBox provider where port forwarding just didn't work if you attempted to forward to a port under 1024. [GH-1421] - Fix cross-device box adds for Windows. [GH-1424] - Fix minor issues with defaults of configuration of the shell provisioner. - Fix Puppet server using "host_name" instead of "hostname" [GH-1444] - Raise a proper error if no hostonly network is found for NFS with VirtualBox. [GH-1437] ## 1.1.0 (March 14, 2013) BACKWARDS INCOMPATIBILITIES: - Vagrantfiles from 1.0.x that _do not use_ any plugins are fully backwards compatible. If plugins are used, they must be removed prior to upgrading. The new plugin system in place will avoid this issue in the future. - Lots of changes introduced in the form of a new configuration version and format, but this is _opt-in_. Old Vagrantfile format continues to be supported, as promised. To use the new features that will be introduced throughout the 1.x series, you'll have to upgrade at some point. - The .vagrant file is no longer supported and has been replaced by a .vagrant directory. Running vagrant will automatically upgrade to the new style directory format, after which old versions of Vagrant will not be able to see or control your VM. FEATURES: - Groundwork for **providers**, alternate backends for Vagrant that allow Vagrant to power systems other than VirtualBox. Much improvement and change will come to this throughout the 1.x lifecycle. The API will continue to change, features will be added, and more. Specifically, a revamped system for handling shared folders gracefully across providers will be introduced in a future release. - New plugin system which adds much more structure and stability to the overall API. The goal of this system is to make it easier to write powerful plugins for Vagrant while providing a backwards-compatible API so that plugins will always _load_ (though they will almost certainly not be _functional_ in future versions of Vagrant). - Plugins are now installed and managed using the `vagrant plugin` interface. - Allow "file://" URLs for box URLs. [GH-1087] - Emit "vagrant-mount" upstart event when NFS shares are mounted. [GH-1118] - Add a VirtualBox provider config `auto_nat_dns_proxy` which when set to false will not attempt to automatically manage NAT DNS proxy settings with VirtualBox. [GH-1313] - `vagrant provision` accepts the `--provision-with` flag [GH-1167] - Set the name of VirtualBox machines with `virtualbox.name` in the VirtualBox provider config. [GH-1126] - `vagrant ssh` will execute an `ssh` binary on Windows if it is on your PATH. [GH-933] - The environmental variable `VAGRANT_VAGRANTFILE` can be used to specify an alternate Vagrantfile filename. IMPROVEMENTS / BUG FIXES: - Vagrant works much better in Cygwin environments on Windows by properly resolving Cygwin paths. [GH-1366] - Improve the SSH "ready?" check by more gracefully handling timeouts. [GH-841] - Human friendly error if connection times out for HTTP downloads. [GH-849] - Detect when the VirtualBox installation is incomplete and error. [GH-846] - Detect when kernel modules for VirtualBox need to be installed on Gentoo systems and report a user-friendly error. [GH-710] - All `vagrant` commands that can take a target VM name can take one even if you're not in a multi-VM environment. [GH-894] - Hostname is set before networks are setup to avoid very slow `sudo` speeds on CentOS. [GH-922] - `config.ssh.shell` now includes the flags to pass to it, such as `-l` [GH-917] - The check for whether a port is open or not is more complete by catching ENETUNREACH errors. [GH-948] - SSH uses LogLevel FATAL so that errors are still shown. - Sending a SIGINT (Ctrl-C) very early on when executing `vagrant` no longer results in an ugly stack trace. - Chef JSON configuration output is now pretty-printed to be human readable. [GH-1146] that SSHing succeeds when booting a machine. - VMs in the "guru meditation" state can be destroyed now using `vagrant destroy`. - Fix issue where changing SSH key permissions didn't properly work. [GH-911] - Fix issue where Vagrant didn't properly detect VBoxManage on Windows if VBOX_INSTALL_PATH contained multiple paths. [GH-885] - Fix typo in setting host name for Gentoo guests. [GH-931] - Files that are included with `vagrant package --include` now properly preserve file attributes on earlier versions of Ruby. [GH-951] - Multiple interfaces now work with Arch linux guests. [GH-957] - Fix issue where subprocess execution would always spin CPU of Ruby process to 100%. [GH-832] - Fix issue where shell provisioner would sometimes never end. [GH-968] - Fix issue where puppet would reorder module paths. [GH-964] - When console input is asked for (destroying a VM, bridged interfaces, etc.), keystrokes such as ctrl-D and ctrl-C are more gracefully handled. [GH-1017] - Fixed bug where port check would use "localhost" on systems where "localhost" is not available. [GH-1057] - Add missing translation for "saving" state on VirtualBox. [GH-1110] - Proper error message if the remote end unexpectedly resets the connection while downloading a box over HTTP. [GH-1090] - Human-friendly error is raised if there are permission issues when using SCP to upload files. [GH-924] - Box adding doesn't use `/tmp` anymore which can avoid some cross-device copy issues. [GH-1199] - Vagrant works properly in folders with strange characters. [GH-1223] - Vagrant properly handles "paused" VirtualBox machines. [GH-1184] - Better behavior around permissions issues when copying insecure private key. [GH-580] ## 1.0.7 (March 13, 2013) - Detect if a newer version of Vagrant ran and error if it did, because we're not forward-compatible. - Check for guest additions version AFTER booting. [GH-1179] - Quote IdentityFile in `ssh-config` so private keys with spaces in the path work. [GH-1322] - Fix issue where multiple Puppet module paths can be re-ordered [GH-964] - Shell provisioner won't hang on Windows anymore due to unclosed tempfile. [GH-1040] - Retry setting default VM name, since it sometimes fails first time. [GH-1368] - Support setting hostname on Suse [GH-1063] ## 1.0.6 (December 21, 2012) - Shell provisioner outputs proper line endings on Windows [GH-1164] - SSH upload opens file to stream which fixes strange upload issues. - Check for proper exit codes for Puppet, since multiple exit codes can mean success. [GH-1180] - Fix issue where DNS doesn't resolve properly for 12.10. [GH-1176] - Allow hostname to be a substring of the box name for Ubuntu [GH-1163] - Use `puppet agent` instead of `puppetd` to be Puppet 3.x compatible. [GH-1169] - Work around bug in VirtualBox exposed by bug in OS X 10.8 where VirtualBox executables couldn't handle garbage being injected into stdout by OS X. ## 1.0.5 (September 18, 2012) - Work around a critical bug in VirtualBox 4.2.0 on Windows that causes Vagrant to not work. [GH-1130] - Plugin loading works better on Windows by using the proper file path separator. - NFS works on Fedora 16+. [GH-1140] - NFS works with newer versions of Arch hosts that use systemd. [GH-1142] ## 1.0.4 (September 13, 2012) - VirtualBox 4.2 driver. [GH-1120] - Correct `ssh-config` help to use `--host`, not `-h`. - Use "127.0.0.1" instead of "localhost" for port checking to fix problem where "localhost" is not properly setup. [GH-1057] - Disable read timeout on Net::HTTP to avoid `rbuf_fill` error. [GH-1072] - Retry SSH on `EHOSTUNREACH` errors. - Add missing translation for "saving" state. [GH-1110] ## 1.0.3 (May 1, 2012) - Don't enable NAT DNS proxy on machines where resolv.conf already points to localhost. This allows Vagrant to work once again with Ubuntu 12.04. [GH-909] ## 1.0.2 (March 25, 2012) - Provisioners will still mount folders and such if `--no-provision` is used, so that `vagrant provision` works. [GH-803] - Nicer error message if an unsupported SSH key type is used. [GH-805] - Gentoo guests can now have their host names changed. [GH-796] - Relative paths can be used for the `config.ssh.private_key_path` setting. [GH-808] - `vagrant ssh` now works on Solaris, where `IdentitiesOnly` was not an available option. [GH-820] - Output works properly in the face of broken pipes. [GH-819] - Enable Host IO Cache on the SATA controller by default. - Chef-solo provisioner now supports encrypted data bags. [GH-816] - Enable the NAT DNS proxy by default, allowing your DNS to continue working when you switch networks. [GH-834] - Checking for port forwarding collisions also checks for other applications that are potentially listening on that port as well. [GH-821] - Multiple VM names can be specified for the various commands now. For example: `vagrant up web db service`. [GH-795] - More robust error handling if a VM fails to boot. The error message is much clearer now. [GH-825] ## 1.0.1 (March 11, 2012) - Installers are now bundled with Ruby 1.9.3p125. Previously they were bundled with 1.9.3p0. This actually fixes some IO issues with Windows. - Windows installer now outputs a `vagrant` binary that will work in msys or Cygwin environments. - Fix crashing issue which manifested itself in multi-VM environments. - Add missing `rubygems` require in `environment.rb` to avoid possible load errors. [GH-781] - `vagrant destroy` shows a nice error when called without a TTY (and hence can't confirm). [GH-779] - Fix an issue with the `:vagrantfile_name` option to `Vagrant::Environment` not working properly. [GH-778] - `VAGRANT_CWD` environmental variable can be used to set the CWD to something other than the current directory. - Downloading boxes from servers that don't send a content-length now works properly. [GH-788] - The `:facter` option now works for puppet server. [GH-790] - The `--no-provision` and `--provision-with` flags are available to `vagrant reload` now. - `:openbsd` guest which supports only halting at the moment. [GH-773] - `ssh-config -h` now shows help, instead of assuming a host is being specified. For host, you can still use `--host`. [GH-793] ## 1.0.0 (March 6, 2012) - `vagrant gem` should now be used to install Vagrant plugins that are gems. This installs the gems to a private gem folder that Vagrant adds to its own load path. This isolates Vagrant-related gems from system gems. - Plugin loading no longer happens right when Vagrant is loaded, but when a Vagrant environment is loaded. I don't anticipate this causing any problems but it is a backwards incompatible change should a plugin depend on this (but I don't see any reason why they would). - `vagrant destroy` now asks for confirmation by default. This can be overridden with the `--force` flag. [GH-699] - Fix issue with Puppet config inheritance. [GH-722] - Fix issue where starting a VM on some systems was incorrectly treated as failing. [GH-720] - It is now an error to specify the packaging `output` as a directory. [GH-730] - Unix-style line endings are used properly for guest OS. [GH-727] - Retry certain VirtualBox operations, since they intermittently fail. [GH-726] - Fix issue where Vagrant would sometimes "lose" a VM if an exception occurred. [GH-725] - `vagrant destroy` destroys virtual machines in reverse order. [GH-739] - Add an `fsid` option to Linux NFS exports. [GH-736] - Fix edge case where an exception could be raised in networking code. [GH-742] - Add missing translation for the "guru meditation" state. [GH-745] - Check that VirtualBox exists before certain commands. [GH-746] - NIC type can be defined for host-only network adapters. [GH-750] - Fix issue where re-running chef-client would sometimes cause problems due to file permissions. [GH-748] - FreeBSD guests can now have their hostnames changed. [GH-757] - FreeBSD guests now support host only networking and bridged networking. [GH-762] - `VM#run_action` is now public so plugin-devs can hook into it. - Fix crashing bug when attempting to run commands on the "primary" VM in a multi-VM environment. [GH-761] - With puppet you can now specify `:facter` as a dictionary of facts to override what is generated by Puppet. [GH-753] - Automatically convert all arguments to `customize` to strings. - openSUSE host system. [GH-766] - Fix subprocess IO deadlock which would occur on Windows. [GH-765] - Fedora 16 guest support. [GH-772] ## 0.9.7 (February 9, 2012) - Fix regression where all subprocess IO simply didn't work with Windows. [GH-721] ## 0.9.6 (February 7, 2012) - Fix strange issue with inconsistent childprocess reads on JRuby. [GH-711] - `vagrant ssh` does a direct `exec()` syscall now instead of going through the shell. This makes it so things like shell expansion oddities no longer cause problems. [GH-715] - Fix crashing case if there are no ports to forward. - Fix issue surrounding improper configuration of host only networks on RedHat guests. [GH-719] - NFS should work properly on Gentoo. [GH-706] ## 0.9.5 (February 5, 2012) - Fix crashing case when all network options are `:auto_config false`. [GH-689] - Type of network adapter can be specified with `:nic_type`. [GH-690] - The NFS version can be specified with the `:nfs_version` option on shared folders. [GH-557] - Greatly improved FreeBSD guest and host support. [GH-695] - Fix instability with RedHat guests and host only and bridged networks. [GH-698] - When using bridged networking, only list the network interfaces that are up as choices. [GH-701] - More intelligent handling of the `certname` option for puppet server. [GH-702] - You may now explicitly set the network to bridge to in the Vagrantfile using the `:bridge` parameter. [GH-655] ## 0.9.4 (January 28, 2012) - Important internal changes to middlewares that make plugin developer's lives much easier. [GH-684] - Match VM names that have parens, brackets, etc. - Detect when the VirtualBox kernel module is not loaded and error. [GH-677] - Set `:auto_config` to false on any networking option to not automatically configure it on the guest. [GH-663] - NFS shared folder guest paths can now contain shell expansion characters such as `~`. - NFS shared folders with a `:create` flag will have their host folders properly created if they don't exist. [GH-667] - Fix the precedence for Arch, Ubuntu, and FreeBSD host classes so they are properly detected. [GH-683] - Fix issue where VM import sometimes made strange VirtualBox folder layouts. [GH-669] - Call proper `id` command on Solaris. [GH-679] - More accurate VBoxManage error detection. - Shared folders can now be marked as transient using the `:transient` flag. [GH-688] ## 0.9.3 (January 24, 2012) - Proper error handling for not enough arguments to `box` commands. - Fix issue causing crashes with bridged networking. [GH-673] - Ignore host only network interfaces that are "down." [GH-675] - Use "printf" instead of "echo" to determine shell expanded files paths which is more generally POSIX compliant. [GH-676] ## 0.9.2 (January 20, 2012) - Support shell expansions in shared folder guest paths again. [GH-656] - Fix issue where Chef solo always expected the host to have a "cookbooks" folder in their directory. [GH-638] - Fix `forward_agent` not working when outside of blocks. [GH-651] - Fix issue causing custom guest implementations to not load properly. - Filter clear screen character out of output on SSH. - Log output now goes on `stderr`, since it is utility information. - Get rid of case where a `NoMethodError` could be raised while determining VirtualBox version. [GH-658] - Debian/Ubuntu uses `ifdown` again, instead of `ifconfig xxx down`, since the behavior seems different/wrong. - Give a nice error if `:vagrant` is used as a JSON key, since Vagrant uses this. [GH-661] - If there is only one bridgeable interface, use that without asking the user. [GH-655] - The shell will have color output if ANSICON is installed on Windows. [GH-666] ## 0.9.1 (January 18, 2012) - Use `ifconfig device down` instead of `ifdown`. [GH-649] - Clearer invalid log level error. [GH-645] - Fix exception raised with NFS `recover` method. - Fix `ui` `NoMethodError` exception in puppet server. - Fix `vagrant box help` on Ruby 1.8.7. [GH-647] ## 0.9.0 (January 17, 2012) - VirtualBox 4.0 support backported in addition to supporting VirtualBox 4.1. - `config.vm.network` syntax changed so that the first argument is now the type of argument. Previously where you had `config.vm.network "33.33.33.10"` you should now put `config.vm.network :hostonly, "33.33.33.10"`. This is in order to support bridged networking, as well. - `config.vm.forward_port` no longer requires a name parameter. - Bridged networking. `config.vm.network` with `:bridged` as the option will setup a bridged network. - Host only networks can be configured with DHCP now. Specify `:dhcp` as the IP and it will be done. - `config.vm.customize` now takes a command to send to `VBoxManage`, so any arbitrary command can be sent. The older style of passing a block no longer works and Vagrant will give a proper error message if it notices this old-style being used. - `config.ssh.forwarded_port_key` is gone. Vagrant no longer cares about forwarded port names for any reason. Please use `config.ssh.guest_port` (more below). - `config.ssh.forwarded_port_destination` has been replaced by `config.ssh.guest_port` which more accurately reflects what it is used for. Vagrant will automatically scan forwarded ports that match the guest port to find the SSH port. - Logging. The entire Vagrant source has had logging sprinkled throughout to make debugging issues easier. To enable logging, set the VAGRANT_LOG environmental variable to the log level you wish to see. By default, logging is silent. - `system` renamed to `guest` throughout the source. Any `config.vm.system` configurations must be changed to `config.vm.guest` - Puppet provisioner no longer defaults manifest to "box.pp." Instead, it is now "default.pp" - All Vagrant commands that take a VM name in a Multi-VM environment can now be given a regular expression. If the name starts and ends with a "/" then it is assumed to be a regular expression. [GH-573] - Added a "--plain" flag to `vagrant ssh` which will cause Vagrant to not perform any authentication. It will simply `ssh` into the proper IP and port of the virtual machine. - If a shared folder now has a `:create` flag set to `true`, the path on the host will be created if it doesn't exist. - Added `--force` flag to `box add`, which will overwrite any existing boxes if they exist. [GH-631] - Added `--provision-with` to `up` which configures what provisioners run, by shortcut. [GH-367] - Arbitrary mount options can be passed with `:extra` to any shared folders. [GH-551] - Options passed after a `--` to `vagrant ssh` are now passed directly to `ssh`. [GH-554] - Ubuntu guests will now emit a `vagrant-mounted` upstart event after shared folders are mounted. - `attempts` is a new option on chef client and chef solo provisioners. This will run the provisioner multiple times until erroring about failing convergence. [GH-282] - Removed Thor as a dependency for the command line interfaces. This resulted in general speed increases across all command line commands. - Linux uses `shutdown -h` instead of `halt` to hopefully more consistently power off the system. [GH-575] - Tweaks to SSH to hopefully be more reliable in coming up. - Helpful error message when SCP is unavailable in the guest. [GH-568] - Error message for improperly packaged box files. [GH-198] - Copy insecure private key to user-owned directory so even `sudo` installed Vagrant installations work. [GH-580] - Provisioner stdout/stderr is now color coded based on stdout/stderr. stdout is green, stderr is red. [GH-595] - Chef solo now prompts users to run a `reload` if shared folders are not found on the VM. [GH-253] - "--no-provision" once again works for certain commands. [GH-591] - Resuming a VM from a saved state will show an error message if there would be port collisions. [GH-602] - `vagrant ssh -c` will now exit with the same exit code as the command run. [GH-598] - `vagrant ssh -c` will now send stderr to stderr and stdout to stdout on the host machine, instead of all output to stdout. - `vagrant box add` path now accepts unexpanded shell paths such as `~/foo` and will properly expand them. [GH-633] - Vagrant can now be interrupted during the "importing" step. - NFS exports will no longer be cleared when an expected error occurs. [GH-577] ## 0.8.10 (December 10, 2011) - Revert the SSH tweaks made in 0.8.8. It affected stability ## 0.8.8 (December 1, 2011) - Mount shared folders shortest to longest to avoid mounting subfolders first. [GH-525] - Support for basic HTTP auth in the URL for boxes. - Solaris support for host only networks. [GH-533] - `vagrant init` respects `Vagrant::Environment` cwd. [GH-528] - `vagrant` commands will not output color when stdout is not a TTY. - Fix issue where `box_url` set with multiple VMs could cause issues. [GH-564] - Chef provisioners no longer depend on a "v-root" share being available. [GH-556] - NFS should work for FreeBSD hosts now. [GH-510] - SSH executed methods respect `config.ssh.max_tries`. [GH-508] - `vagrant box add` now respects the "no_proxy" environmental variable. [GH-502] - Tweaks that should make "Waiting for VM to boot" slightly more reliable. - Add comments to Vagrantfile to make it detected as Ruby file for `vi` and `emacs`. [GH-515] - More correct guest addition version checking. [GH-514] - Chef solo support on Windows is improved. [GH-542] - Put encrypted data bag secret into `/tmp` by default so that permissions are almost certainly guaranteed. [GH-512] ## 0.8.7 (September 13, 2011) - Fix regression with remote paths from chef-solo. [GH-431] - Fix issue where Vagrant crashes if `.vagrant` file becomes invalid. [GH-496] - Issue a warning instead of an error for attempting to forward a port <= 1024. [GH-487] ## 0.8.6 (August 28, 2011) - Fix issue with download progress not properly clearing the line. [GH-476] - NFS should work properly on Fedora. [GH-450] - Arguments can be specified to the `shell` provisioner via the `args` option. [GH-475] - Vagrant behaves much better when there are "inaccessible" VMs. [GH-453] ## 0.8.5 (August 15, 2011) Note: 0.8.3 and 0.8.4 was yanked due to RubyGems encoding issue. - Fix SSH `exec!` to inherit proper `$PATH`. [GH-426] - Chef client now accepts an empty (`nil`) run list again. [GH-429] - Fix incorrect error message when running `provision` on halted VM. [GH-447] - Checking guest addition versions now ignores OSE. [GH-438] - Chef solo from a remote URL fixed. [GH-431] - Arch linux support: host only networks and changing the host name. [GH-439] [GH-448] - Chef solo `roles_path` and `data_bags_path` can only be single paths. [GH-446] - Fix `virtualbox_not_detected` error message to require 4.1.x. [GH-458] - Add shortname (`hostname -s`) for hostname setting on RHEL systems. [GH-456] - `vagrant ssh -c` output no longer has a prefix and respects newlines from the output. [GH-462] ## 0.8.2 (July 22, 2011) - Fix issue with SSH disconnects not reconnecting. - Fix chef solo simply not working with roles/data bags. [GH-425] - Multiple chef solo provisioners now work together. - Update Puppet provisioner so no deprecation warning is shown. [GH-421] - Removed error on "provisioner=" in config, as this has not existed for some time now. - Add better validation for networking. ## 0.8.1 (July 20, 2011) - Repush of 0.8.0 to fix a Ruby 1.9.2 RubyGems issue. ## 0.8.0 (July 20, 2011) - VirtualBox 4.1 support _only_. Previous versions of VirtualBox are supported by earlier versions of Vagrant. - Performance optimizations in `virtualbox` gem. Huge speed gains. - `:chef_server` provisioner is now `:chef_client`. [GH-359] - SSH connection is now cached after first access internally, speeding up `vagrant up`, `reload`, etc. quite a bit. - Actions which modify the VM now occur much more quickly, greatly speeding up `vagrant up`, `reload`, etc. - SUSE host only networking support. [GH-369] - Show nice error message for invalid HTTP responses for HTTP downloader. [GH-403] - New `:inline` option for shell provisioner to provide inline scripts as a string. [GH-395] - Host only network now properly works on multiple adapters. [GH-365] - Can now specify owner/group for regular shared folders. [GH-350] - `ssh_config` host name will use VM name if given. [GH-332] - `ssh` `-e` flag changed to `-c` to align with `ssh` standard behavior. [GH-323] - Forward agent and forward X11 settings properly appear in `ssh_config` output. [GH-105] - Chef JSON can now be set with `chef.json =` instead of the old `merge` technique. [GH-314] - Provisioner configuration is no longer cleared when the box needs to be downloaded during an `up`. [GH-308] - Multiple Chef provisioners no longer overwrite cookbook folders. [GH-407] - `package` won't delete previously existing file. [GH-408] - Vagrantfile can be lowercase now. [GH-399] - Only one copy of Vagrant may be running at any given time. [GH-364] - Default home directory for Vagrant moved to `~/.vagrant.d` [GH-333] - Specify a `forwarded_port_destination` for SSH configuration and SSH port searching will fall back to that if it can't find any other port. [GH-375] ## 0.7.8 (July 19, 2011) - Make sure VirtualBox version check verifies that it is 4.0.x. ## 0.7.7 (July 12, 2011) - Fix crashing bug with Psych and Ruby 1.9.2. [GH-411] ## 0.7.6 (July 2, 2011) - Run Chef commands in a single command. [GH-390] - Add `nfs` option for Chef to mount Chef folders via NFS. [GH-378] - Add translation for `aborted` state in VM. [GH-371] - Use full paths with the Chef provisioner so that restart cookbook will work. [GH-374] - Add "--no-color" as an argument and no colorized output will be used. [GH-379] - Added DEVICE option to the RedHat host only networking entry, which allows host only networking to work even if the VM has multiple NICs. [GH-382] - Touch the network configuration file for RedHat so that the `sed` works with host only networking. [GH-381] - Load prerelease versions of plugins if available. - Do not load a plugin if it depends on an invalid version of Vagrant. - Encrypted data bag support in Chef server provisioner. [GH-398] - Use the `-H` flag to set the proper home directory for `sudo`. [GH-370] ## 0.7.5 (May 16, 2011) - `config.ssh.port` can be specified and takes highest precedence if specified. Otherwise, Vagrant will still attempt to auto-detect the port. [GH-363] - Get rid of RubyGems deprecations introduced with RubyGems 1.8.x - Search in pre-release gems for plugins as well as release gems. - Support for Chef-solo `data_bags_path` [GH-362] - Can specify path to Chef binary using `binary_path` [GH-342] - Can specify additional environment data for Chef using `binary_env` [GH-342] ## 0.7.4 (May 12, 2011) - Chef environments support (for Chef 0.10) [GH-358] - Suppress the "added to known hosts" message for SSH [GH-354] - Ruby 1.8.6 support [GH-352] - Chef proxy settings now work for chef server [GH-335] ## 0.7.3 (April 19, 2011) - Retry all SSH on Net::SSH::Disconnect in case SSH is just restarting. [GH-313] - Add NFS shared folder support for Arch linux. [GH-346] - Fix issue with unknown terminal type output for sudo commands. - Forwarded port protocol can now be set as UDP. [GH-311] - Chef server file cache path and file backup path can be configured. [GH-310] - Setting hostname should work on Debian now. [GH-307] ## 0.7.2 (February 8, 2011) - Update JSON dependency to 1.5.1, which works with Ruby 1.9 on Windows. - Fix sudo issues on sudo < 1.7.0 (again). - Fix race condition in SSH, which specifically manifested itself in the chef server provisioner. [GH-295] - Change sudo shell to use `bash` (configurable). [GH-301] - Can now set mac address of host only network. [GH-294] - NFS shared folders with spaces now work properly. [GH-293] - Failed SSH commands now show output in error message. [GH-285] ## 0.7.1 (January 28, 2011) - Change error output with references to VirtualBox 3.2 to 4.0. - Internal SSH through net-ssh now uses `IdentitiesOnly` thanks to upstream net-ssh fix. - Fix issue causing warnings to show with `forwardx11` enabled for SSH. [GH-279] - FreeBSD support for host only networks, NFS, halting, etc. [GH-275] - Make SSH commands which use sudo compatible with sudo < 1.7.0. [GH-278] - Fix broken puppet server provisioner which called a nonexistent method. - Default SSH host changed from `localhost` to `127.0.0.1` since `localhost` is not always loopback. - New `shell` provisioner which simply uploads and executes a script as root on the VM. - Gentoo host only networking no longer fails if already setup. [GH-286] - Set the host name of your guest OS with `config.vm.host_name` [GH-273] - `vagrant ssh-config` now outputs the configured `config.ssh.host` ## 0.7.0 (January 19, 2011) - VirtualBox 4.0 support. Support for VirtualBox 3.2 is _dropped_, since the API is so different. Stay with the 0.6.x series if you have VirtualBox 3.2.x. - Puppet server provisioner. [GH-262] - Use numeric uid/gid in mounting shared folders to increase portability. [GH-252] - HTTP downloading follows redirects. [GH-163] - Downloaders have clearer output to note what they're doing. - Shared folders with no guest path are not automounted. [GH-184] - Boxes downloaded during `vagrant up` reload the Vagrantfile config, which fixes a problem with box settings not being properly loaded. [GH-231] - `config.ssh.forward_x11` to enable the ForwardX11 SSH option. [GH-255] - Vagrant source now has a `contrib` directory where contributions of miscellaneous addons for Vagrant will be added. - Vagrantfiles are now loaded only once (instead of 4+ times) [GH-238] - Ability to move home vagrant dir (~/.vagrant) by setting VAGRANT_HOME environmental variable. - Removed check and error for the "OSE" version of VirtualBox, since with VirtualBox 4 this distinction no longer exists. - Ability to specify proxy settings for chef. [GH-169] - Helpful error message shown if NFS mounting fails. [GH-135] - Gentoo guests now support host only networks. [GH-240] - RedHat (CentOS included) guests now support host only networks. [GH-260] - New Vagrantfile syntax for enabling and configuring provisioners. This change is not backwards compatible. [GH-265] - Provisioners are now RVM-friendly, meaning if you installed chef or puppet with an RVM managed Ruby, Vagrant now finds then. [GH-254] - Changed the unused host only network destroy mechanism to check for uselessness after the VM is destroyed. This should result in more accurate checks. - Networks are no longer disabled upon halt/destroy. With the above change, its unnecessary. - Puppet supports `module_path` configuration to mount local modules directory as a shared folder and configure puppet with it. [GH-270] - `ssh-config` now outputs `127.0.0.1` as the host instead of `localhost`. ## 0.6.9 (December 21, 2010) - Puppet provisioner. [GH-223] - Solaris system configurable to use `sudo`. - Solaris system registered, so it can be set with `:solaris`. - `vagrant package` include can be a directory name, which will cause the contents to be recursively copied into the package. [GH-241] - Arbitrary options to puppet binary can be set with `config.puppet.options`. [GH-242] - BSD hosts use proper GNU sed syntax for clearing NFS shares. [GH-243] - Enumerate VMs in a multi-VM environment in order they were defined. [GH-244] - Check for VM boot changed to use `timeout` library, which works better with Windows. - Show special error if VirtualBox not detected on 64-bit Windows. - Show error to Windows users attempting to use host only networking since it doesn't work yet. ## 0.6.8 (November 30, 2010) - Network interfaces are now up/down in distinct commands instead of just restarting "networking." [GH-192] - Add missing translation for chef binary missing. [GH-203] - Fix default settings for Opscode platform and comments. [GH-213] - Blank client name for chef server now uses FQDN by default, instead of "client" [GH-214] - Run list can now be nil, which will cause it to sync with chef server (when chef server is enabled). [GH-214] - Multiple NFS folders now work on linux. [GH-215] - Add translation for state "stuck" which is very rare. [GH-218] - virtualbox gem dependency minimum raised to 0.7.6 to verify FFI < 1.0.0 is used. - Fix issue where box downloading from `vagrant up` didn't reload the box collection. [GH-229] ## 0.6.7 (November 3, 2010) - Added validation to verify that a box is specified. - Proper error message when box is not found for `config.vm.box`. [GH-195] - Fix output of `vagrant status` with multi-vm to be correct. [GH-196] ## 0.6.6 (October 14, 2010) - `vagrant status NAME` works once again. [GH-191] - Conditional validation of Vagrantfile so that some commands don't validate. [GH-188] - Fix "junk" output for ssh-config. [GH-189] - Fix port collision handling with greater than two VMs. [GH-185] - Fix potential infinite loop with root path if bad CWD is given to environment. ## 0.6.5 (October 8, 2010) - Validations on base MAC address to avoid situation described in GH-166, GH-181 from ever happening again. - Properly load sub-VM configuration on first-pass of config loading. Solves a LOT of problems with multi-VM. [GH-166] [GH-181] - Configuration now only validates on final Vagrantfile proc, so multi-VM validates correctly. - A nice error message is given if ".vagrant" is a directory and therefore can't be accessed. [GH-172] - Fix plugin loading in a Rails 2.3.x project. [GH-176] ## 0.6.4 (October 4, 2010) - Default VM name is now properly the parent folder of the working directory of the environment. - Added method to `TestHelpers` to assist with testing new downloaders. - `up --no-provision` works again. This disables provisioning during the boot process. - Action warden doesn't do recovery process on `SystemExit` exceptions, allowing the double ctrl-C to work properly again. [related to GH-166] - Initial Vagrantfile is now heavily commented with various available options. [GH-171] - Box add checks if a box already exists before the download. [GH-170] - NFS no longer attempts to clean exports file if VM is not created, which was causing a stack trace during recovery. [related to GH-166] - Basic validation added for Chef configuration (both solo and server). - Top config class is now available in all `Vagrant::Config::Base` subclasses, which is useful for config validation. - Subcommand help shows proper full command in task listing. [GH-168] - SSH gives error message if `ssh` binary is not found. [GH-161] - SSH gives proper error message if VM is not running. [GH-167] - Fix some issues with undefined constants in command errors. ## 0.6.1, 0.6.2, 0.6.3 (September 27, 2010) A lot of quick releases which all were to fix issues with Ruby 1.8.7 compatibility. ## 0.6.0 (September 27, 2010) - VM name now defaults to the name of the containing folder, plus a timestamp. This should make it easier to identify VMs in the VirtualBox GUI. - Exposed Vagrant test helpers in `Vagrant::TestHelpers` for plugins to easily test themselves against Vagrant environments. - **Plugins** have landed. Plugins are simply gems which have a `vagrant_init.rb` file somewhere in their load path. Please read the documentation on vagrantup.com before attempting to create a plugin (which is very easy) for more information on how it all works and also some guidelines. - `vagrant package` now takes a `--vagrantfile` option to specify a Vagrantfile to package. The `--include` approach for including a Vagrantfile no longer works (previously built boxes will continue to work). - `vagrant package` has new logic with regards to the `--include` option depending on if the file path is relative or absolute (they can be intermixed): * _Relative_ paths are copied directly into the box, preserving their path. So `--include lib/foo` would be in the box as "lib/foo" * _Absolute_ paths are simply copied files into the root of the box. So `--include /lib/foo` would be in the box as "foo" - "vagrant_main" is no longer the default run list. Instead, chef run list starts empty. It is up to you to specify all recipes in the Vagrantfile now. - Fixed various issues with certain action middleware not working if the VM was not created. - SSH connection is retried 5 times if there is a connection refused. Related to GH-140. - If `http_proxy` environmental variable is set, it will be used as the proxy box adding via http. - Remove `config.ssh.password`. It hasn't been used for a few versions now and was only kept around to avoid exceptions in Vagrantfiles. - Configuration is now validated so improper input can be found in Vagrantfiles. - Fixed issue with not detecting Vagrantfile at root directory ("/"). - Vagrant now gives a nice error message if there is a syntax error in any Vagrantfile. [GH-154] - The format of the ".vagrant" file which stores persisted VMs has changed. This is **backwards incompatible**. Will provide an upgrade utility prior to 0.6 launch. - Every [expected] Vagrant error now exits with a clean error message and a unique exit status, and raises a unique exception (if you're scripting Vagrant). - Added I18n gem dependency for pulling strings into clean YML files. Vagrant is now localizable as a side effect! Translations welcome. - Fixed issue with "Waiting for cleanup" message appearing twice in some cases. [GH-145] - Converted CLI to use Thor. As a tradeoff, there are some backwards incompatibilities: * `vagrant package` - The `--include` flag now separates filenames by spaces, instead of by commas. e.g. `vagrant package --include x y z` * `vagrant ssh` - If you specify a command to execute using the `--execute` flag, you may now only specify one command (before you were able to specify an arbitrary amount). e.g. `vagrant ssh -e "echo hello"` * `vagrant ssh-config` has become `vagrant ssh_config` due to a limitation in Thor. ## 0.5.4 (September 7, 2010) - Fix issue with the "exec failed" by running on Tiger as well. - Give an error when downloading a box which already exists prior to actually downloading the box. ## 0.5.3 (August 23, 2010) - Add erubis as a dependency since its rendering of `erb` is sane. - Fixed poorly formatted Vagrantfile after `vagrant init`. [GH-142] - Fixed NFS not working properly with multiple NFS folders. - Fixed chef solo provision to work on Windows. It was expanding a linux path which prepended a drive letter onto it. ## 0.5.2 (August 3, 2010) - `vagrant up` can be used as a way to resume the VM as well (same as `vagrant resume`). [GH-134] - Sudo uses "-E" flag to preserve environment for chef provisioners. This fixes issues with CentOS. [GH-133] - Added "IdentitiesOnly yes" to options when `vagrant ssh` is run to avoid "Too Many Authentication Failures" error. [GH-131] - Fix regression with `package` not working. [GH-132] - Added ability to specify box url in `init`, which populates the Vagrantfile with the proper `config.vm.box_url`. ## 0.5.1 (July 31, 2010) - Allow specifying cookbook paths which exist only on the VM in `config.chef.cookbooks_path`. This is used for specifying cookbook paths when `config.chef.recipe_url` is used. [GH-130] See updated chef solo documentation for more information on this. - No longer show "Disabling host only networks..." if no host only networks are destroyed. Quiets `destroy`, `halt`, etc output a bit. - Updated getting started guide to be more up to date and generic. [GH-125] - Fixed error with doing a `vagrant up` when no Vagrantfile existed. [GH-128] - Fixed NFS erroring when NFS wasn't even enabled if `/etc/exports` doesn't exist. [GH-126] - Fixed `vagrant resume` to properly resume a suspended VM. [GH-122] - Fixed `halt`, `destroy`, `reload` to where they failed if the VM was in a saved state. [GH-123] - Added `config.chef.recipe_url` which allows you to specify a URL to a gzipped tar file for chef solo to download cookbooks. See the [chef-solo docs](https://docs.chef.io/chef_solo.html) for more information. [GH-121] - Added `vagrant box repackage` which repackages boxes which have been added. This is useful in case you want to redistribute a base box you have but may have lost the actual "box" file. [GH-120] ## Previous The changelog began with version 0.5.1 so any changes prior to that can be seen by checking the tagged releases and reading git commit messages. ================================================ FILE: Gemfile ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 source "https://rubygems.org" gemspec if File.exist?(File.expand_path("../../vagrant-spec", __FILE__)) gem 'vagrant-spec', path: "../vagrant-spec" else gem 'vagrant-spec', git: "https://github.com/hashicorp/vagrant-spec.git", branch: :main end ================================================ FILE: LICENSE ================================================ License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of MariaDB Corporation Ab. Parameters Licensor: International Business Machines Corporation (IBM). Licensed Work: Vagrant 2.4.3 or later. The Licensed Work is (c) 2024 IBM Corp. Additional Use Grant: You may make production use of the Licensed Work, provided Your use does not include offering the Licensed Work to third parties on a hosted or embedded basis in order to compete with IBM Corp’s paid version(s) of the Licensed Work. For purposes of this license: A "competitive offering" is a Product that is offered to third parties on a paid basis, including through paid support arrangements, that significantly overlaps with the capabilities of IBM Corp's paid version(s) of the Licensed Work. If Your Product is not a competitive offering when You first make it generally available, it will not become a competitive offering later due to IBM Corp releasing a new version of the Licensed Work with additional capabilities. In addition, Products that are not provided on a paid basis are not competitive. "Product" means software that is offered to end users to manage in their own environments or offered as a service on a hosted basis. "Embedded" means including the source code or executable code from the Licensed Work in a competitive offering. "Embedded" also means packaging the competitive offering in such a way that the Licensed Work must be accessed or downloaded for the competitive offering to operate. Hosting or using the Licensed Work(s) for internal purposes within an organization is not considered a competitive offering. IBM Corp considers your organization to include all of your affiliates under common control. For binding interpretive guidance on using IBM Corp products under the Business Source License, please visit our FAQ. (https://www.hashicorp.com/license-faq) Change Date: Four years from the date the Licensed Work is published. Change License: MPL 2.0 For information about alternative licensing arrangements for the Licensed Work, please contact licensing@hashicorp.com. Notice Business Source License 1.1 Terms The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate. If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work. All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor. You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work. Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work. This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License). TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. ================================================ FILE: README.md ================================================ # Vagrant - Website: [https://www.vagrantup.com/](https://www.vagrantup.com/) - Source: [https://github.com/hashicorp/vagrant](https://github.com/hashicorp/vagrant) - HashiCorp Discuss: [https://discuss.hashicorp.com/c/vagrant/24](https://discuss.hashicorp.com/c/vagrant/24) Vagrant is a tool for building and distributing development environments. Development environments managed by Vagrant can run on local virtualized platforms such as VirtualBox or VMware, in the cloud via AWS or OpenStack, or in containers such as with Docker or raw LXC. Vagrant provides the framework and configuration format to create and manage complete portable development environments. These development environments can live on your computer or in the cloud, and are portable between Windows, Mac OS X, and Linux. ## Quick Start Package dependencies: Vagrant requires `bsdtar` and `curl` to be available on your system PATH to run successfully. For the quick-start, we'll bring up a development machine on [VirtualBox](https://www.virtualbox.org/) because it is free and works on all major platforms. Vagrant can, however, work with almost any system such as [OpenStack](https://www.openstack.org/), [VMware](https://www.vmware.com/), [Docker](https://docs.docker.com/), etc. First, make sure your development machine has [VirtualBox](https://www.virtualbox.org/) installed. After this, [download and install the appropriate Vagrant package for your OS](https://www.vagrantup.com/downloads.html). To build your first virtual environment: vagrant init hashicorp/bionic64 vagrant up Note: The above `vagrant up` command will also trigger Vagrant to download the `bionic64` box via the specified URL. Vagrant only does this if it detects that the box doesn't already exist on your system. ## Getting Started Guide To learn how to build a fully functional development environment, follow the [getting started guide](https://www.vagrantup.com/docs/getting-started). ## Installing from Source If you want the bleeding edge version of Vagrant, we try to keep main pretty stable and you're welcome to give it a shot. Please review the installation page [here](https://www.vagrantup.com/docs/installation/source). ## Contributing to Vagrant Please take time to read the [HashiCorp Community Guidelines](https://www.hashicorp.com/community-guidelines) and the [Vagrant Contributing Guide](https://github.com/hashicorp/vagrant/blob/main/.github/CONTRIBUTING.md). Then you're good to go! ================================================ FILE: RELEASE.md ================================================ # Releasing Vagrant This documents how to release Vagrant. Various steps in this document will require privileged access to private systems, so this document is only targeted at Vagrant core members who have the ability to cut a release. 1. Go to the [release initiator workflow](https://github.com/hashicorp/vagrant/actions/workflows/initiate-release.yml) 1. Trigger a new run with the version to be released (it should not include a `v` prefix, for example: `1.0.0`) 1. After release is complete, update [Checkpoint](https://checkpoint.hashicorp.com/control) ================================================ FILE: Rakefile ================================================ require 'rubygems' require 'bundler/setup' require "rake/extensiontask" # Immediately sync all stdout so that tools like buildbot can # immediately load in the output. $stdout.sync = true $stderr.sync = true Rake::ExtensionTask.new "vagrant/vagrant_ssl" do |ext| end # Load all the rake tasks from the "tasks" folder. This folder # allows us to nicely separate rake tasks into individual files # based on their role, which makes development and debugging easier # than one monolithic file. task_dir = File.expand_path("../tasks", __FILE__) Dir["#{task_dir}/**/*.rake"].each do |task_file| load task_file end task default: "test:unit" ================================================ FILE: Vagrantfile ================================================ # This Vagrantfile can be used to develop Vagrant. Note that VirtualBox # doesn't run in VirtualBox so you can't actually _run_ Vagrant within # the VM created by this Vagrantfile, but you can use it to develop the # Ruby, run unit tests, etc. Vagrant.configure("2") do |config| config.vm.box = "hashicorp/bionic64" config.vm.hostname = "vagrant" config.ssh.shell = "bash -c 'BASH_ENV=/etc/profile exec bash'" ["vmware_desktop", "virtualbox", "hyperv"].each do |provider| config.vm.provider provider do |v, override| v.memory = "2048" end end # We split apart `install_rvm` from `setup_tests` because rvm says to # logout and log back in just after installing RVM. # https://github.com/rvm/ubuntu_rvm#3-reboot config.vm.provision "shell", path: "scripts/install_rvm" config.vm.provision "shell", path: "scripts/setup_tests" config.push.define "www", strategy: "local-exec" do |push| push.script = "scripts/website_push_www.sh" end config.push.define "docs", strategy: "local-exec" do |push| push.script = "scripts/website_push_docs.sh" end end ================================================ FILE: bin/vagrant ================================================ #!/usr/bin/env ruby # Trap interrupts to quit cleanly. This will be overridden at some point # by Vagrant. This is made to catch any interrupts while Vagrant is # initializing which have historically resulted in stack traces. Signal.trap("INT") { abort } # Disable exception reporting by default if available if Thread.respond_to?(:report_on_exception=) Thread.report_on_exception = false end # Split arguments by "--" if it's there, we'll recombine them later argv = ARGV.dup argv_extra = [] # These will be the options that are passed to initialize the Vagrant # environment. opts = {} if idx = argv.index("--") argv_extra = argv.slice(idx+1, argv.length-2) argv = argv.slice(0, idx) end require_relative "../lib/vagrant/version" # Fast path the version of Vagrant if argv.include?("-v") || argv.include?("--version") puts "Vagrant #{Vagrant::VERSION}" exit 0 end # Disable plugin loading for commands where plugins are not required. This will # also disable loading of the Vagrantfile if it available as the environment # is not required for these commands argv.each_index do |i| arg = argv[i] if !arg.start_with?("-") if arg == "box" && argv[i+1] == "list" opts[:vagrantfile_name] = "" ENV['VAGRANT_NO_PLUGINS'] = "1" end # Do not load plugins when performing plugin operations if arg == "plugin" if argv.none?{|a| a == "--local" } && !ENV["VAGRANT_LOCAL_PLUGINS_LOAD"] opts[:vagrantfile_name] = "" end ENV['VAGRANT_NO_PLUGINS'] = "1" # Only initialize plugins when listing installed plugins if argv[i+1] != "list" ENV['VAGRANT_DISABLE_PLUGIN_INIT'] = "1" end end break end end # Set logging level to `debug`. This is done before loading 'vagrant', as it # sets up the logging system. if argv.include?("--debug") argv.delete("--debug") ENV["VAGRANT_LOG"] = "debug" end # Enable log timestamps if requested if argv.include?("--timestamp") argv.delete("--timestamp") ENV["VAGRANT_LOG_TIMESTAMP"] = "1" end # Convenience flag to enable debug with timestamps if argv.include?("--debug-timestamp") argv.delete("--debug-timestamp") ENV["VAGRANT_LOG"] = "debug" ENV["VAGRANT_LOG_TIMESTAMP"] = "1" end # Stdout/stderr should not buffer output $stdout.sync = true $stderr.sync = true # Before we start activate all our dependencies # so we can provide correct resolutions later builtin_specs = [] vagrant_spec = Gem::Specification.find_all_by_name("vagrant").detect do |spec| spec.version == Gem::Version.new(Vagrant::VERSION) end dep_activator = proc do |spec| spec.runtime_dependencies.each do |dep| gem(dep.name, *dep.requirement.as_list) dep_spec = Gem::Specification.find_all_by_name(dep.name).detect(&:activated?) if dep_spec builtin_specs << dep_spec dep_activator.call(dep_spec) end end end if vagrant_spec dep_activator.call(vagrant_spec) end env = nil begin require 'vagrant' require 'vagrant/bundler' require 'vagrant/cli' require 'vagrant/util/platform' require 'vagrant/util/experimental' # Set our list of builtin specs Vagrant::Bundler.instance.builtin_specs = builtin_specs # Schedule the cleanup of things at_exit(&Vagrant::Bundler.instance.method(:deinit)) # If this is not a pre-release disable verbose output if !Vagrant.prerelease? $VERBOSE = nil end # Add any option flags defined within this file here # so they are automatically propagated to all commands Vagrant.add_default_cli_options(proc { |o| o.on("--[no-]color", "Enable or disable color output") o.on("--machine-readable", "Enable machine readable output") o.on("-v", "--version", "Display Vagrant version") o.on("--debug", "Enable debug output") o.on("--timestamp", "Enable timestamps on log output") o.on("--debug-timestamp", "Enable debug output with timestamps") o.on("--no-tty", "Enable non-interactive output") }) # Create a logger right away logger = Log4r::Logger.new("vagrant::bin::vagrant") logger.info("`vagrant` invoked: #{ARGV.inspect}") # Disable color in a few cases: # # * --no-color is anywhere in our arguments # * STDOUT is not a TTY # * The terminal doesn't support colors (Windows) # if argv.include?("--no-color") || ENV["VAGRANT_NO_COLOR"] # Delete the argument from the list so that it doesn't # cause any invalid arguments down the road. argv.delete("--no-color") opts[:ui_class] = Vagrant::UI::Basic elsif !Vagrant::Util::Platform.terminal_supports_colors? opts[:ui_class] = Vagrant::UI::Basic elsif !$stdout.tty? && !Vagrant::Util::Platform.cygwin? # Cygwin always reports STDOUT is not a TTY, so we only disable # colors if its not a TTY AND its not Cygwin. opts[:ui_class] = Vagrant::UI::Basic end # Also allow users to force colors. if argv.include?("--color") || ENV["VAGRANT_FORCE_COLOR"] argv.delete("--color") opts[:ui_class] = Vagrant::UI::Colored end # Highest precedence is if we have enabled machine-readable output if argv.include?("--machine-readable") argv.delete("--machine-readable") opts[:ui_class] = Vagrant::UI::MachineReadable end # Setting to enable/disable showing progress bars if argv.include?("--no-tty") argv.delete("--no-tty") opts[:ui_class] = Vagrant::UI::NonInteractive end # Default to colored output opts[:ui_class] ||= Vagrant::UI::Colored # Recombine the arguments if !argv_extra.empty? argv << "--" argv += argv_extra end # Create the environment, which is the cwd of wherever the # `vagrant` command was invoked from logger.debug("Creating Vagrant environment") env = Vagrant::Environment.new(opts) # If we are running with the Windows Subsystem for Linux do # some extra setup to allow access to Vagrant managed machines # outside the subsystem if Vagrant::Util::Platform.wsl? recreate_env = Vagrant::Util::Platform.wsl_init(env, logger) if recreate_env logger.info("Re-creating Vagrant environment due to WSL modifications.") env = Vagrant::Environment.new(opts) end end # If not being run from the installer, check if expected tools # are available. if !Vagrant.in_installer? missing_tools = Vagrant.detect_missing_tools if !missing_tools.empty? env.ui.warn( I18n.t("vagrant.general.not_in_installer", tools: missing_tools.sort.join(", ")) + "\n", prefix: false ) end end # Acceptable experimental flag values include: # # Unset - Disables experimental features # 0 - Disables experimental features # 1 - Enables all features # String - Enables one or more features, separated by commas if Vagrant::Util::Experimental.enabled? experimental = Vagrant::Util::Experimental.features_requested ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant") logger.debug("Experimental flag is enabled") if Vagrant::Util::Experimental.global_enabled? ui.warn(I18n.t("vagrant.general.experimental.all"), bold: true, prefix: true, channel: :error) else ui.warn(I18n.t("vagrant.general.experimental.features", features: experimental.join(", ")), bold: true, prefix: true, channel: :error) end end begin # Execute the CLI interface, and exit with the proper error code exit_status = env.cli(argv) ensure # Unload the environment so cleanup can be done env.unload end # Exit with the exit status from our CLI command exit(exit_status) rescue Exception => e # It is possible for errors to happen in Vagrant's initialization. In # this case, we don't have access to this class yet, so we check for it. raise if !defined?(Vagrant) || !defined?(Vagrant::Errors) raise if !e.is_a?(Vagrant::Errors::VagrantError) require 'log4r' logger = Log4r::Logger.new("vagrant::bin::vagrant") logger.error("Vagrant experienced an error! Details:") logger.error(e.inspect) logger.error(e.message) logger.error(e.backtrace.join("\n")) if env opts = { prefix: false } env.ui.error(e.message, **opts) if e.message env.ui.machine("error-exit", e.class.to_s, e.message.to_s) else $stderr.puts "Vagrant failed to initialize at a very early stage:\n\n" $stderr.puts e.message end exit e.status_code if e.respond_to?(:status_code) exit 255 # An error occurred with no status code defined end ================================================ FILE: builtin/README.md ================================================ # Built-in Plugins This directory contains all the "built-in" plugins. These are real plugins, they dogfood the full plugin SDK, do not depend on any internal packages, and they are executed via subprocess just like a real plugin would be. The difference is that these plugins are linked directly into the single command binary. We do this currently for ease of development of the project. In the future we will split these out into standalone repositories and binaries. ================================================ FILE: builtin/configvagrant/main.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package configvagrant import ( "github.com/hashicorp/go-argmapper" "github.com/hashicorp/go-hclog" sdk "github.com/hashicorp/vagrant-plugin-sdk" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/core" "github.com/hashicorp/vagrant-plugin-sdk/terminal" ) var CommandOptions = []sdk.Option{ sdk.WithComponents( &Config{}, ), sdk.WithComponent(&Command{}, &component.CommandOptions{Primary: false}), sdk.WithName("configvagrant"), } type Vagrant struct { Sensitive []string `hcl:"sensitive,optional" json:",omitempty"` Host *string `hcl:"host,optional" json:"host,omitempty"` FinalizedInfo *string `hcl:"finalized_info,optional" json:"finalized_info,omitempty"` Plugins []Plugin `hcl:"plugins,block" json:"plugins,omitempty"` } type Plugin struct { Name string `hcl:"name,label"` EntryPoint *string `hcl:"entry_point,optional" json:"entry_point,omitempty"` Sources []string `hcl:"sources,optional" json:"source,omitempty"` Version *string `hcl:"version,optional" json:"version,omitempty"` } type Config struct{} func (c *Config) Register() (*component.ConfigRegistration, error) { return &component.ConfigRegistration{ Identifier: "vagrants", }, nil } func (c *Config) InitFunc() any { return c.Init } func (c *Config) Init(in *component.ConfigData) (*component.ConfigData, error) { return in, nil } func (c *Config) StructFunc() interface{} { return c.Struct } func (c *Config) Struct() *Vagrant { return &Vagrant{} } func (c *Config) MergeFunc() interface{} { return c.Merge } func (c *Config) Merge( input struct { argmapper.Struct Base *Vagrant Overlay *Vagrant Log hclog.Logger }, ) (*Vagrant, error) { log := input.Log log.Info("merging config values in vagrants namespace", "base", input.Base, "overlay", input.Overlay) result := input.Base if input.Overlay.Host != nil { result.Host = input.Overlay.Host } for _, s := range input.Overlay.Sensitive { result.Sensitive = append(result.Sensitive, s) } log.Info("merged config value for vagrants namespace", "config", result) return result, nil } func (c *Config) FinalizeFunc() interface{} { return c.Finalize } func (c *Config) Finalize(l hclog.Logger, conf *Vagrant) (*Vagrant, error) { l.Warn("checking current content", "host", conf.Host) if conf.Host != nil { l.Warn("checking current value", "host", *conf.Host) } info := "go plugin finalization test content" conf.FinalizedInfo = &info return conf, nil } type Command struct{} func (c *Command) ExecuteFunc(_ []string) interface{} { return c.Execute } func (c *Command) Execute(ui terminal.UI, p core.Project) int32 { ui.Output("Checking for our defined config...") v, err := p.Vagrantfile() if err != nil { ui.Output("Failed to get Vagrantfile instance: %s", err) return 1 } ui.Output("Our vagrantfile value is: %#v", v) conf, err := v.GetConfig("vagrants") if err != nil { ui.Output("failed to get configuration for 'vagrants' namespace: %q", err) return 1 } ui.Output("We got something here!") ui.Output("Config defined host: %s", conf.Data["host"]) if _, ok := conf.Data["finalized"]; !ok { ui.Output("ERROR: finalized data expected and not found in config!") } if _, ok := conf.Data["merged"]; !ok { ui.Output("ERROR: merged data expected and not found in config!") return 1 } return 0 } func (c *Command) CommandInfoFunc() interface{} { return c.CommandInfo } func (c *Command) CommandInfo() *component.CommandInfo { return &component.CommandInfo{ Name: "configvagrant", Help: "I display config", } } ================================================ FILE: builtin/httpdownloader/downloader/downloader.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package downloader import ( "bytes" "io/ioutil" "net/http" "os" "github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/vagrant-plugin-sdk/component" ) // Type is an enum of all the available http methods type HTTPMethod int64 const ( GET HTTPMethod = iota DELETE HEAD POST PUT ) type Downloader struct { config DownloaderConfig } type DownloaderConfig struct { Dest string Headers http.Header Method HTTPMethod RetryCount int RequestBody []byte Src string UrlQueryParams map[string]string } func (d *Downloader) InitFunc() any { return d.Init } func (d *Downloader) Init(in *component.ConfigData) (*component.ConfigData, error) { return in, nil } func (d *Downloader) StructFunc() any { return d.Struct } func (d *Downloader) MergeFunc() any { return d.Merge } func (d *Downloader) FinalizeFunc() any { return d.Finalize } func (d *Downloader) Struct() *DownloaderConfig { return &DownloaderConfig{} } func (d *Downloader) Merge(val *component.ConfigMerge) *component.ConfigData { return val.Base } func (d *Downloader) Finalize(val *component.ConfigData) *component.ConfigData { return val } func (d *Downloader) Register() (*component.ConfigRegistration, error) { return &component.ConfigRegistration{ Identifier: "downloader", }, nil } func (d *Downloader) DownloadFunc() interface{} { return d.Download } func (d *Downloader) Download() (err error) { client := retryablehttp.NewClient() client.RetryMax = d.config.RetryCount var req *retryablehttp.Request // Create request with request body if one is provided if d.config.RequestBody != nil { req, err = retryablehttp.NewRequest( d.config.Method.String(), d.config.Src, bytes.NewBuffer(d.config.RequestBody), ) if err != nil { return err } } else { // If no request body is provided then create an empty request req, err = retryablehttp.NewRequest( d.config.Method.String(), d.config.Src, nil, ) } // Add query params if provided if d.config.UrlQueryParams != nil { q := req.URL.Query() for k, v := range d.config.UrlQueryParams { q.Add(k, v) } req.URL.RawQuery = q.Encode() } // Set headers req.Header = d.config.Headers // Add headers to redirects client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { for key, val := range via[0].Header { req.Header[key] = val } return err } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { return err } err = os.WriteFile(d.config.Dest, data, 0644) return } var ( _ component.Downloader = (*Downloader)(nil) _ component.Config = (*Downloader)(nil) ) ================================================ FILE: builtin/httpdownloader/downloader/httpmethod_string.go ================================================ // Code generated by "stringer -type=HTTPMethod -linecomment ./downloader"; DO NOT EDIT. package downloader import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[GET-0] _ = x[DELETE-1] _ = x[HEAD-2] _ = x[POST-3] _ = x[PUT-4] } const _HTTPMethod_name = "GETDELETEHEADPOSTPUT" var _HTTPMethod_index = [...]uint8{0, 3, 9, 13, 17, 20} func (i HTTPMethod) String() string { if i < 0 || i >= HTTPMethod(len(_HTTPMethod_index)-1) { return "HTTPMethod(" + strconv.FormatInt(int64(i), 10) + ")" } return _HTTPMethod_name[_HTTPMethod_index[i]:_HTTPMethod_index[i+1]] } ================================================ FILE: builtin/httpdownloader/main.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package httpdownloader import ( sdk "github.com/hashicorp/vagrant-plugin-sdk" "github.com/hashicorp/vagrant/builtin/httpdownloader/downloader" ) //go:generate stringer -type=HTTPMethod -linecomment ./downloader var PluginOptions = []sdk.Option{ sdk.WithComponents( &downloader.Downloader{}, ), sdk.WithName("httpdownloader"), } ================================================ FILE: builtin/myplugin/command/command.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package command import ( "github.com/hashicorp/vagrant-plugin-sdk/component" plugincore "github.com/hashicorp/vagrant-plugin-sdk/core" "github.com/hashicorp/vagrant-plugin-sdk/docs" "github.com/hashicorp/vagrant-plugin-sdk/terminal" ) type Subcommand interface { CommandInfo() (*component.CommandInfo, error) } type CommandConfig struct { } // Command is the Command implementation for myplugin. type Command struct { config CommandConfig } func (c *Command) ConfigSet(v interface{}) error { return nil } func (c *Command) CommandFunc() interface{} { return nil } func (c *Command) Config() (interface{}, error) { return &c.config, nil } func (c *Command) Documentation() (*docs.Documentation, error) { doc, err := docs.New(docs.FromConfig(&CommandConfig{})) if err != nil { return nil, err } return doc, nil } // ExecuteFunc implements component.Command func (c *Command) ExecuteFunc(cliArgs []string) interface{} { if len(cliArgs) < 2 { return c.Execute } n := cliArgs[1] switch n { case "info": return c.ExecuteInfo case "dothing": return c.ExecuteDoThing case "interactive": return c.ExecuteInteractive case "host": return c.ExecuteHost } return c.Execute } func (c *Command) ExecuteInfo(trm terminal.UI, env plugincore.Project) int32 { return (&Info{Command: c}).Execute(trm, env) } func (c *Command) ExecuteDoThing(trm terminal.UI, params *component.CommandParams) int32 { return (&DoThing{Command: c}).Execute(trm, params) } func (c *Command) ExecuteInteractive(trm terminal.UI, params *component.CommandParams) int32 { return (&Interactive{Command: c}).Execute(trm) } func (c *Command) ExecuteHost(trm terminal.UI, env plugincore.Project) int32 { return (&Host{Command: c}).Execute(trm, env) } // CommandInfoFunc implements component.Command func (c *Command) CommandInfoFunc() interface{} { return c.CommandInfo } func (c *Command) CommandInfo() *component.CommandInfo { return &component.CommandInfo{ Name: "myplugin", Help: c.Help(), Synopsis: c.Synopsis(), Flags: c.Flags(), Subcommands: c.subcommandsInfo(), } } func (c *Command) Synopsis() string { return "I don't do much, just hanging around" } func (c *Command) Help() string { return "I'm here for testing, try running some subcommands" } func (c *Command) Flags() component.CommandFlags { return []*component.CommandFlag{ { LongName: "hehe", ShortName: "", Description: "a test flag for strings", DefaultValue: "a default message", Type: component.FlagString, }, } } func (c *Command) Execute(trm terminal.UI, params *component.CommandParams) int32 { trm.Output("You gave me the flag: " + params.Flags["hehe"].(string)) trm.Output(c.Help()) trm.Output("My subcommands are: ") for _, cmd := range c.subcommandsInfo() { trm.Output(" " + cmd.Name) } return 0 } func (c *Command) subcommandsInfo() (r []*component.CommandInfo) { for _, cmd := range c.subcommands() { v, _ := cmd.CommandInfo() r = append(r, v) } return } func (c *Command) subcommands() map[string]Subcommand { return map[string]Subcommand{ "info": &Info{Command: c}, "dothing": &DoThing{Command: c}, "interactive": &Interactive{Command: c}, "host": &Host{Command: c}, } } var ( _ component.Command = (*Command)(nil) ) ================================================ FILE: builtin/myplugin/command/dothing.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package command import ( "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/docs" "github.com/hashicorp/vagrant-plugin-sdk/localizer" "github.com/hashicorp/vagrant-plugin-sdk/terminal" "github.com/hashicorp/vagrant/builtin/myplugin/locales" "golang.org/x/text/language" ) // DoThing is a Command implementation for myplugin // It is a subcommand of myplugin type DoThing struct { *Command } func (c *DoThing) ConfigSet(v interface{}) error { return nil } func (c *DoThing) CommandFunc() interface{} { return nil } func (c *DoThing) Config() (interface{}, error) { return &c.config, nil } func (c *DoThing) Documentation() (*docs.Documentation, error) { doc, err := docs.New(docs.FromConfig(&CommandConfig{})) if err != nil { return nil, err } return doc, nil } // ExecuteFunc implements component.Command func (c *DoThing) ExecuteFunc([]string) interface{} { return c.Execute } // CommandInfoFunc implements component.Command func (c *DoThing) CommandInfoFunc() interface{} { return c.CommandInfo } func (c *DoThing) CommandInfo() (*component.CommandInfo, error) { return &component.CommandInfo{ Name: "dothing", Help: c.Help(), Synopsis: c.Synopsis(), Flags: c.Flags(), }, nil } func (c *DoThing) Synopsis() string { return "Really important *stuff*" } func (c *DoThing) Help() string { return "Usage: vagrant myplugin dothing" } func (c *DoThing) Flags() component.CommandFlags { return []*component.CommandFlag{ { LongName: "booltest", ShortName: "b", Description: "test flag for bools", DefaultValue: "true", Type: component.FlagBool, }, { LongName: "stringflag", ShortName: "s", Description: "test flag for strings", DefaultValue: "a default message value", Type: component.FlagString, }, } } func (c *DoThing) Execute(trm terminal.UI, params *component.CommandParams) int32 { localeDataEN, err := locales.Asset("locales/assets/en.json") if err != nil { return 1 } localeDataES, err := locales.Asset("locales/assets/es.json") if err != nil { return 1 } d := []localizer.LocaleData{ { LocaleData: localeDataEN, LocalePath: "locales/assets/en.json", Languages: []language.Tag{language.English, language.AmericanEnglish, language.BritishEnglish}, }, { LocaleData: localeDataES, LocalePath: "locales/assets/es.json", Languages: []language.Tag{language.Spanish}, }, } l, err := localizer.NewPluginLocalizer(d...) if err != nil { return 1 } msg, err := l.LocalizeMsg("dothing", nil) if err != nil { trm.Output(err.Error()) return 1 } trm.Output(msg, terminal.WithColor("magenta")) return 0 } var ( _ component.Command = (*DoThing)(nil) ) ================================================ FILE: builtin/myplugin/command/host.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package command import ( "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/core" "github.com/hashicorp/vagrant-plugin-sdk/terminal" ) // Info is a Command implementation for myplugin. // It is a subcommand of myplugin type Host struct { *Command } // ExecuteFunc implements component.Command func (c *Host) ExecuteFunc(cliArgs []string) interface{} { return c.Execute } // CommandInfoFunc implements component.Command func (c *Host) CommandInfoFunc() interface{} { return c.CommandInfo } func (c *Host) CommandInfo() (*component.CommandInfo, error) { return &component.CommandInfo{ Name: "host", Help: c.Help(), Synopsis: c.Synopsis(), Flags: c.Flags(), }, nil } func (c *Host) Synopsis() string { return "runs host capability" } func (c *Host) Help() string { return c.Synopsis() } func (c *Host) Flags() component.CommandFlags { return []*component.CommandFlag{} } func (c *Host) Execute(trm terminal.UI, project core.Project) int32 { trm.Output("Attempting to run capability on host plugin") h, err := project.Host() if err != nil { trm.Output("ERROR: %s", err) return 1 } trm.Output("have host plugin to run against") if r, err := h.HasCapability("write_hello"); !r { trm.Output("No write_hello capability found (%s)", err) return 1 } trm.Output("host plugin has write_hello capability to run") result, err := h.Capability("write_hello", trm) if err != nil { trm.Output("Error running capability: %s", err) return 1 } trm.Output("Result: %#v", result) return 0 } var ( _ component.Command = (*Host)(nil) ) ================================================ FILE: builtin/myplugin/command/info.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package command import ( "strings" "github.com/hashicorp/vagrant-plugin-sdk/component" plugincore "github.com/hashicorp/vagrant-plugin-sdk/core" "github.com/hashicorp/vagrant-plugin-sdk/docs" "github.com/hashicorp/vagrant-plugin-sdk/terminal" ) // Info is a Command implementation for myplugin. // It is a subcommand of myplugin type Info struct { *Command } func (c *Info) ConfigSet(v interface{}) error { return nil } func (c *Info) CommandFunc() interface{} { return nil } func (c *Info) Config() (interface{}, error) { return &c.config, nil } func (c *Info) Documentation() (*docs.Documentation, error) { doc, err := docs.New(docs.FromConfig(&CommandConfig{})) if err != nil { return nil, err } return doc, nil } // ExecuteFunc implements component.Command func (c *Info) ExecuteFunc(cliArgs []string) interface{} { return c.Execute } // CommandInfoFunc implements component.Command func (c *Info) CommandInfoFunc() interface{} { return c.CommandInfo } func (c *Info) CommandInfo() (*component.CommandInfo, error) { return &component.CommandInfo{ Name: "info", Help: c.Help(), Synopsis: c.Synopsis(), Flags: c.Flags(), }, nil } func (c *Info) Synopsis() string { return "Output some project information!" } func (c *Info) Help() string { return "Output some project information!" } func (c *Info) Flags() component.CommandFlags { return []*component.CommandFlag{} } func (c *Info) Execute(trm terminal.UI, p plugincore.Project) int32 { mn, _ := p.TargetNames() trm.Output("\nMachines in this project") trm.Output(strings.Join(mn[:], "\n")) cwd, _ := p.CWD() datadir, _ := p.DataDir() vagrantfileName, _ := p.VagrantfileName() home, _ := p.Home() localDataPath, _ := p.LocalData() defaultPrivateKeyPath, _ := p.DefaultPrivateKey() trm.Output("\nEnvironment information") trm.Output("Working directory: " + cwd.String()) trm.Output("Data directory: " + datadir.DataDir().String()) trm.Output("Vagrantfile name: " + vagrantfileName) trm.Output("Home directory: " + home.String()) trm.Output("Local data directory: " + localDataPath.String()) trm.Output("Default private key path: " + defaultPrivateKeyPath.String()) return 0 } var ( _ component.Command = (*Info)(nil) ) ================================================ FILE: builtin/myplugin/command/interactive.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package command import ( "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/docs" "github.com/hashicorp/vagrant-plugin-sdk/terminal" ) // Info is a Command implementation for myplugin. // It is a subcommand of myplugin type Interactive struct { *Command } func (c *Interactive) ConfigSet(v interface{}) error { return nil } func (c *Interactive) CommandFunc() interface{} { return nil } func (c *Interactive) Config() (interface{}, error) { return &c.config, nil } func (c *Interactive) Documentation() (*docs.Documentation, error) { doc, err := docs.New(docs.FromConfig(&CommandConfig{})) if err != nil { return nil, err } return doc, nil } // ExecuteFunc implements component.Command func (c *Interactive) ExecuteFunc(cliArgs []string) interface{} { return c.Execute } // CommandInfoFunc implements component.Command func (c *Interactive) CommandInfoFunc() interface{} { return c.CommandInfo } func (c *Interactive) CommandInfo() (*component.CommandInfo, error) { return &component.CommandInfo{ Name: "interactive", Help: c.Help(), Synopsis: c.Synopsis(), Flags: c.Flags(), }, nil } func (c *Interactive) Synopsis() string { return "Test out interactive input" } func (c *Interactive) Help() string { return "Test out interactive input!" } func (c *Interactive) Flags() component.CommandFlags { return []*component.CommandFlag{} } func (c *Interactive) Execute(trm terminal.UI) int32 { output, err := trm.Input(&terminal.Input{Prompt: "\nWhat do you have to say: "}) if err != nil { trm.Output("Error getting input") trm.Output(err.Error()) return 1 } trm.Output("Did you say " + output) output, err = trm.Input(&terminal.Input{Prompt: "\nTell me a secret: "}) if err != nil { trm.Output("Error getting input") trm.Output(err.Error()) return 1 } trm.Output("Did you say " + output) return 0 } var ( _ component.Command = (*Interactive)(nil) ) ================================================ FILE: builtin/myplugin/communicator/communicator.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package communicator import ( "github.com/hashicorp/go-argmapper" "github.com/hashicorp/go-hclog" "github.com/hashicorp/vagrant-plugin-sdk/component" plugincore "github.com/hashicorp/vagrant-plugin-sdk/core" pb "github.com/hashicorp/vagrant/builtin/myplugin/proto" ) type DummyConfig struct { } // DummyCommunicator is a Communicator implementation for myplugin. type DummyCommunicator struct { config DummyConfig } func (h *DummyCommunicator) MatchFunc() interface{} { return h.Match } func (h *DummyCommunicator) Match(machine plugincore.Machine) (isMatch bool, err error) { return true, nil } func (h *DummyCommunicator) InitFunc() interface{} { return h.Init } func (h *DummyCommunicator) Init(machine plugincore.Machine) error { return nil } func (h *DummyCommunicator) ReadyFunc() interface{} { return h.Ready } func (h *DummyCommunicator) Ready(machine plugincore.Machine) (isReady bool, err error) { return false, nil } func (h *DummyCommunicator) WaitForReadyFunc() interface{} { return h.WaitForReady } func (h *DummyCommunicator) WaitForReady(machine plugincore.Machine, wait int) (isReady bool, err error) { return false, nil } func (h *DummyCommunicator) DownloadFunc() interface{} { return h.Download } func (h *DummyCommunicator) Download(input struct { argmapper.Struct Machine plugincore.Machine `argmapper:",typeOnly"` Logger hclog.Logger `argmapper:",typeOnly"` Source string Destination string }, ) error { input.Logger.Debug("got Source ", input.Source) input.Logger.Debug("got Destination ", input.Destination) return nil } func (h *DummyCommunicator) UploadFunc() interface{} { return h.Upload } func (h *DummyCommunicator) Upload(input struct { argmapper.Struct Machine plugincore.Machine `argmapper:",typeOnly"` Logger hclog.Logger `argmapper:",typeOnly"` Source string Destination string }, ) error { input.Logger.Debug("got Source ", input.Source) input.Logger.Debug("got Destination ", input.Destination) return nil } func (h *DummyCommunicator) ExecuteFunc() interface{} { return h.Execute } func (h *DummyCommunicator) Execute( machine plugincore.Machine, command []string, options *pb.CommunicatorOptions, ) (status int32, err error) { return 0, nil } func (h *DummyCommunicator) PrivilegedExecuteFunc() interface{} { return h.PrivilegedExecute } func (h *DummyCommunicator) PrivilegedExecute( machine plugincore.Machine, command []string, options *pb.CommunicatorOptions, ) (status int32, err error) { return 0, nil } func (h *DummyCommunicator) TestFunc() interface{} { return h.Test } func (h *DummyCommunicator) Test( machine plugincore.Machine, command []string, options ...pb.CommunicatorOptions, ) (valid bool, err error) { return true, nil } func (h *DummyCommunicator) ResetFunc() interface{} { return h.Reset } func (h *DummyCommunicator) Reset(machine plugincore.Machine) (err error) { return nil } var ( _ component.Communicator = (*DummyCommunicator)(nil) ) ================================================ FILE: builtin/myplugin/host/alwaystrue.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package host import ( "fmt" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/terminal" "github.com/hashicorp/vagrant/builtin/myplugin/host/cap" ) type HostConfig struct { } // AlwaysTrueHost is a Host implementation for myplugin. type AlwaysTrueHost struct { config HostConfig } func (c *AlwaysTrueHost) Seed(args ...interface{}) error { return nil } func (c *AlwaysTrueHost) Seeds() ([]interface{}, error) { return nil, nil } // DetectFunc implements component.Host func (h *AlwaysTrueHost) HostDetectFunc() interface{} { return h.Detect } func (h *AlwaysTrueHost) Detect() bool { return true } // ParentFunc implements component.Host func (h *AlwaysTrueHost) ParentFunc() interface{} { return h.Parent } func (h *AlwaysTrueHost) Parent() string { return "" } // HasCapabilityFunc implements component.Host func (h *AlwaysTrueHost) HasCapabilityFunc() interface{} { return h.CheckCapability } func (h *AlwaysTrueHost) CheckCapability(n *component.NamedCapability) bool { if n.Capability == "write_hello" || n.Capability == "write_hello_file" { return true } return false } // CapabilityFunc implements component.Host func (h *AlwaysTrueHost) CapabilityFunc(name string) interface{} { if name == "write_hello" { return h.WriteHelloCap } else if name == "write_hello_file" { return h.WriteHelloToTempFileCap } return fmt.Errorf("requested capability %s not found", name) } func (h *AlwaysTrueHost) WriteHelloCap(ui terminal.UI) error { return cap.WriteHello(ui) } func (h *AlwaysTrueHost) WriteHelloToTempFileCap() error { return cap.WriteHelloToTempfile() } var ( _ component.Host = (*AlwaysTrueHost)(nil) ) ================================================ FILE: builtin/myplugin/host/cap/write_hello.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package cap import ( "os" "github.com/hashicorp/vagrant-plugin-sdk/terminal" ) func WriteHello(ui terminal.UI) error { msg := "Hello from the write hello capability, compliments of the AlwaysTrue Host" ui.Output(msg) return nil } func WriteHelloToTempfile() error { msg := []byte("Hello from the write hello capability, compliments of the AlwaysTrue Host") err := os.WriteFile("/tmp/write_hello", msg, 0644) if err != nil { panic(err) } return nil } ================================================ FILE: builtin/myplugin/locales/assets/en.json ================================================ { "dothing": "Tricked ya! I actually do nothing :P" } ================================================ FILE: builtin/myplugin/locales/assets/es.json ================================================ { "dothing": "¡Te engañé! en realidad no hago nada :P" } ================================================ FILE: builtin/myplugin/locales/locales.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 // Code generated for package locales by go-bindata DO NOT EDIT. (@generated) // sources: // locales/assets/en.json // locales/assets/es.json package locales import ( "bytes" "compress/gzip" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "time" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, fmt.Errorf("Read %q: %v", name, err) } var buf bytes.Buffer _, err = io.Copy(&buf, gz) clErr := gz.Close() if err != nil { return nil, fmt.Errorf("Read %q: %v", name, err) } if clErr != nil { return nil, err } return buf.Bytes(), nil } type asset struct { bytes []byte info os.FileInfo } type bindataFileInfo struct { name string size int64 mode os.FileMode modTime time.Time } // Name return file name func (fi bindataFileInfo) Name() string { return fi.name } // Size return file size func (fi bindataFileInfo) Size() int64 { return fi.size } // Mode return file mode func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } // Mode return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } // IsDir return file whether a directory func (fi bindataFileInfo) IsDir() bool { return fi.mode&os.ModeDir != 0 } // Sys return file is sys mode func (fi bindataFileInfo) Sys() interface{} { return nil } var _localesAssetsEnJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xe6\x52\x50\x50\x4a\xc9\x2f\xc9\xc8\xcc\x4b\x57\xb2\x52\x50\x0a\x29\xca\x4c\xce\x4e\x4d\x51\xa8\x4c\x54\x54\xf0\x54\x48\x4c\x2e\x29\x4d\xcc\xc9\xa9\x54\x48\xc9\x57\xc8\x83\x28\x52\xb0\x0a\x50\xe2\xaa\xe5\x02\x04\x00\x00\xff\xff\xae\xf3\xa3\xbd\x38\x00\x00\x00") func localesAssetsEnJsonBytes() ([]byte, error) { return bindataRead( _localesAssetsEnJson, "locales/assets/en.json", ) } func localesAssetsEnJson() (*asset, error) { bytes, err := localesAssetsEnJsonBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "locales/assets/en.json", size: 56, mode: os.FileMode(420), modTime: time.Unix(1651259149, 0)} a := &asset{bytes: bytes, info: info} return a, nil } var _localesAssetsEsJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xe6\x52\x50\x50\x4a\xc9\x2f\xc9\xc8\xcc\x4b\x57\xb2\x52\x50\x3a\xb4\x30\x24\x55\x21\x35\x2f\x3d\xf1\xf0\xc6\xc3\x2b\x15\x15\x52\xf3\x14\x8a\x52\x13\x73\x32\x53\x12\x53\x14\xf2\xf2\x15\x32\x12\xd3\xf3\x15\xf2\x12\x53\x12\x15\xac\x02\x94\xb8\x6a\xb9\x00\x01\x00\x00\xff\xff\xe5\x85\x54\x9c\x3e\x00\x00\x00") func localesAssetsEsJsonBytes() ([]byte, error) { return bindataRead( _localesAssetsEsJson, "locales/assets/es.json", ) } func localesAssetsEsJson() (*asset, error) { bytes, err := localesAssetsEsJsonBytes() if err != nil { return nil, err } info := bindataFileInfo{name: "locales/assets/es.json", size: 62, mode: os.FileMode(420), modTime: time.Unix(1651262545, 0)} a := &asset{bytes: bytes, info: info} return a, nil } // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. func Asset(name string) ([]byte, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { a, err := Asset(name) if err != nil { panic("asset: Asset(" + name + "): " + err.Error()) } return a } // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. func AssetInfo(name string) (os.FileInfo, error) { cannonicalName := strings.Replace(name, "\\", "/", -1) if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) for name := range _bindata { names = append(names, name) } return names } // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "locales/assets/en.json": localesAssetsEnJson, "locales/assets/es.json": localesAssetsEsJson, } // AssetDir returns the file names below a certain // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: // data/ // foo.txt // img/ // a.png // b.png // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree if len(name) != 0 { cannonicalName := strings.Replace(name, "\\", "/", -1) pathList := strings.Split(cannonicalName, "/") for _, p := range pathList { node = node.Children[p] if node == nil { return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { rv = append(rv, childName) } return rv, nil } type bintree struct { Func func() (*asset, error) Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ "locales": &bintree{nil, map[string]*bintree{ "assets": &bintree{nil, map[string]*bintree{ "en.json": &bintree{localesAssetsEnJson, map[string]*bintree{}}, "es.json": &bintree{localesAssetsEsJson, map[string]*bintree{}}, }}, }}, }} // RestoreAsset restores an asset under the given directory func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { return err } info, err := AssetInfo(name) if err != nil { return err } err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) if err != nil { return err } err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) if err != nil { return err } err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) if err != nil { return err } return nil } // RestoreAssets restores an asset under the given directory recursively func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File if err != nil { return RestoreAsset(dir, name) } // Dir for _, child := range children { err = RestoreAssets(dir, filepath.Join(name, child)) if err != nil { return err } } return nil } func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } ================================================ FILE: builtin/myplugin/main.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package myplugin import ( sdk "github.com/hashicorp/vagrant-plugin-sdk" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant/builtin/myplugin/command" "github.com/hashicorp/vagrant/builtin/myplugin/communicator" "github.com/hashicorp/vagrant/builtin/myplugin/host" "github.com/hashicorp/vagrant/builtin/myplugin/provider" "github.com/hashicorp/vagrant/builtin/myplugin/push" ) //go:generate protoc -I ../../.. --go_opt=plugins=grpc --go_out=../../.. vagrant-ruby/builtin/myplugin/proto/plugin.proto // Locales data bundling //go:generate go-bindata -o ./locales/locales.go -pkg locales locales/assets // Options are the SDK options to use for instantiation. var CommandOptions = []sdk.Option{ sdk.WithComponents( // &Provider{}, &host.AlwaysTrueHost{}, &communicator.DummyCommunicator{}, &push.Encouragement{}, ), sdk.WithComponent(&command.Command{}, &component.CommandOptions{ // Should keep the plugin out of the default help output Primary: false, }), sdk.WithComponent(&provider.Happy{}, &component.ProviderOptions{ Priority: 100, }), sdk.WithMappers(StructToCommunincatorOptions), sdk.WithName("myplugin"), } ================================================ FILE: builtin/myplugin/mappers.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package myplugin import ( pb "github.com/hashicorp/vagrant/builtin/myplugin/proto" "github.com/mitchellh/mapstructure" "google.golang.org/protobuf/types/known/structpb" ) func StructToCommunincatorOptions(in *structpb.Struct) (*pb.CommunicatorOptions, error) { var result pb.CommunicatorOptions return &result, mapstructure.Decode(in.AsMap(), &result) } ================================================ FILE: builtin/myplugin/proto/plugin.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 // protoc v3.19.4 // source: vagrant-ruby/builtin/myplugin/proto/plugin.proto package proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type UpResult struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *UpResult) Reset() { *x = UpResult{} if protoimpl.UnsafeEnabled { mi := &file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UpResult) String() string { return protoimpl.X.MessageStringOf(x) } func (*UpResult) ProtoMessage() {} func (x *UpResult) ProtoReflect() protoreflect.Message { mi := &file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UpResult.ProtoReflect.Descriptor instead. func (*UpResult) Descriptor() ([]byte, []int) { return file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDescGZIP(), []int{0} } type CommunicatorOptions struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields KeepAlive string `protobuf:"bytes,1,opt,name=keep_alive,json=keepAlive,proto3" json:"keep_alive,omitempty"` Timeout int64 `protobuf:"varint,2,opt,name=timeout,proto3" json:"timeout,omitempty"` } func (x *CommunicatorOptions) Reset() { *x = CommunicatorOptions{} if protoimpl.UnsafeEnabled { mi := &file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CommunicatorOptions) String() string { return protoimpl.X.MessageStringOf(x) } func (*CommunicatorOptions) ProtoMessage() {} func (x *CommunicatorOptions) ProtoReflect() protoreflect.Message { mi := &file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CommunicatorOptions.ProtoReflect.Descriptor instead. func (*CommunicatorOptions) Descriptor() ([]byte, []int) { return file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDescGZIP(), []int{1} } func (x *CommunicatorOptions) GetKeepAlive() string { if x != nil { return x.KeepAlive } return "" } func (x *CommunicatorOptions) GetTimeout() int64 { if x != nil { return x.Timeout } return 0 } var File_vagrant_ruby_builtin_myplugin_proto_plugin_proto protoreflect.FileDescriptor var file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDesc = []byte{ 0x0a, 0x30, 0x76, 0x61, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x2d, 0x72, 0x75, 0x62, 0x79, 0x2f, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x2f, 0x6d, 0x79, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x6d, 0x79, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x0a, 0x0a, 0x08, 0x55, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x4e, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x75, 0x6e, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x6b, 0x65, 0x65, 0x70, 0x5f, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x42, 0x25, 0x5a, 0x23, 0x76, 0x61, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x2d, 0x72, 0x75, 0x62, 0x79, 0x2f, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x69, 0x6e, 0x2f, 0x6d, 0x79, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDescOnce sync.Once file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDescData = file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDesc ) func file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDescGZIP() []byte { file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDescOnce.Do(func() { file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDescData = protoimpl.X.CompressGZIP(file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDescData) }) return file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDescData } var file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_goTypes = []interface{}{ (*UpResult)(nil), // 0: myplugin.UpResult (*CommunicatorOptions)(nil), // 1: myplugin.CommunicatorOptions } var file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_init() } func file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_init() { if File_vagrant_ruby_builtin_myplugin_proto_plugin_proto != nil { return } if !protoimpl.UnsafeEnabled { file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UpResult); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CommunicatorOptions); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDesc, NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 0, }, GoTypes: file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_goTypes, DependencyIndexes: file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_depIdxs, MessageInfos: file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_msgTypes, }.Build() File_vagrant_ruby_builtin_myplugin_proto_plugin_proto = out.File file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_rawDesc = nil file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_goTypes = nil file_vagrant_ruby_builtin_myplugin_proto_plugin_proto_depIdxs = nil } ================================================ FILE: builtin/myplugin/proto/plugin.proto ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 syntax = "proto3"; package myplugin; option go_package = "vagrant-ruby/builtin/myplugin/proto"; message UpResult {} message CommunicatorOptions { string keep_alive = 1; int64 timeout = 2; } ================================================ FILE: builtin/myplugin/provider/happy.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package provider import ( "context" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/core" ) // Happy is a provider that is just happy to be backing your vagrant VMs. type Happy struct{} func (p *Happy) Action(name string, args ...interface{}) error { return nil } // ActionFunc implements component.Provider func (h *Happy) ActionFunc(actionName string) interface{} { return h.Action } func (h *Happy) Capability(name string, args ...interface{}) (interface{}, error) { return nil, nil } // CapabilityFunc implements component.Provider func (h *Happy) CapabilityFunc(name string) interface{} { return h.Capability } func (h *Happy) HasCapability(n *component.NamedCapability) bool { return false } // HasCapabilityFunc implements component.Provicer func (h *Happy) HasCapabilityFunc() interface{} { return h.HasCapability } func (h *Happy) MachineIdChanged() error { return nil } // MachineIdChangedFunc implements component.Provicer func (h *Happy) MachineIdChangedFunc() interface{} { return h.MachineIdChanged } func (h *Happy) Installed(context.Context) (bool, error) { return true, nil } // InstalledFunc implements component.Provider func (h *Happy) InstalledFunc() interface{} { return h.Installed } func (h *Happy) Init() (bool, error) { return true, nil } // InitFunc implements component.Provider func (h *Happy) InitFunc() interface{} { return h.Init } func (h *Happy) SshInfo() (*core.SshInfo, error) { return nil, nil } // SshInfoFunc implements component.Provider func (h *Happy) SshInfoFunc() interface{} { return h.SshInfo } func (h *Happy) State() (*core.MachineState, error) { return nil, nil } // StateFunc implements component.Provider func (h *Happy) StateFunc() interface{} { return h.State } func (h *Happy) Usable() (bool, error) { return false, nil } // UsableFunc implements component.Provider func (h *Happy) UsableFunc() interface{} { return h.Usable } var ( _ component.Provider = (*Happy)(nil) ) ================================================ FILE: builtin/myplugin/push/encouragement.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package push import ( "encoding/json" "fmt" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/core" "github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk" "github.com/hashicorp/vagrant-plugin-sdk/terminal" "google.golang.org/protobuf/types/known/structpb" ) // Encouragement is a push strategy that provides encouragement for the code // you push. Everybody could use some encouragement sometimes! type Encouragement struct{} func (e *Encouragement) PushFunc() interface{} { return e.Push } // Push runs this is the first ever Golang push plugin! func (e *Encouragement) Push(ui terminal.UI, proj core.Project) error { ui.Output("You've invoked a push plugin written in Go! Great work!") pushConfig, err := findPushConfig(proj, "myplugin") if err != nil { return err } if pushConfig != nil { ui.Output("Look a this nice config you sent along too!") ui.Output("We'll print it as JSON for fun:") config, err := unpackConfig(pushConfig) if err != nil { return err } jsonConfig, err := json.MarshalIndent(config, " ", "\t") if err != nil { return err } ui.Output(" %s", jsonConfig) } return nil } // findPushConfig finds the relevant PushConfig for the name given. // // For now, there are no config related helpers, so each push plugin needs to // walk its way down to its relevant config in the Vagrantfile. func findPushConfig(proj core.Project, name string) (*vagrant_plugin_sdk.Vagrantfile_PushConfig, error) { return nil, fmt.Errorf("unimplemented") // v, err := proj.Config() // if err != nil { // return nil, err // } // for _, p := range v.GetPushConfigs() { // if p.GetName() == name { // return p, nil // } // } // return nil, nil } // unpackConfig takes a PushConfig and unpack the underlying map of config // // For now, there are no config related helpers, so each push plugin needs to // unpack from a generic struct into whatever types it might need. For this // demo plugin we're just leaving it untyped. func unpackConfig(pc *vagrant_plugin_sdk.Vagrantfile_PushConfig) (map[string]interface{}, error) { gc := pc.GetConfig() s := &structpb.Struct{} err := gc.GetConfig().UnmarshalTo(s) if err != nil { return nil, err } return s.AsMap(), nil } var ( _ component.Push = (*Encouragement)(nil) ) ================================================ FILE: builtin/otherplugin/command.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package otherplugin import ( "strings" "github.com/hashicorp/vagrant-plugin-sdk/component" plugincore "github.com/hashicorp/vagrant-plugin-sdk/core" // "github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk" "github.com/hashicorp/vagrant-plugin-sdk/terminal" //"google.golang.org/protobuf/types/known/anypb" ) type Command struct{} func (c *Command) ExecuteFunc(cliArgs []string) interface{} { if len(cliArgs) < 2 { return c.ExecuteMain } switch cliArgs[1] { case "info": if len(cliArgs) < 3 { return c.ExecuteInfo } switch cliArgs[2] { case "ofni": return c.ExecuteOfni } return c.ExecuteInfo case "dothing": return c.ExecuteThing case "use-host": return c.ExecuteUseHostPlugin } return c.ExecuteMain } func (c *Command) CommandInfoFunc() interface{} { return c.CommandInfo } func (c *Command) CommandInfo() *component.CommandInfo { return &component.CommandInfo{ Name: "otherplugin", Help: "HELP MEEEEE!", Synopsis: "This command does stuff", Flags: []*component.CommandFlag{ { LongName: "thing", Description: "a thing flag", DefaultValue: "I'm a thing!", Type: component.FlagString, }, }, Subcommands: []*component.CommandInfo{ &component.CommandInfo{ Name: "info", Help: "Shows info", Synopsis: "IT. SHOWS. INFO.", Flags: []*component.CommandFlag{}, Subcommands: []*component.CommandInfo{ &component.CommandInfo{ Name: "ofni", Help: "Shows ofni", Synopsis: "BIZZARO info", Flags: []*component.CommandFlag{}, }, }, }, &component.CommandInfo{ Name: "dothing", Help: "Does thing", Synopsis: "Does this super great thing!", Flags: []*component.CommandFlag{ { LongName: "stringflag", Description: "a test flag", DefaultValue: "I'm a string!", Type: component.FlagString, }, }, }, &component.CommandInfo{ Name: "use-host", Help: "Executes a host capability", Synopsis: "Executes a host capability", Flags: []*component.CommandFlag{}, }, }, } } func (c *Command) ExecuteMain(trm terminal.UI, flags map[string]interface{}) int32 { trm.Output("You gave me the flag: " + flags["thing"].(string)) trm.Output("My subcommands are: `info` and `dothing`") return 0 } func (c *Command) ExecuteThing(trm terminal.UI, flags map[string]interface{}) int32 { trm.Output("Tricked ya! I actually do nothing :P") trm.Output("You gave me the stringflag: " + flags["stringflag"].(string)) return 0 } func (c *Command) ExecuteInfo(trm terminal.UI, p plugincore.Project) int32 { mn, _ := p.TargetNames() trm.Output("\nMachines in this project") trm.Output(strings.Join(mn[:], "\n")) cwd, _ := p.CWD() datadir, _ := p.DataDir() vagrantfileName, _ := p.VagrantfileName() home, _ := p.Home() localDataPath, _ := p.LocalData() defaultPrivateKeyPath, _ := p.DefaultPrivateKey() trm.Output("\nEnvironment information") trm.Output("Working directory: " + cwd.String()) if datadir != nil && datadir.DataDir() != nil { trm.Output("Data directory: " + datadir.DataDir().String()) } trm.Output("Vagrantfile name: " + vagrantfileName) trm.Output("Home directory: " + home.String()) trm.Output("Local data directory: " + localDataPath.String()) trm.Output("Default private key path: " + defaultPrivateKeyPath.String()) ptrm, err := p.UI() if err != nil { trm.Output("Failed to get project specific UI! Reason: " + err.Error()) } else { ptrm.Output("YAY! This is project specific output!") } t, err := p.Target("one", "") if err != nil { trm.Output("Failed to load `one' target -- " + err.Error()) return 1 } m, err := t.Specialize((*plugincore.Machine)(nil)) if err != nil { trm.Output("Failed to specialize to machine! -- " + err.Error()) return 1 } machine := m.(plugincore.Machine) trm.Output("successfully specialized to machine") id, err := machine.ID() if err != nil { trm.Output("failed to get machine id --> " + err.Error()) } else { trm.Output("machine id is: " + id) } return 10 } func (c *Command) ExecuteOfni(trm terminal.UI) int32 { trm.Output("I am bizzaro info! Call me ofni") return 0 } func (c *Command) ExecuteUseHostPlugin(trm terminal.UI, basis plugincore.Basis) int32 { trm.Output("Requesting host plugin...") host, err := basis.Host() if err != nil { trm.Output("Error: Failed to receive host plugin - " + err.Error()) return 1 } trm.Output("Host plugin received. Checking for `write_hello` capability...") ok, err := host.HasCapability("write_hello") if err != nil { trm.Output("ERROR: " + err.Error()) // return 1 } if ok { trm.Output("Found `write_hello` capability for host plugin, calling...") _, err = host.Capability("write_hello", trm) if err != nil { trm.Output("Error executing capability - " + err.Error()) return 1 } } else { trm.Output("no `write_hello` capability found") } return 0 } ================================================ FILE: builtin/otherplugin/guest/alwaystrue.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package guest import ( "errors" "github.com/hashicorp/vagrant-plugin-sdk/component" plugincore "github.com/hashicorp/vagrant-plugin-sdk/core" "github.com/hashicorp/vagrant/builtin/otherplugin/guest/cap" ) type GuestConfig struct { } // AlwaysTrueGuest is a Guest implementation for myplugin. type AlwaysTrueGuest struct { config GuestConfig } func (c *AlwaysTrueGuest) Seed(args ...interface{}) error { return nil } func (c *AlwaysTrueGuest) Seeds() ([]interface{}, error) { return nil, nil } // DetectFunc implements component.Guest func (h *AlwaysTrueGuest) GuestDetectFunc() interface{} { return h.Detect } func (h *AlwaysTrueGuest) Detect(t plugincore.Target) bool { m, err := t.Specialize((*plugincore.Machine)(nil)) if err != nil { return false } machine := m.(plugincore.Machine) machine.ConnectionInfo() // TODO: need a communicator to connect to guest return true } // ParentsFunc implements component.Guest func (h *AlwaysTrueGuest) ParentFunc() interface{} { return h.Parent } func (h *AlwaysTrueGuest) Parent() string { return "" } // HasCapabilityFunc implements component.Guest func (h *AlwaysTrueGuest) HasCapabilityFunc() interface{} { return h.CheckCapability } func (h *AlwaysTrueGuest) CheckCapability(n *component.NamedCapability) bool { if n.Capability == "hello" { return true } return false } // CapabilityFunc implements component.Guest func (h *AlwaysTrueGuest) CapabilityFunc(name string) interface{} { if name == "hello" { return h.WriteHelloCap } return errors.New("invalid capability requested") } func (h *AlwaysTrueGuest) WriteHelloCap(m plugincore.Machine) error { return cap.WriteHello(m) } var ( _ component.Guest = (*AlwaysTrueGuest)(nil) ) ================================================ FILE: builtin/otherplugin/guest/cap/write_hello.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package cap import ( "io/ioutil" plugincore "github.com/hashicorp/vagrant-plugin-sdk/core" ) func WriteHello(machine plugincore.Machine) (err error) { d1 := []byte("hello\ngo\n") ioutil.WriteFile("/tmp/dat1", d1, 0644) machine.ConnectionInfo() // TODO: write something to guest machine // need a communicator return } ================================================ FILE: builtin/otherplugin/main.go ================================================ // Copyright IBM Corp. 2010, 2025 // SPDX-License-Identifier: BUSL-1.1 package otherplugin import ( sdk "github.com/hashicorp/vagrant-plugin-sdk" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant/builtin/otherplugin/guest" ) var CommandOptions = []sdk.Option{ sdk.WithComponents( &guest.AlwaysTrueGuest{}, ), sdk.WithComponent(&Command{}, &component.CommandOptions{ // Hide command from default help output Primary: false, }), sdk.WithName("otherplugin"), } ================================================ FILE: contrib/README.md ================================================ # Vagrant Contrib Miscellaneous contributions which assist people in using Vagrant will make their way into this directory. An up-to-date list of short descriptions for each item will be kept below. ## List of Contrib Items * `bash` - Contains a bash script for improving autocompletion with bash. * `emacs` - Contains a file for enabling Ruby syntax highlighting for `Vagrantfile`s in `emacs`. * `st` - Contains a `.sublime-settings` file for enabling Ruby syntax highlighting for `Vagrantfile`s in Sublime Text. * `sudoers` - Contains the pieces of `/etc/sudoers` configuration to avoid password entry when starting machines. * `vim` - Contains a `.vim` file for enabling Ruby syntax highlighting for `Vagrantfile`s in `vim`. * `zsh` - Contains a zsh script for improving autocompletion with zsh. ================================================ FILE: contrib/bash/completion.sh ================================================ #!/bin/bash # (The MIT License) # # Copyright (c) 2014 Kura # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the 'Software'), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. __pwdln() { pwdmod="${PWD}/" itr=0 until [[ -z "$pwdmod" ]];do itr=$(($itr+1)) pwdmod="${pwdmod#*/}" done echo -n $(($itr-1)) } __vagrantinvestigate() { if [ -f "${PWD}/.vagrant" -o -d "${PWD}/.vagrant" ];then echo "${PWD}/.vagrant" return 0 else pwdmod2="${PWD}" for (( i=2; i<=$(__pwdln); i++ ));do pwdmod2="${pwdmod2%/*}" if [ -f "${pwdmod2}/.vagrant" -o -d "${pwdmod2}/.vagrant" ];then echo "${pwdmod2}/.vagrant" return 0 fi done fi return 1 } _vagrant() { cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" commands="box cloud connect destroy docker-exec docker-logs docker-run global-status halt help init list-commands login package plugin provision push rdp reload resume rsync rsync-auto share snapshot ssh ssh-config status suspend up version" if [ $COMP_CWORD == 1 ] then COMPREPLY=($(compgen -W "${commands}" -- ${cur})) return 0 fi if [ $COMP_CWORD == 2 ] then case "$prev" in "init") local box_list=$(find "${VAGRANT_HOME:-${HOME}/.vagrant.d}/boxes" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed -e 's/-VAGRANTSLASH-/\//') COMPREPLY=($(compgen -W "${box_list}" -- ${cur})) return 0 ;; "up") vagrant_state_file=$(__vagrantinvestigate) || return 1 if [[ -d "${vagrant_state_file}" ]] then local vm_list=$(find "${vagrant_state_file}/machines" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) fi local up_commands="\ --provision \ --no-provision \ --provision-with \ --destroy-on-error \ --no-destroy-on-error \ --parallel \ --no-parallel --provider \ --install-provider \ --no-install-provider \ -h \ --help" COMPREPLY=($(compgen -W "${up_commands} ${vm_list}" -- ${cur})) return 0 ;; "destroy"|"ssh"|"provision"|"reload"|"halt"|"suspend"|"resume"|"ssh-config") vagrant_state_file=$(__vagrantinvestigate) || return 1 if [[ -f "${vagrant_state_file}" ]] then running_vm_list=$(grep 'active' "${vagrant_state_file}" | sed -e 's/"active"://' | tr ',' '\n' | cut -d '"' -f 2 | tr '\n' ' ') else running_vm_list=$(find "${vagrant_state_file}/machines" -type f -name "id" | awk -F"/" '{print $(NF-2)}') fi COMPREPLY=($(compgen -W "${running_vm_list}" -- ${cur})) return 0 ;; "box") box_commands="add help list outdated prune remove repackage update" COMPREPLY=($(compgen -W "${box_commands}" -- ${cur})) return 0 ;; "cloud") cloud_commands="auth box search provider publish version" COMPREPLY=($(compgen -W "${cloud_commands}" -- ${cur})) return 0 ;; "plugin") plugin_commands="install license list uninstall update" COMPREPLY=($(compgen -W "${plugin_commands}" -- ${cur})) return 0 ;; "help") COMPREPLY=($(compgen -W "${commands}" -- ${cur})) return 0 ;; "snapshot") snapshot_commands="delete list pop push restore save" COMPREPLY=($(compgen -W "${snapshot_commands}" -- ${cur})) return 0 ;; *) ;; esac fi if [ $COMP_CWORD == 3 ] then action="${COMP_WORDS[COMP_CWORD-2]}" case "$action" in "up") if [ "$prev" == "--no-provision" ] then if [[ -d "${vagrant_state_file}" ]] then local vm_list=$(find "${vagrant_state_file}/machines" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) fi COMPREPLY=($(compgen -W "${vm_list}" -- ${cur})) return 0 fi ;; "box") case "$prev" in "remove"|"repackage") local box_list=$(find "${VAGRANT_HOME:-${HOME}/.vagrant.d}/boxes" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sed -e 's/-VAGRANTSLASH-/\//') COMPREPLY=($(compgen -W "${box_list}" -- ${cur})) return 0 ;; "add") local add_commands="\ --name \ --checksum \ --checksum-type \ -c --clean \ -f --force \ " if [[ $cur == -* ]]; then COMPREPLY=($(compgen -W "${add_commands}" -- ${cur})) else COMPREPLY=($(compgen -o default -- "${cur}")) fi return 0 ;; *) ;; esac ;; "snapshot") case "$prev" in "restore"|"delete") local snapshot_list=$(vagrant snapshot list) COMPREPLY=($(compgen -W "${snapshot_list}" -- ${cur})) return 0 ;; esac ;; *) ;; esac fi } complete -F _vagrant vagrant ================================================ FILE: contrib/emacs/vagrant.el ================================================ ;; Copyright IBM Corp. 2010, 2025 ;; SPDX-License-Identifier: BUSL-1.1 ;;-------------------------------------------------------------------- ;; Teach emacs to syntax highlight Vagrantfile as Ruby. ;; ;; Installation: Copy the line below into your emacs configuration, ;; or drop this file anywhere in your "~/.emacs.d" directory and be ;; sure to "load" it. ;;-------------------------------------------------------------------- (add-to-list 'auto-mode-alist '("Vagrantfile$" . ruby-mode)) ================================================ FILE: contrib/st/Ruby.sublime-settings ================================================ { "extensions": [ "Vagrantfile" ] } ================================================ FILE: contrib/sudoers/linux-fedora ================================================ Cmnd_Alias VAGRANT_EXPORTS_ADD = /usr/bin/tee -a /etc/exports Cmnd_Alias VAGRANT_NFSD_CHECK = /usr/bin/systemctl status --no-pager 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 /*/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 ================================================ FILE: contrib/sudoers/linux-suse ================================================ Cmnd_Alias VAGRANT_CHOWN = /usr/bin/chown 0\:0 /tmp/vagrant[a-z0-9-]* Cmnd_Alias VAGRANT_MV = /usr/bin/mv -f /tmp/vagrant[a-z0-9-]* /etc/exports Cmnd_Alias VAGRANT_START = /usr/bin/systemctl start --no-pager nfs-server Cmnd_Alias VAGRANT_STATUS = /usr/bin/systemctl status --no-pager nfs-server Cmnd_Alias VAGRANT_APPLY = /usr/sbin/exportfs -ar %vagrant ALL=(root) NOPASSWD: VAGRANT_CHOWN, VAGRANT_MV, VAGRANT_START, VAGRANT_STATUS, VAGRANT_APPLY ================================================ FILE: contrib/sudoers/linux-ubuntu ================================================ # These work with Ubuntu - they might need tweaking for other distributions Cmnd_Alias VAGRANT_EXPORTS_CHOWN = /bin/chown 0\:0 /tmp/* Cmnd_Alias VAGRANT_EXPORTS_MV = /bin/mv -f /tmp/* /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 %sudo ALL=(root) NOPASSWD: VAGRANT_EXPORTS_CHOWN, VAGRANT_EXPORTS_MV, VAGRANT_NFSD_CHECK, VAGRANT_NFSD_START, VAGRANT_NFSD_APPLY ================================================ FILE: contrib/sudoers/osx ================================================ Cmnd_Alias VAGRANT_EXPORTS_ADD = /usr/bin/tee -a /etc/exports Cmnd_Alias VAGRANT_NFSD = /sbin/nfsd ^(restart|status|update)$ Cmnd_Alias VAGRANT_EXPORTS_REMOVE = /usr/bin/sed -E -e /*/ d -ibak /etc/exports Cmnd_Alias VAGRANT_SMB_ADD = /usr/sbin/sharing -a * -S * -s * -g * -n * Cmnd_Alias VAGRANT_SMB_REMOVE = /usr/sbin/sharing -r * Cmnd_Alias VAGRANT_SMB_LIST = /usr/sbin/sharing -l Cmnd_Alias VAGRANT_SMB_PLOAD = /bin/launchctl load -w /System/Library/LaunchDaemons/com.apple.smb.preferences.plist Cmnd_Alias VAGRANT_SMB_DLOAD = /bin/launchctl load -w /System/Library/LaunchDaemons/com.apple.smbd.plist Cmnd_Alias VAGRANT_SMB_DSTART = /bin/launchctl start com.apple.smbd %admin ALL=(root) NOPASSWD: VAGRANT_EXPORTS_ADD, VAGRANT_NFSD, VAGRANT_EXPORTS_REMOVE, VAGRANT_SMB_ADD, VAGRANT_SMB_REMOVE, VAGRANT_SMB_LIST, VAGRANT_SMB_PLOAD, VAGRANT_SMB_DLOAD, VAGRANT_SMB_DSTART ================================================ FILE: contrib/vim/vagrantfile.vim ================================================ " Teach vim to syntax highlight Vagrantfile as ruby " " Install: $HOME/.vim/plugin/vagrant.vim " Author: Brandon Philips augroup vagrant au! au BufRead,BufNewFile Vagrantfile set filetype=ruby augroup END ================================================ FILE: contrib/zsh/_vagrant ================================================ #compdef _vagrant vagrant # ZSH completion for Vagrant # # To use this completion add this to ~/.zshrc # fpath=(/path/to/this/dir $fpath) # compinit # # For development reload the function after making changes # unfunction _vagrant && autoload -U _vagrant __box_list () { _wanted application expl 'command' compadd $(command vagrant box list | awk '{print $1}' ) } __plugin_list () { _wanted application expl 'command' compadd $(command vagrant plugin list | awk '{print $1}') } function _vagrant () { local -a sub_commands && sub_commands=( 'autocomplete:manages autocomplete installation on host' 'box:manages boxes: installation, removal, etc.' 'cloud:manages everything related to Vagrant Cloud' 'destroy:stops and deletes all traces of the vagrant machine' 'global-status:outputs status Vagrant environments for this user' 'halt:stops the vagrant machine' 'help:shows the help for a subcommand' 'init:initializes a new Vagrant environment by creating a Vagrantfile' 'login:' 'package:packages a running vagrant environment into a box' 'plugin:manages plugins: install, uninstall, update, etc.' 'port:displays information about guest port mappings' 'powershell:connects to machine via powershell remoting' 'provision:provisions the vagrant machine' 'push:deploys code in this environment to a configured destination' 'rdp:connects to machine via RDP' 'reload:restarts vagrant machine, loads new Vagrantfile configuration' 'resume:resume a suspended vagrant machine' 'snapshot:manages snapshots: saving, restoring, etc.' 'ssh:connects to machine via SSH' 'ssh-config:outputs OpenSSH valid configuration to connect to the machine' 'status:outputs status of the vagrant machine' 'suspend:suspends the machine' 'up:starts and provisions the vagrant environment' 'upload:upload to machine via communicator' 'validate:validates the Vagrantfile' 'version:prints current and latest Vagrant version' 'winrm:executes commands on a machine via WinRM' 'winrm-config:outputs WinRM configuration to connect to the machine' ) local -a cloud_arguments && cloud_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a destroy_arguments && destroy_arguments=( '--(no-)parallel=[Enable or disable parallelism if provider supports it (automatically enables force)]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a global_status_arguments && global_status_arguments=( '--prune=[Prune invalid entries.]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a halt_arguments && halt_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a help_arguments && help_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a init_arguments && init_arguments=( '--box-version=[Version of the box to add]' '--output=[Output path for the box. _ for stdout]' '--template=[Path to custom Vagrantfile template]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a login_arguments && login_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a package_arguments && package_arguments=( '--base=[Name of a VM in VirtualBox to package as a base box (VirtualBox Only)]' '--output=[Name of the file to output]' '--include=[Comma separated additional files to package with the box]' '--vagrantfile=[Vagrantfile to package with the box]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a port_arguments && port_arguments=( '--guest=[Output the host port that maps to the given guest port]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a powershell_arguments && powershell_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a provision_arguments && provision_arguments=( '--provision-with=[Enable only certain provisioners, by type or by name.]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a push_arguments && push_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a rdp_arguments && rdp_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a reload_arguments && reload_arguments=( '--(no-)provision=[Enable or disable provisioning]' '--provision-with=[Enable only certain provisioners, by type or by name.]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a resume_arguments && resume_arguments=( '--(no-)provision=[Enable or disable provisioning]' '--provision-with=[Enable only certain provisioners, by type or by name.]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a ssh_arguments && ssh_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a ssh_config_arguments && ssh_config_arguments=( '--host=[Name the host for the config]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a status_arguments && status_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a suspend_arguments && suspend_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a up_arguments && up_arguments=( '--(no-)provision=[Enable or disable provisioning]' '--provision-with=[Enable only certain provisioners, by type or by name.]' '--(no-)destroy-on-error=[Destroy machine if any fatal error happens (default to true)]' '--(no-)parallel=[Enable or disable parallelism if provider supports it]' '--provider=[Back the machine with a specific provider]' '--(no-)install-provider=[If possible, install the provider if it isnt installed]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a upload_arguments && upload_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a validate_arguments && validate_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a version_arguments && version_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a winrm_arguments && winrm_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a winrm_config_arguments && winrm_config_arguments=( '--host=[Name the host for the config]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) _arguments -C ':command:->command' '*::options:->options' case $state in (command) _describe -t commands 'command' sub_commands return ;; (options) case $line[1] in autocomplete) __vagrant-autocomplete ;; box) __vagrant-box ;; cloud) _arguments -s -S : $cloud_arguments ;; destroy) _arguments -s -S : $destroy_arguments ;; global-status) _arguments -s -S : $global_status_arguments ;; halt) _arguments -s -S : $halt_arguments ;; help) _arguments -s -S : $help_arguments ;; init) _arguments -s -S : $init_arguments ;; login) _arguments -s -S : $login_arguments ;; package) _arguments -s -S : $package_arguments ;; plugin) __vagrant-plugin ;; port) _arguments -s -S : $port_arguments ;; powershell) _arguments -s -S : $powershell_arguments ;; provision) _arguments -s -S : $provision_arguments ;; push) _arguments -s -S : $push_arguments ;; rdp) _arguments -s -S : $rdp_arguments ;; reload) _arguments -s -S : $reload_arguments ;; resume) _arguments -s -S : $resume_arguments ;; snapshot) __vagrant-snapshot ;; ssh) _arguments -s -S : $ssh_arguments ;; ssh-config) _arguments -s -S : $ssh_config_arguments ;; status) _arguments -s -S : $status_arguments ;; suspend) _arguments -s -S : $suspend_arguments ;; up) _arguments -s -S : $up_arguments ;; upload) _arguments -s -S : $upload_arguments ;; validate) _arguments -s -S : $validate_arguments ;; version) _arguments -s -S : $version_arguments ;; winrm) _arguments -s -S : $winrm_arguments ;; winrm-config) _arguments -s -S : $winrm_config_arguments ;; esac ;; esac } function __vagrant-box () { local -a sub_commands && sub_commands=( 'add:add' 'list:list' 'outdated:outdated' 'prune:prune' 'remove:remove' 'repackage:repackage' 'update:update' ) local -a add_arguments && add_arguments=( '--insecure=[Do not validate SSL certificates]' '--cacert=[CA certificate for SSL download]' '--capath=[CA certificate directory for SSL download]' '--cert=[A client SSL cert, if needed]' '--location-trusted=[Trust Location header from HTTP redirects and use the same credentials for subsequent urls as for the initial one]' '--provider=[Provider the box should satisfy]' '--box-version=[Constrain version of the added box]' '--checksum=[Checksum for the box]' '--checksum-type=[Checksum type (md5, sha1, sha256)]' '--name=[Name of the box]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a list_arguments && list_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a outdated_arguments && outdated_arguments=( '--global=[Check all boxes installed]' '--insecure=[Do not validate SSL certificates]' '--cacert=[CA certificate for SSL download]' '--capath=[CA certificate directory for SSL download]' '--cert=[A client SSL cert, if needed]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a prune_arguments && prune_arguments=( '--name=[The specific box name to check for outdated versions.]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a remove_arguments && remove_arguments=( '--provider=[The specific provider type for the box to remove]' '--box-version=[The specific version of the box to remove]' '--all=[Remove all available versions of the box]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a repackage_arguments && repackage_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a update_arguments && update_arguments=( '--box=[__box flag.]' '--box=[Update a specific box]' '--provider=[Update box with specific provider]' '--insecure=[Do not validate SSL certificates]' '--cacert=[CA certificate for SSL download]' '--capath=[CA certificate directory for SSL download]' '--cert=[A client SSL cert, if needed]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) _arguments -C ':command:->command' '*::options:->options' case $state in (command) _describe -t commands 'command' sub_commands return ;; (options) case $line[1] in add) _arguments -s -S : $add_arguments ;; list) _arguments -s -S : $list_arguments ;; outdated) _arguments -s -S : $outdated_arguments ;; prune) _arguments -s -S : $prune_arguments ;; remove) _arguments -s -S : $remove_arguments ':feature:__box_list' ;; repackage) _arguments -s -S : $repackage_arguments ':feature:__box_list' ;; update) _arguments -s -S : $update_arguments ':feature:__box_list' ;; esac ;; esac } function __vagrant-snapshot () { local -a sub_commands && sub_commands=( 'delete:delete' 'list:list' 'pop:pop' 'push:push' 'restore:restore' 'save:save' ) local -a delete_arguments && delete_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a list_arguments && list_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a pop_arguments && pop_arguments=( '--(no-)provision=[Enable or disable provisioning]' '--provision-with=[Enable only certain provisioners, by type or by name.]' '--no-delete=[Dont delete the snapshot after the restore]' '--no-start=[Dont start the snapshot after the restore]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a push_arguments && push_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a restore_arguments && restore_arguments=( '--(no-)provision=[Enable or disable provisioning]' '--provision-with=[Enable only certain provisioners, by type or by name.]' '--no-start=[Dont start the snapshot after the restore]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a save_arguments && save_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) _arguments -C ':command:->command' '*::options:->options' case $state in (command) _describe -t commands 'command' sub_commands return ;; (options) case $line[1] in delete) _arguments -s -S : $delete_arguments ;; list) _arguments -s -S : $list_arguments ;; pop) _arguments -s -S : $pop_arguments ;; push) _arguments -s -S : $push_arguments ;; restore) _arguments -s -S : $restore_arguments ;; save) _arguments -s -S : $save_arguments ;; esac ;; esac } function __vagrant-plugin () { local -a sub_commands && sub_commands=( 'expunge:expunge' 'install:install' 'license:license' 'list:list' 'repair:repair' 'uninstall:uninstall' 'update:update' ) local -a expunge_arguments && expunge_arguments=( '--force=[Do not prompt for confirmation]' '--local=[Include plugins from local project for expunge]' '--local-only=[Only expunge local project plugins]' '--global-only=[Only expunge global plugins]' '--reinstall=[Reinstall current plugins after expunge]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a install_arguments && install_arguments=( '--entry-point=[The name of the entry point file for loading the plugin.]' '--plugin-clean-sources=[Remove all plugin sources defined so far (including defaults)]' '--plugin-source=[__plugin_source PLUGIN_SOURCE]' '--plugin-version=[__plugin_version PLUGIN_VERSION]' '--local=[Install plugin for local project only]' '--verbose=[Enable verbose output for plugin installation]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a license_arguments && license_arguments=( '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a list_arguments && list_arguments=( '--local=[Include local project plugins]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a repair_arguments && repair_arguments=( '--local=[Repair plugins in local project]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a uninstall_arguments && uninstall_arguments=( '--local=[Remove plugin from local project]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) local -a update_arguments && update_arguments=( '--local=[Update plugin in local project]' '--(no-)color=[Enable or disable color output]' '--machine-readable=[Enable machine readable output]' '--debug=[Enable debug output]' '--timestamp=[Enable timestamps on log output]' '--debug-timestamp=[Enable debug output with timestamps]' '--no-tty=[Enable non_interactive output]' ) _arguments -C ':command:->command' '*::options:->options' case $state in (command) _describe -t commands 'command' sub_commands return ;; (options) case $line[1] in expunge) _arguments -s -S : $expunge_arguments ;; install) _arguments -s -S : $install_arguments ;; license) _arguments -s -S : $license_arguments ;; list) _arguments -s -S : $list_arguments ;; repair) _arguments -s -S : $repair_arguments ':feature:__plugin_list' ;; uninstall) _arguments -s -S : $uninstall_arguments ':feature:__plugin_list' ;; update) _arguments -s -S : $update_arguments ':feature:__plugin_list' ;; esac ;; esac } ================================================ FILE: contrib/zsh/generate_zsh_completion.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'open3' HEAD = """#compdef _vagrant vagrant # ZSH completion for Vagrant # # To use this completion add this to ~/.zshrc # fpath=(/path/to/this/dir $fpath) # compinit # # For development reload the function after making changes # unfunction _vagrant && autoload -U _vagrant """ BOX_LIST_FUNCTION = """ __box_list () { _wanted application expl 'command' compadd $(command vagrant box list | awk '{print $1}' ) } """ PLUGIN_LIST_FUNCTION = """ __plugin_list () { _wanted application expl 'command' compadd $(command vagrant plugin list | awk '{print $1}') } """ ADD_FEATURE_FLAGS = ["remove", "repackage", "update", "repair", "uninstall"] VAGRANT_COMMAND = "vagrant" FLAG_REGEX = /--(\S)*/ CMDS_REGEX = /^(\s){1,}(\w)(\S)*/ def make_string_script_safe(s) s.gsub("[","(").gsub("]",")").gsub("-","_").gsub("'", "") end def remove_square_brakets(s) s.gsub("[","(").gsub("]",")") end def format_flags(group_name, flags) group_name = make_string_script_safe(group_name) opts_str = "local -a #{group_name} && #{group_name}=(\n" flags.each do |flag, desc| opts_str = opts_str + " '#{remove_square_brakets(flag)}=[#{make_string_script_safe(desc)}]'\n" end opts_str + ")" end def format_subcommand(group_name, cmds) opts_str = "local -a #{group_name} && #{group_name}=(\n" cmds.each do |cmd, desc| opts_str = opts_str + " '#{cmd}:#{desc}'\n" end opts_str + ")" end def format_case(group_name, cmds, cmd_list, feature_string) case_str = """case $state in (command) _describe -t commands 'command' #{group_name} return ;; (options) case $line[1] in """ cmds.each do |cmd, desc| if cmd_list.include?(cmd) case_append = """ #{cmd}) _arguments -s -S : $#{make_string_script_safe(cmd)}_arguments #{feature_string if ADD_FEATURE_FLAGS.include?(cmd)} ;; """ else case_append = """ #{cmd}) __vagrant-#{cmd} ;; """ end case_str = case_str + case_append end case_str = case_str + """esac ;; esac """ end def extract_flags(top_level_commands) flags = top_level_commands.map { |c| [c.match(FLAG_REGEX)[0], c.split(" ")[-1].strip] if c.strip.start_with?("--") }.compact end def extract_subcommand(top_level_commands) cmds = top_level_commands.map { |c| [c.match(CMDS_REGEX)[0].strip, c.split(" ")[-1].strip] if c.match(CMDS_REGEX) }.compact end def get_top_level_commands(root_command, cmd_list) stdout, stderr, status = Open3.capture3("vagrant #{root_command} -h") top_level_commands = stdout.split("\n") root_subcommand = extract_subcommand(top_level_commands) commands = format_subcommand("sub_commands", root_subcommand) if root_command == "box" feature_string = "':feature:__box_list'" elsif root_command == "plugin" feature_string = "':feature:__plugin_list'" else feature_string = "" end case_string = format_case("sub_commands", root_subcommand, cmd_list, feature_string) flags_def = "" root_subcommand.each do |cmd, desc| next if !cmd_list.include?(cmd) stdout, stderr, status = Open3.capture3("vagrant #{root_command} #{cmd} -h") cmd_help = stdout.split("\n") flags_def = flags_def + format_flags("#{cmd}_arguments", extract_flags(cmd_help)) + "\n\n" end return commands, flags_def, case_string end def format_script(root_command, subcommands, function_name) top_level_commands, top_level_args, state_case = get_top_level_commands(root_command, subcommands) script = """ function #{function_name} () { #{top_level_commands} #{top_level_args} _arguments -C ':command:->command' '*::options:->options' #{state_case} } """ end def generate_script subcommand_list = { "" => ["cloud", "destroy", "global-status", "halt", "help", "login", "init", "package", "port", "powershell", "provision", "push", "rdp", "reload", "resume", "ssh", "ssh-config", "status", "suspend", "up", "upload", "validate", "version", "winrm", "winrm-config"], "box" => ["add", "list", "outdated", "prune", "remove", "repackage", "update"], "snapshot" => ["delete", "list", "pop", "push", "restore", "save"], "plugin" => ["install", "expunge", "license", "list", "repair", "uninstall", "update"], } script = """#{HEAD} #{BOX_LIST_FUNCTION} #{PLUGIN_LIST_FUNCTION} """ subcommand_list.each do |cmd, opts| if cmd != "" function_name = "__vagrant-#{cmd}" else function_name = "_vagrant" end script = script + format_script(cmd, opts, function_name) end script end puts generate_script ================================================ FILE: ext/vagrant/vagrant_ssl/extconf.rb ================================================ #!/usr/bin/env ruby # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "mkmf" require "shellwords" # If extra flags are included via the environment, append them append_cflags(Shellwords.shellwords(ENV["CFLAGS"])) if ENV["CFLAGS"] append_cppflags(Shellwords.shellwords(ENV["CPPFLAGS"])) if ENV["CPPFLAGS"] append_ldflags(Shellwords.shellwords(ENV["LDFLAGS"])) if ENV["LDFLAGS"] if have_header("openssl/opensslv.h") append_ldflags(["-lssl", "-lcrypto"]) create_makefile("vagrant/vagrant_ssl") else # If the header file isn't found, just create a dummy # Makefile and stub the library to make it a noop File.open("Makefile", "wb") do |f| f.write(dummy_makefile(__dir__).join("\n")) end FileUtils.touch("vagrant_ssl.so") end ================================================ FILE: ext/vagrant/vagrant_ssl/vagrant_ssl.c ================================================ /* * Copyright IBM Corp. 2010, 2025 * SPDX-License-Identifier: BUSL-1.1 */ #include "vagrant_ssl.h" #if defined(_VAGRANT_SSL_PROVIDER_) static VALUE vagrant_ssl_load(VALUE self) { OSSL_PROVIDER *legacy; OSSL_PROVIDER *deflt; legacy = OSSL_PROVIDER_load(NULL, "legacy"); if(legacy == NULL) { rb_raise(rb_eStandardError, "Failed to load OpenSSL legacy provider"); return self; } deflt = OSSL_PROVIDER_load(NULL, "default"); if(deflt == NULL) { rb_raise(rb_eStandardError, "Failed to load OpenSSL default provider"); return self; } } void Init_vagrant_ssl(void) { VALUE vagrant; vagrant = rb_define_module("Vagrant"); rb_define_singleton_method(vagrant, "vagrant_ssl_load", vagrant_ssl_load, 0); } #else void Init_vagrant_ssl(void) {} #endif ================================================ FILE: ext/vagrant/vagrant_ssl/vagrant_ssl.h ================================================ /* * Copyright IBM Corp. 2010, 2025 * SPDX-License-Identifier: BUSL-1.1 */ #if !defined(_VAGRANT_SSL_H_) #define _VAGRANT_SSL_H_ #include #if OPENSSL_VERSION_NUMBER >= (3 << 28) #define _VAGRANT_SSL_PROVIDER_ #include #include #endif void Init_vagrant_ssl(void); #endif ================================================ FILE: keys/README.md ================================================ # Insecure Keypairs These keys are the "insecure" public/private keypair we offer to [base box creators](https://www.vagrantup.com/docs/boxes/base.html) for use in their base boxes so that vagrant installations can automatically SSH into the boxes. # Vagrant Keypairs There are currently two "insecure" public/private keypairs for Vagrant. One keypair was generated using the older RSA algorithm and the other keypair was generated using the more recent ED25519 algorithm. The `vagrant.pub` file includes the public key for both keypairs. It is important for box creators to include both keypairs as versions of Vagrant prior to 2.3.8 will only use the RSA private key. # Custom Keys If you're working with a team or company or with a custom box and you want more secure SSH, you should create your own keypair and configure the private key in the Vagrantfile with `config.ssh.private_key_path` ================================================ FILE: keys/vagrant ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzI w+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoP kcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2 hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NO Td0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcW yLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQIBIwKCAQEA4iqWPJXtzZA68mKd ELs4jJsdyky+ewdZeNds5tjcnHU5zUYE25K+ffJED9qUWICcLZDc81TGWjHyAqD1 Bw7XpgUwFgeUJwUlzQurAv+/ySnxiwuaGJfhFM1CaQHzfXphgVml+fZUvnJUTvzf TK2Lg6EdbUE9TarUlBf/xPfuEhMSlIE5keb/Zz3/LUlRg8yDqz5w+QWVJ4utnKnK iqwZN0mwpwU7YSyJhlT4YV1F3n4YjLswM5wJs2oqm0jssQu/BT0tyEXNDYBLEF4A sClaWuSJ2kjq7KhrrYXzagqhnSei9ODYFShJu8UWVec3Ihb5ZXlzO6vdNQ1J9Xsf 4m+2ywKBgQD6qFxx/Rv9CNN96l/4rb14HKirC2o/orApiHmHDsURs5rUKDx0f9iP cXN7S1uePXuJRK/5hsubaOCx3Owd2u9gD6Oq0CsMkE4CUSiJcYrMANtx54cGH7Rk EjFZxK8xAv1ldELEyxrFqkbE4BKd8QOt414qjvTGyAK+OLD3M2QdCQKBgQDtx8pN CAxR7yhHbIWT1AH66+XWN8bXq7l3RO/ukeaci98JfkbkxURZhtxV/HHuvUhnPLdX 3TwygPBYZFNo4pzVEhzWoTtnEtrFueKxyc3+LjZpuo+mBlQ6ORtfgkr9gBVphXZG YEzkCD3lVdl8L4cw9BVpKrJCs1c5taGjDgdInQKBgHm/fVvv96bJxc9x1tffXAcj 3OVdUN0UgXNCSaf/3A/phbeBQe9xS+3mpc4r6qvx+iy69mNBeNZ0xOitIjpjBo2+ dBEjSBwLk5q5tJqHmy/jKMJL4n9ROlx93XS+njxgibTvU6Fp9w+NOFD/HvxB3Tcz 6+jJF85D5BNAG3DBMKBjAoGBAOAxZvgsKN+JuENXsST7F89Tck2iTcQIT8g5rwWC P9Vt74yboe2kDT531w8+egz7nAmRBKNM751U/95P9t88EDacDI/Z2OwnuFQHCPDF llYOUI+SpLJ6/vURRbHSnnn8a/XG+nzedGH5JGqEJNQsz+xT2axM0/W/CRknmGaJ kda/AoGANWrLCz708y7VYgAtW2Uf1DPOIYMdvo6fxIB5i9ZfISgcJ/bbCUkFrhoH +vq/5CIWxCPp0f85R4qxxQ5ihxJ0YDQT9Jpx4TMss4PSavPaBH3RXow5Ohe+bYoQ NE5OgEXk2wVfZczCZpigBKbKZHNYcelXtTt/nP3rsCuGcM4h53s= -----END RSA PRIVATE KEY----- ================================================ FILE: keys/vagrant.key.ed25519 ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACDdWHcQaTZc8Q6nycsP0CqMNRfsLxvYVxqKosrHyTp+WAAAAJj2TBMT9kwT EwAAAAtzc2gtZWQyNTUxOQAAACDdWHcQaTZc8Q6nycsP0CqMNRfsLxvYVxqKosrHyTp+WA AAAEAveRHRHSCjIxbNKHDRzezD0U3R3UEEmS7R33fzvPQAD91YdxBpNlzxDqfJyw/QKow1 F+wvG9hXGoqiysfJOn5YAAAAEHNwb3hAdmFncmFudC1kZXYBAgMEBQ== -----END OPENSSH PRIVATE KEY----- ================================================ FILE: keys/vagrant.key.rsa ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzI w+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoP kcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2 hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NO Td0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcW yLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQIBIwKCAQEA4iqWPJXtzZA68mKd ELs4jJsdyky+ewdZeNds5tjcnHU5zUYE25K+ffJED9qUWICcLZDc81TGWjHyAqD1 Bw7XpgUwFgeUJwUlzQurAv+/ySnxiwuaGJfhFM1CaQHzfXphgVml+fZUvnJUTvzf TK2Lg6EdbUE9TarUlBf/xPfuEhMSlIE5keb/Zz3/LUlRg8yDqz5w+QWVJ4utnKnK iqwZN0mwpwU7YSyJhlT4YV1F3n4YjLswM5wJs2oqm0jssQu/BT0tyEXNDYBLEF4A sClaWuSJ2kjq7KhrrYXzagqhnSei9ODYFShJu8UWVec3Ihb5ZXlzO6vdNQ1J9Xsf 4m+2ywKBgQD6qFxx/Rv9CNN96l/4rb14HKirC2o/orApiHmHDsURs5rUKDx0f9iP cXN7S1uePXuJRK/5hsubaOCx3Owd2u9gD6Oq0CsMkE4CUSiJcYrMANtx54cGH7Rk EjFZxK8xAv1ldELEyxrFqkbE4BKd8QOt414qjvTGyAK+OLD3M2QdCQKBgQDtx8pN CAxR7yhHbIWT1AH66+XWN8bXq7l3RO/ukeaci98JfkbkxURZhtxV/HHuvUhnPLdX 3TwygPBYZFNo4pzVEhzWoTtnEtrFueKxyc3+LjZpuo+mBlQ6ORtfgkr9gBVphXZG YEzkCD3lVdl8L4cw9BVpKrJCs1c5taGjDgdInQKBgHm/fVvv96bJxc9x1tffXAcj 3OVdUN0UgXNCSaf/3A/phbeBQe9xS+3mpc4r6qvx+iy69mNBeNZ0xOitIjpjBo2+ dBEjSBwLk5q5tJqHmy/jKMJL4n9ROlx93XS+njxgibTvU6Fp9w+NOFD/HvxB3Tcz 6+jJF85D5BNAG3DBMKBjAoGBAOAxZvgsKN+JuENXsST7F89Tck2iTcQIT8g5rwWC P9Vt74yboe2kDT531w8+egz7nAmRBKNM751U/95P9t88EDacDI/Z2OwnuFQHCPDF llYOUI+SpLJ6/vURRbHSnnn8a/XG+nzedGH5JGqEJNQsz+xT2axM0/W/CRknmGaJ kda/AoGANWrLCz708y7VYgAtW2Uf1DPOIYMdvo6fxIB5i9ZfISgcJ/bbCUkFrhoH +vq/5CIWxCPp0f85R4qxxQ5ihxJ0YDQT9Jpx4TMss4PSavPaBH3RXow5Ohe+bYoQ NE5OgEXk2wVfZczCZpigBKbKZHNYcelXtTt/nP3rsCuGcM4h53s= -----END RSA PRIVATE KEY----- ================================================ FILE: keys/vagrant.pub ================================================ ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1YdxBpNlzxDqfJyw/QKow1F+wvG9hXGoqiysfJOn5Y vagrant insecure public key ================================================ FILE: keys/vagrant.pub.ed25519 ================================================ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN1YdxBpNlzxDqfJyw/QKow1F+wvG9hXGoqiysfJOn5Y vagrant insecure public key ================================================ FILE: keys/vagrant.pub.rsa ================================================ ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key ================================================ FILE: lib/vagrant/action/builder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action # Action builder which provides a nice DSL for building up # a middleware sequence for Vagrant actions. This code is based # heavily off of `Rack::Builder` and `ActionDispatch::MiddlewareStack` # in Rack and Rails, respectively. # # Usage # # Building an action sequence is very easy: # # app = Vagrant::Action::Builder.new.tap do |b| # b.use MiddlewareA # b.use MiddlewareB # end # # Vagrant::Action.run(app) # class Builder # Container for Action arguments MiddlewareArguments = Struct.new(:parameters, :block, :keywords, keyword_init: true) # Item within the stack StackItem = Struct.new(:middleware, :arguments, keyword_init: true) # This is the stack of middlewares added. This should NOT be used # directly. # # @return [Array] attr_reader :stack # Action Hooks allow plugin authors to inject their code wherever they # want in the action stack. The methods they get are: # # - prepend/append, which puts their middleware at the beginning or end # of the whole stack # - before/after, which attaches their middleware to an existing item # in the stack # # Applying Action Hooks properly gets tricky because the action stack # becomes deeply nested with things like Action::Builtin::Call and # Builder#use(other_builder). The way it breaks down is: # # - prepend/append hooks should be applied only at the top level stack, # so they run once at the beginning or end # - before/after hooks should be applied in every sub-builder, because # they will only actually attach if they find their target sibling # # We achieve this behavior by tracking if we are a "primary" Builder, and # only running prepend/append operations when we are. # # Note this difference only applies to action_hooks registered with # machine action names and not action_hooks which reference middleware # directly, which only support prepend/append and are handled in # #apply_dynamic_updates. # # @return [Boolean] true if this is a primary / top-level Builder # @see Vagrant::Action::PrimaryRunner # @see Vagrant::Action::Hook attr_accessor :primary # This is a shortcut for a middleware sequence with only one item # in it. For a description of the arguments and the documentation, please # see {#use} instead. # # @return [Builder] def self.build(middleware, *args, **keywords, &block) new.use(middleware, *args, **keywords, &block) end def initialize @stack = [] @logger = Log4r::Logger.new("vagrant::action::builder") end # Implement a custom copy that copies the stack variable over so that # we don't clobber that. def initialize_copy(original) super @stack = original.stack.dup end # Returns a mergeable version of the builder. If `use` is called with # the return value of this method, then the stack will merge, instead # of being treated as a separate single middleware. def flatten lambda do |env| self.call(env) end end # Adds a middleware class to the middleware stack. Any additional # args and a block, if given, are saved and passed to the initializer # of the middleware. # # @param [Class] middleware The middleware class def use(middleware, *args, **keywords, &block) item = StackItem.new( middleware: middleware, arguments: MiddlewareArguments.new( parameters: args, keywords: keywords, block: block ) ) if middleware.kind_of?(Builder) # Merge in the other builder's stack into our own self.stack.concat(middleware.stack) else self.stack << item end self end # Inserts a middleware at the given index or directly before the # given middleware object. def insert(idx_or_item, middleware, *args, **keywords, &block) item = StackItem.new( middleware: middleware, arguments: MiddlewareArguments.new( parameters: args, keywords: keywords, block: block ) ) if idx_or_item.is_a?(Integer) index = idx_or_item else index = self.index(idx_or_item) end raise "no such middleware to insert before: #{index.inspect}" unless index if middleware.kind_of?(Builder) middleware.stack.reverse.each do |stack_item| stack.insert(index, stack_item) end else stack.insert(index, item) end end alias_method :insert_before, :insert # Inserts a middleware after the given index or middleware object. def insert_after(idx_or_item, middleware, *args, **keywords, &block) if idx_or_item.is_a?(Integer) index = idx_or_item else index = self.index(idx_or_item) end raise "no such middleware to insert after: #{index.inspect}" unless index insert(index + 1, middleware, *args, &block) end # Replaces the given middleware object or index with the new # middleware. def replace(index, middleware, *args, **keywords, &block) if index.is_a?(Integer) delete(index) insert(index, middleware, *args, **keywords, &block) else insert_before(index, middleware, *args, **keywords, &block) delete(index) end end # Deletes the given middleware object or index def delete(index) index = self.index(index) unless index.is_a?(Integer) stack.delete_at(index) end # Runs the builder stack with the given environment. def call(env) to_app(env).call(env) end # Returns the numeric index for the given middleware object. # # @param [Object] object The item to find the index for # @return [Integer] def index(object) stack.each_with_index do |item, i| return i if item == object return i if item.middleware == object return i if item.middleware.respond_to?(:name) && item.middleware.name == object end nil end # Converts the builder stack to a runnable action sequence. # # @param [Hash] env The action environment hash # @return [Warden] A callable object def to_app(env) # Start with a duplicate of ourself which can # be modified builder = self.dup # Apply all dynamic modifications of the stack. This # will generate dynamic hooks for all actions within # the stack, load any triggers for action classes, and # apply them to the builder's stack builder.apply_dynamic_updates(env) # Now that the stack is fully expanded, apply any # action hooks that may be defined so they are on # the outermost locations of the stack builder.apply_action_name(env) # Wrap the middleware stack with the Warden to provide a consistent # and predictable behavior upon exceptions. Warden.new(builder.stack.dup, env) end # Find any action hooks or triggers which have been defined # for items within the stack. Update the stack with any # hooks or triggers found. # # @param [Hash] env Call environment # @return [Builder] self def apply_dynamic_updates(env) triggers = env[:triggers] # Use a Hook as a convenient interface for injecting # any applicable trigger actions within the stack machine_name = env[:machine].name if env[:machine] # Iterate over all items in the stack and apply new items # into the hook as they are found. Must be sure to dup the # stack here since we are modifying the stack in the loop. stack.dup.each do |item| hook = Hook.new action = item.first next if action.is_a?(Proc) # Start with adding any action triggers that may be defined if triggers && !triggers.find(action, :before, machine_name, :action).empty? hook.prepend(Vagrant::Action::Builtin::Trigger, action.name, triggers, :before, :action) end if triggers && !triggers.find(action, :after, machine_name, :action).empty? hook.append(Vagrant::Action::Builtin::Trigger, action.name, triggers, :after, :action) end # Next look for any hook triggers that may be defined against # the dynamically generated action class hooks if triggers && !triggers.find(action, :before, machine_name, :hook).empty? hook.prepend(Vagrant::Action::Builtin::Trigger, action.name, triggers, :before, :hook) end if triggers && !triggers.find(action, :after, machine_name, :hook).empty? hook.append(Vagrant::Action::Builtin::Trigger, action.name, triggers, :after, :hook) end # Finally load any registered hooks for dynamically generated # action class based hooks Vagrant.plugin("2").manager.find_action_hooks(action).each do |hook_proc| hook_proc.call(hook) end hook.apply(self, root: item) end # Apply the hook to ourself to update the stack self end # If action hooks have not already been set, this method # will perform three tasks: # 1. Load any hook triggers defined for the action_name # 2. Load any action_hooks defined from plugins # 3. Load any action triggers based on machine action called (not action classes) # # @param [Hash] env Call environment # @return [Builder] def apply_action_name(env) env[:builder_raw_applied] ||= [] return self if !env[:action_name] hook = Hook.new machine_name = env[:machine].name if env[:machine] # Start with loading any hook triggers if applicable if env[:triggers] if !env[:triggers].find(env[:action_name], :before, machine_name, :hook).empty? hook.prepend(Vagrant::Action::Builtin::Trigger, env[:action_name], env[:triggers], :before, :hook) end if !env[:triggers].find(env[:action_name], :after, machine_name, :hook).empty? hook.append(Vagrant::Action::Builtin::Trigger, env[:action_name], env[:triggers], :after, :hook) end end # Next we load up all the action hooks that plugins may # have defined action_hooks = Vagrant.plugin("2").manager.action_hooks(env[:action_name]) action_hooks.each do |hook_proc| hook_proc.call(hook) end # Finally load any action triggers defined. The action triggers # are the originally implemented trigger style. They run before # and after specific provider actions (like :up, :halt, etc) and # are different from true action triggers if env[:triggers] && !env[:builder_raw_applied].include?(env[:raw_action_name]) env[:builder_raw_applied] << env[:raw_action_name] if !env[:triggers].find(env[:raw_action_name], :before, machine_name, :action, all: true).empty? hook.prepend(Vagrant::Action::Builtin::Trigger, env[:raw_action_name], env[:triggers], :before, :action, all: true) end if !env[:triggers].find(env[:raw_action_name], :after, machine_name, :action, all: true).empty? # NOTE: These after triggers need to be delayed before running to # allow the rest of the call stack to complete before being # run. The delayed action is prepended to the stack (not appended) # to ensure it is called first, which results in it properly # waiting for everything to finish before itself completing. builder = self.class.build(Vagrant::Action::Builtin::Trigger, env[:raw_action_name], env[:triggers], :after, :action, all: true) hook.prepend(Vagrant::Action::Builtin::Delayed, builder) end end # If the hooks are empty, then there was nothing to apply and # we can just send ourself back return self if hook.empty? # Apply all the hooks to the new builder instance hook.apply(self, { # Only primary builders run prepend/append, otherwise nested builders # would duplicate hooks. See explanation at self#primary. no_prepend_or_append: !primary, }) self end end end end ================================================ FILE: lib/vagrant/action/builtin/box_add.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/sha1" require "log4r" require "pathname" require "uri" require "vagrant/box_metadata" require "vagrant/util/downloader" require "vagrant/util/file_checksum" require "vagrant/util/file_mutex" require "vagrant/util/platform" module Vagrant module Action module Builtin # This middleware will download a remote box and add it to the # given box collection. class BoxAdd # This is the size in bytes that if a file exceeds, is considered # to NOT be metadata. METADATA_SIZE_LIMIT = 20971520 # This is the amount of time to "resume" downloads if a partial box # file already exists. RESUME_DELAY = 24 * 60 * 60 def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::box_add") @parser = URI::RFC2396_Parser.new end def call(env) @download_interrupted = false unless env[:box_name].nil? begin if URI.parse(env[:box_name]).kind_of?(URI::HTTP) env[:ui].warn(I18n.t("vagrant.box_add_url_warn")) end rescue URI::InvalidURIError # do nothing end end url = Array(env[:box_url]).map do |u| u = u.gsub("\\", "/") if Util::Platform.windows? && u =~ /^[a-z]:/i # On Windows, we need to be careful about drive letters u = "file:///#{@parser.escape(u)}" end if u =~ /^[a-z0-9]+:.*$/i && !u.start_with?("file://") # This is not a file URL... carry on next u end # Expand the path and try to use that, if possible p = File.expand_path(@parser.unescape(u.gsub(/^file:\/\//, ""))) p = Util::Platform.cygwin_windows_path(p) next "file://#{@parser.escape(p.gsub("\\", "/"))}" if File.file?(p) u end # If we received a shorthand URL ("mitchellh/precise64"), # then expand it properly. expanded = false # Mark if only a single url entry was provided single_entry = url.size == 1 url = url.map do |url_entry| if url_entry =~ /^[^\/]+\/[^\/]+$/ && !File.file?(url_entry) server = Vagrant.server_url env[:box_server_url] raise Errors::BoxServerNotSet if !server expanded = true # If only a single entry, expand to both the API endpoint and # the direct shorthand endpoint. if single_entry url_entry = [ "#{server}/api/v2/vagrant/#{url_entry}", "#{server}/#{url_entry}" ] else url_entry = "#{server}/#{url_entry}" end end url_entry end.flatten # Call the hook to transform URLs into authenticated URLs. # In the case we don't have a plugin that does this, then it # will just return the same URLs. hook_env = env[:hook].call( :authenticate_box_url, box_urls: url.dup) authed_urls = hook_env[:box_urls] if !authed_urls || authed_urls.length != url.length raise "Bad box authentication hook, did not generate proper results." end # Test if any of our URLs point to metadata is_metadata_results = authed_urls.map do |u| begin metadata_url?(u, env) rescue Errors::DownloaderError => e e end end # If only a single entry was provided, and it was expanded, # inspect the metadata check results and extract the one that # was successful, with preference to the API endpoint if single_entry && expanded idx = is_metadata_results.index { |v| v === true } # If none of the urls were successful, set the index # as the last entry idx = is_metadata_results.size - 1 if idx.nil? # Now reset collections with single value is_metadata_results = [is_metadata_results[idx]] authed_urls = [authed_urls[idx]] url = [url[idx]] end if expanded && url.length == 1 is_error = is_metadata_results.find do |b| b.is_a?(Errors::DownloaderError) end if is_error raise Errors::BoxAddShortNotFound, error: is_error.extra_data[:message], name: env[:box_url], url: url end end is_error = is_metadata_results.find do |b| b.is_a?(Errors::DownloaderError) end if is_error raise Errors::BoxMetadataDownloadError, message: is_error.extra_data[:message] end is_metadata = is_metadata_results.any? { |b| b === true } if is_metadata && url.length > 1 raise Errors::BoxAddMetadataMultiURL, urls: url.join(", ") end if is_metadata url = [url.first, authed_urls.first] add_from_metadata(url, env, expanded) else add_direct(authed_urls, env) end @app.call(env) end # Adds a box file directly (no metadata component, versioning, # etc.) # # @param [Array] urls # @param [Hash] env def add_direct(urls, env) env[:ui].output(I18n.t("vagrant.box_adding_direct")) name = env[:box_name] if !name || name == "" raise Errors::BoxAddNameRequired end if env[:box_version] raise Errors::BoxAddDirectVersion end provider = env[:box_provider] provider = Array(provider) if provider box_add( urls, name, "0", provider, nil, env, checksum: env[:box_checksum], checksum_type: env[:box_checksum_type], architecture: env[:box_architecture] ) end # Adds a box given that the URL is a metadata document. # # @param [String | Array] url The URL of the metadata for # the box to add. If this is an array, then it must be a two-element # array where the first element is the original URL and the second # element is an authenticated URL. # @param [Hash] env # @param [Bool] expanded True if the metadata URL was expanded with # a Atlas server URL. def add_from_metadata(url, env, expanded) original_url = env[:box_url] architecture = env[:box_architecture] display_architecture = architecture == :auto ? Util::Platform.architecture : architecture provider = env[:box_provider] provider = Array(provider) if provider version = env[:box_version] authenticated_url = url if url.is_a?(Array) # We have both a normal URL and "authenticated" URL. Split # them up. authenticated_url = url[1] url = url[0] end display_original_url = Util::CredentialScrubber.scrub(Array(original_url).first) display_url = Util::CredentialScrubber.scrub(url) env[:ui].output(I18n.t( "vagrant.box_loading_metadata", name: display_original_url)) if original_url != url env[:ui].detail(I18n.t( "vagrant.box_expanding_url", url: display_url)) end metadata = nil begin metadata_path = download( authenticated_url, env, json: true, ui: false) return if @download_interrupted File.open(metadata_path) do |f| metadata = BoxMetadata.new(f, url: authenticated_url) end rescue Errors::DownloaderError => e raise if !expanded raise Errors::BoxAddShortNotFound, error: e.extra_data[:message], name: display_original_url, url: display_url ensure metadata_path.delete if metadata_path && metadata_path.file? end if env[:box_name] && metadata.name != env[:box_name] raise Errors::BoxAddNameMismatch, actual_name: metadata.name, requested_name: env[:box_name] end metadata_version = metadata.version( version || ">= 0", provider: provider, architecture: architecture, ) if !metadata_version if provider # If no version found that supports the provider, then the # box has no support for the provider if !metadata.version(">= 0", provider: provider) raise Errors::BoxAddNoMatchingProvider, name: metadata.name, requested: Array(provider).join(", "), url: display_url end # Get all versions that support the provider and architecture available_versions = metadata.versions( provider: provider, architecture: architecture ) # If no versions are found, then the box does not provide # support for the requested architecture using the requested # architecture if available_versions.empty? supported_providers = metadata.versions(architecture: architecture).map do |v| metadata.version(v).providers(architecture) end.compact.uniq.sort # If no providers are found, then the box does not # have any support for the requested architecture if supported_providers.empty? raise Errors::BoxAddNoArchitectureSupport, architecture: display_architecture, name: metadata.name, url: display_url end raise Errors::BoxAddNoMatchingArchitecture, provider: Array(provider).join(", "), architecture: display_architecture, name: metadata.name, url: display_url, supported_providers: supported_providers end raise Errors::BoxAddNoMatchingProviderVersion, constraints: version || ">= 0", provider: Array(provider).join(", "), architecture: display_architecture, name: metadata.name, url: display_url, versions: available_versions.reverse.join(", ") else # Report that no version can match the constraints requested # but show what versions are supported raise Errors::BoxAddNoMatchingVersion, constraints: version || ">= 0", name: metadata.name, url: display_url, versions: metadata.versions(architecture: architecture).reverse.join(", ") end end metadata_provider = nil if provider # If a provider was specified, make sure we get that specific # version. provider.each do |p| metadata_provider = metadata_version.provider(p, architecture) break if metadata_provider end elsif metadata_version.providers(architecture).length == 1 # If we have only one provider in the metadata, just use that # provider. metadata_provider = metadata_version.provider( metadata_version.providers(architecture).first, architecture) else providers = metadata_version.providers(architecture).sort choice = 0 options = providers.map do |p| choice += 1 "#{choice}) #{p}" end.join("\n") # We have more than one provider, ask the user what they want choice = env[:ui].ask(I18n.t( "vagrant.box_add_choose_provider", options: options) + " ", prefix: false) choice = choice.to_i if choice while !choice || choice <= 0 || choice > providers.length choice = env[:ui].ask(I18n.t( "vagrant.box_add_choose_provider_again") + " ", prefix: false) choice = choice.to_i if choice end metadata_provider = metadata_version.provider( providers[choice-1], architecture) end provider_url = metadata_provider.url if provider_url != authenticated_url # Authenticate the provider URL since we're using auth hook_env = env[:hook].call(:authenticate_box_url, box_urls: [provider_url]) authed_urls = hook_env[:box_urls] if !authed_urls || authed_urls.length != 1 raise "Bad box authentication hook, did not generate proper results." end provider_url = authed_urls[0] end # The architecture name used when adding the box should be # the value extracted from the metadata provider arch_name = metadata_provider.architecture # In the special case where the architecture name is "unknown" and # it is listed as the default architecture, unset the architecture # name so it is installed without architecture information if arch_name == "unknown" && metadata_provider.default_architecture arch_name = nil end box_add( [[provider_url, metadata_provider.url]], metadata.name, metadata_version.version, metadata_provider.name, url, env, checksum: metadata_provider.checksum, checksum_type: metadata_provider.checksum_type, architecture: arch_name, ) end protected # Shared helper to add a box once you know various details # about it. Shared between adding via metadata or by direct. # # @param [Array] urls # @param [String] name # @param [String] version # @param [String] provider # @param [Hash] env # @return [Box] def box_add(urls, name, version, provider, md_url, env, **opts) display_architecture = opts[:architecture] == :auto ? Util::Platform.architecture : opts[:architecture] env[:ui].output(I18n.t( "vagrant.box_add_with_version", name: name, version: version, providers: [ provider, display_architecture ? "(#{display_architecture})" : nil ].compact.join(" "))) # Verify the box we're adding doesn't already exist if provider && !env[:box_force] box = env[:box_collection].find( name, provider, version, opts[:architecture]) if box raise Errors::BoxAlreadyExists, name: name, provider: provider, version: version end end # Now we have a URL, we have to download this URL. box = nil begin box_url = nil urls.each do |url| show_url = nil if url.is_a?(Array) show_url = url[1] url = url[0] end begin box_url = download(url, env, show_url: show_url) break rescue Errors::DownloaderError => e # If we don't have multiple URLs, just raise the error raise if urls.length == 1 env[:ui].error(I18n.t( "vagrant.box_download_error", message: e.message)) box_url = nil end end if opts[:checksum] && opts[:checksum_type] if opts[:checksum].to_s.strip.empty? @logger.warn("Given checksum is empty, cannot validate checksum for box") elsif opts[:checksum_type].to_s.strip.empty? @logger.warn("Given checksum type is empty, cannot validate checksum for box") else env[:ui].detail(I18n.t("vagrant.actions.box.add.checksumming")) validate_checksum( opts[:checksum_type], opts[:checksum], box_url) end end # Add the box! box = env[:box_collection].add( box_url, name, version, force: env[:box_force], metadata_url: md_url, providers: provider, architecture: opts[:architecture] ) ensure # Make sure we delete the temporary file after we add it, # unless we were interrupted, in which case we keep it around # so we can resume the download later. if !@download_interrupted @logger.debug("Deleting temporary box: #{box_url}") begin box_url.delete if box_url rescue Errno::ENOENT # Not a big deal, the temp file may not actually exist end end end env[:ui].success(I18n.t( "vagrant.box_added", name: box.name, version: box.version, provider: [ provider, display_architecture ? "(#{display_architecture})" : nil ].compact.join(" "))) # Store the added box in the env for future middleware env[:box_added] = box box end # Returns the download options for the download. # # @return [Hash] def downloader(url, env, **opts) opts[:ui] = true if !opts.key?(:ui) temp_path = env[:tmp_path].join("box" + Digest::SHA1.hexdigest(url)) @logger.info("Downloading box: #{url} => #{temp_path}") if File.file?(url) || url !~ /^[a-z0-9]+:.*$/i @logger.info("URL is a file or protocol not found and assuming file.") file_path = File.expand_path(url) file_path = Util::Platform.cygwin_windows_path(file_path) file_path = file_path.gsub("\\", "/") file_path = "/#{file_path}" if !file_path.start_with?("/") url = "file://#{file_path}" end # If the temporary path exists, verify it is not too old. If its # too old, delete it first because the data may have changed. if temp_path.file? delete = false if env[:box_clean] @logger.info("Cleaning existing temp box file.") delete = true elsif temp_path.mtime.to_i < (Time.now.to_i - RESUME_DELAY) @logger.info("Existing temp file is too old. Removing.") delete = true end temp_path.unlink if delete end downloader_options = {} downloader_options[:ca_cert] = env[:box_download_ca_cert] downloader_options[:ca_path] = env[:box_download_ca_path] downloader_options[:continue] = true downloader_options[:insecure] = env[:box_download_insecure] downloader_options[:client_cert] = env[:box_download_client_cert] downloader_options[:headers] = ["Accept: application/json"] if opts[:json] downloader_options[:ui] = env[:ui] if opts[:ui] downloader_options[:location_trusted] = env[:box_download_location_trusted] downloader_options[:disable_ssl_revoke_best_effort] = env[:box_download_disable_ssl_revoke_best_effort] downloader_options[:box_extra_download_options] = env[:box_extra_download_options] d = Util::Downloader.new(url, temp_path, downloader_options) env[:hook].call(:authenticate_box_downloader, downloader: d) d end def download(url, env, **opts) opts[:ui] = true if !opts.key?(:ui) d = downloader(url, env, **opts) env[:hook].call(:authenticate_box_downloader, downloader: d) # Download the box to a temporary path. We store the temporary # path as an instance variable so that the `#recover` method can # access it. if opts[:ui] show_url = opts[:show_url] show_url ||= url display_url = Util::CredentialScrubber.scrub(show_url) translation = "vagrant.box_downloading" # Adjust status message when 'downloading' a local box. if show_url.start_with?("file://") translation = "vagrant.box_unpacking" end env[:ui].detail(I18n.t( translation, url: display_url)) if File.file?(d.destination) env[:ui].info(I18n.t("vagrant.actions.box.download.resuming")) end end begin mutex_path = d.destination + ".lock" Util::FileMutex.new(mutex_path).with_lock do begin d.download! rescue Errors::DownloaderInterrupted # The downloader was interrupted, so just return, because that # means we were interrupted as well. @download_interrupted = true env[:ui].info(I18n.t("vagrant.actions.box.download.interrupted")) end end rescue Errors::VagrantLocked raise Errors::DownloadAlreadyInProgress, dest_path: d.destination, lock_file_path: mutex_path end Pathname.new(d.destination) end # Tests whether the given URL points to a metadata file or a # box file without completely downloading the file. # # @param [String] url # @return [Boolean] true if metadata def metadata_url?(url, env) d = downloader(url, env, json: true, ui: false) env[:hook].call(:authenticate_box_downloader, downloader: d) # If we're downloading a file, cURL just returns no # content-type (makes sense), so we just test if it is JSON # by trying to parse JSON! uri = URI.parse(d.source) if uri.scheme == "file" url = uri.path url ||= uri.opaque #7570 Strip leading slash left in front of drive letter by uri.path Util::Platform.windows? && url.gsub!(/^\/([a-zA-Z]:)/, '\1') url = @parser.unescape(url) begin File.open(url, "r") do |f| if f.size > METADATA_SIZE_LIMIT # Quit early, don't try to parse the JSON of gigabytes # of box files... return false end BoxMetadata.new(f, url: url) end return true rescue Errors::BoxMetadataMalformed return false rescue Errno::EINVAL # Actually not sure what causes this, but its always # in a case that isn't true. return false rescue Errno::EISDIR return false rescue Errno::ENOENT return false end end # If this isn't HTTP, then don't do the HEAD request if !uri.scheme.downcase.start_with?("http") @logger.info("not checking metadata since box URI isn't HTTP") return false end output = d.head match = output.scan(/^Content-Type: (.+?)$/i).last return false if !match !!(match.last.chomp =~ /application\/json/) end def validate_checksum(checksum_type, _checksum, path) checksum = _checksum.strip() @logger.info("Validating checksum with #{checksum_type}") @logger.info("Expected checksum: #{checksum}") _actual = FileChecksum.new(path, checksum_type).checksum actual = _actual.strip() @logger.info("Actual checksum: #{actual}") if actual.casecmp(checksum) != 0 raise Errors::BoxChecksumMismatch, actual: actual, expected: checksum end end end end end end ================================================ FILE: lib/vagrant/action/builtin/box_check_outdated.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Action module Builtin # This middleware checks if there are outdated boxes. By default, # it only checks locally, but if `box_outdated_refresh` is set, it # will refresh the metadata associated with a box. class BoxCheckOutdated def initialize(app, env) @app = app @logger = Log4r::Logger.new( "vagrant::action::builtin::box_check_outdated") end def call(env) machine = env[:machine] if !env[:box_outdated_force] if !machine.config.vm.box_check_update @logger.debug( "Not checking for update: no force and no update config") return @app.call(env) end end if !machine.box # We don't have a box. Just ignore, we can't check for # outdated... @logger.warn("Not checking for update, no box") return @app.call(env) end box = machine.box if box.version == "0" && !box.metadata_url return @app.call(env) end constraints = machine.config.vm.box_version # Have download options specified in the environment override # options specified for the machine. download_options = { automatic_check: !env[:box_outdated_force], 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, disable_ssl_revoke_best_effort: env.fetch(:box_download_disable_ssl_revoke_best_effort, machine.config.vm.box_download_disable_ssl_revoke_best_effort), box_extra_download_options: env[:box_extra_download_options] || machine.config.vm.box_extra_download_options, } env[:ui].output(I18n.t( "vagrant.box_outdated_checking_with_refresh", name: box.name, version: box.version)) update = nil begin update = box.has_update?(constraints, download_options: download_options) rescue Errors::BoxMetadataDownloadError => e env[:ui].warn(I18n.t( "vagrant.box_outdated_metadata_download_error", message: e.extra_data[:message])) rescue Errors::BoxMetadataMalformed => e @logger.warn(e.to_s) env[:ui].warn(I18n.t("vagrant.box_malformed_continue_on_update")) rescue Errors::VagrantError => e raise if !env[:box_outdated_ignore_errors] env[:ui].detail(I18n.t( "vagrant.box_outdated_metadata_error_single", message: e.message)) end env[:box_outdated] = update != nil local_update = check_outdated_local(env) if update && (local_update.nil? || (local_update.version < update[1].version)) env[:ui].warn(I18n.t( "vagrant.box_outdated_single", name: update[0].name, provider: box.provider, current: box.version, latest: update[1].version)) elsif local_update env[:ui].warn(I18n.t( "vagrant.box_outdated_local", name: local_update.name, old: box.version, new: local_update.version)) env[:box_outdated] = true else env[:box_outdated] = false end @app.call(env) end def check_outdated_local(env) machine = env[:machine] # Make sure we respect the constraints set within the Vagrantfile version = machine.config.vm.box_version version += ", " if version version ||= "" version += "> #{machine.box.version}" env[:box_collection].find( machine.box.name, machine.box.provider, version) end end end end end ================================================ FILE: lib/vagrant/action/builtin/box_remove.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Action module Builtin # This middleware will remove a box for a given provider. class BoxRemove def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::box_remove") end def call(env) box_name = env[:box_name] box_architecture = env[:box_architecture] if env[:box_architecture] box_provider = env[:box_provider].to_sym if env[:box_provider] box_version = env[:box_version] box_remove_all_versions = env[:box_remove_all_versions] box_remove_all_providers = env[:box_remove_all_providers] box_remove_all_architectures = env[:box_remove_all_architectures] box_info = Util::HashWithIndifferentAccess.new env[:box_collection].all.each do |name, version, provider, architecture| next if name != box_name box_info[version] ||= Util::HashWithIndifferentAccess.new box_info[version][provider] ||= [] box_info[version][provider] << architecture end # If there's no box info, then the box doesn't exist here if box_info.empty? raise Errors::BoxRemoveNotFound, name: box_name end # Filtering only matters if not removing all versions if !box_remove_all_versions # If no version was provided, and not removing all versions, # only allow one version to proceed if !box_version && box_info.size > 1 raise Errors::BoxRemoveMultiVersion, name: box_name, versions: box_info.keys.sort.map { |k| " * #{k}" }.join("\n") end # If a version was provided, make sure it exists if box_version if !box_info.keys.include?(box_version) raise Errors::BoxRemoveVersionNotFound, name: box_name, version: box_version, versions: box_info.keys.sort.map { |k| " * #{k}" }.join("\n") else box_info.delete_if { |k, _| k != box_version } end end # Only a single version remains box_version = box_info.keys.first # Further filtering only matters if not removing all providers if !box_remove_all_providers # If no provider was given, check if there are more # than a single provider for the version if !box_provider && box_info.values.first.size > 1 raise Errors::BoxRemoveMultiProvider, name: box_name, version: box_version, providers: box_info.values.first.keys.map(&:to_s).sort.join(", ") end # If a provider was given, check the version has it if box_provider if !box_info.values.first.key?(box_provider) raise Errors::BoxRemoveProviderNotFound, name: box_name, version: box_version, provider: box_provider.to_s, providers: box_info.values.first.keys.map(&:to_s).sort.join(", ") else box_info.values.first.delete_if { |k, _| k.to_s != box_provider.to_s } end end # Only a single provider remains box_provider = box_info.values.first.keys.first # Further filtering only matters if not removing all architectures if !box_remove_all_architectures # If no architecture was given, check if there are more # than a single architecture for the provider in version if !box_architecture && box_info.values.first.values.first.size > 1 raise Errors::BoxRemoveMultiArchitecture, name: box_name, version: box_version, provider: box_provider.to_s, architectures: box_info.values.first.values.first.sort.join(", ") end # If architecture was given, check the provider for the version has it if box_architecture if !box_info.values.first.values.first.include?(box_architecture) raise Errors::BoxRemoveArchitectureNotFound, name: box_name, version: box_version, provider: box_provider.to_s, architecture: box_architecture, architectures: box_info.values.first.values.first.sort.join(", ") else box_info.values.first.values.first.delete_if { |v| v != box_architecture } end end end end end box_info.each do |version, provider_info| provider_info.each do |provider, architecture_info| provider = provider.to_sym architecture_info.each do |architecture| box = env[:box_collection].find( box_name, provider, version, architecture ) # 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") force_key = :force_confirm_box_remove message = I18n.t( "vagrant.commands.box.remove_in_use_query", name: box.name, architecture: box.architecture, 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 # Keep used boxes, even if "force" is applied keep_used_boxes = env[:keep_used_boxes] result = env[:action_runner].run(stack, env) if !result[:result] || keep_used_boxes # They said "no", so continue with the next box next end end env[:ui].info(I18n.t("vagrant.commands.box.removing", name: box.name, architecture: box.architecture, provider: box.provider, version: box.version)) box.destroy! env[:box_collection].clean(box.name) # Passes on the removed box to the rest of the middleware chain env[:box_removed] = box end end end @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/box_update.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Action module Builtin # This middleware updates a specific box if there are updates available. class BoxUpdate def initialize(app, env) @app = app @logger = Log4r::Logger.new( "vagrant::action::builtin::box_update") end def call(env) machine = env[:machine] end end end end end ================================================ FILE: lib/vagrant/action/builtin/call.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin # This middleware class allows a sort of "conditional" run within # a single middleware sequence. It takes another middleware runnable, # runs it with the same environment, then yields the resulting env to a block, # allowing that block to determine the next course of action in the # middleware sequence. # # The first argument to this middleware sequence is anywhere middleware # runnable, whether it be a class, lambda, or something else that # responds to `call`. This middleware runnable is run with the same # environment as this class. # # After running, {Call} takes the environment and yields it to a block # given to initialize the class, along with an instance of {Builder}. # The result is used to build up a new sequence on the given builder. # This builder is then run. class Call # For documentation, read the description of the {Call} class. # # @param [Object] callable A valid middleware runnable object. This # can be a class, a lambda, or an object that responds to `call`. # @yield [result, builder] This block is expected to build on `builder` # which is the next middleware sequence that will be run. def initialize(app, env, callable, *callable_args, &block) raise ArgumentError, "A block must be given to Call" if !block @app = app @callable = callable @callable_args = callable_args @block = block @child_app = nil end def call(env) runner = Runner.new # Build the callable that we'll run callable = Builder.build(@callable, *@callable_args) # Run our callable with our environment new_env = runner.run(callable, env) # Build our new builder based on the result builder = Builder.new @block.call(new_env, builder) # Append our own app onto the builder so we slide the new # stack into our own chain... builder.use @app @child_app = builder.to_app(new_env) final_env = runner.run(@child_app, new_env) # Merge the environment into our original environment env.merge!(final_env) end def recover(env) # Call back into our compiled application and recover it. @child_app.recover(env) if @child_app end end end end end ================================================ FILE: lib/vagrant/action/builtin/cleanup_disks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" module Vagrant module Action module Builtin class CleanupDisks # Removes any attached disks no longer defined in a Vagrantfile config def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::disk") end def call(env) machine = env[:machine] defined_disks = get_disks(machine, env) # Call into providers machine implementation for disk management disk_meta_file = read_disk_metadata(machine) if !disk_meta_file.empty? if machine.provider.capability?(:cleanup_disks) machine.provider.capability(:cleanup_disks, defined_disks, disk_meta_file) else env[:ui].warn(I18n.t("vagrant.actions.disk.provider_unsupported", provider: machine.provider_name)) end end # Continue On @app.call(env) end def read_disk_metadata(machine) meta_file = machine.data_dir.join("disk_meta") if File.file?(meta_file) disk_meta = JSON.parse(meta_file.read) else @logger.info("No previous disk_meta file defined for guest #{machine.name}") disk_meta = {} end return disk_meta end def get_disks(machine, env) return @_disks if @_disks @_disks = [] @_disks = machine.config.vm.disks @_disks end end end end end ================================================ FILE: lib/vagrant/action/builtin/cloud_init_setup.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/mime' require 'tmpdir' module Vagrant module Action module Builtin class CloudInitSetup TEMP_PREFIX = "vagrant-cloud-init-iso-temp-".freeze def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::cloudinit::setup") end def call(env) catch(:complete) do machine = env[:machine] # 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 user_data_configs = machine.config.vm.cloud_init_configs.select { |c| c.type == :user_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 @app.call(env) end # @param [Vagrant::Machine] machine # @param [Vagrant::Environment] env # @param [Array<#VagrantPlugins::Kernel_V2::VagrantConfigCloudInit>] user_data_cfgs # @return [Vagrant::Util::Mime::MultiPart] user_data def setup_user_data(machine, env, user_data_cfgs) machine.ui.info(I18n.t("vagrant.actions.vm.cloud_init_user_data_setup")) text_cfgs = user_data_cfgs.map { |cfg| read_text_cfg(machine, cfg) } user_data = generate_cfg_msg(machine, text_cfgs) user_data end # Reads an individual cloud_init config and stores its contents and the # content_type as a MIME text # # @param [Vagrant::Machine] machine # @param [VagrantPlugins::Kernel_V2::VagrantConfigCloudInit] cfg # @return [Vagrant::Util::Mime::Entity] text_msg def read_text_cfg(machine, cfg) if cfg.path text = File.read(Pathname.new(cfg.path).expand_path(machine.env.root_path)) else text = cfg.inline end text_msg = Vagrant::Util::Mime::Entity.new(text, cfg.content_type) text_msg.disposition = "attachment; filename=\"#{File.basename(cfg.content_disposition_filename).gsub('"', '\"')}\"" if cfg.content_disposition_filename text_msg end # Combines all known cloud_init configs into a multipart mixed MIME text # message # # @param [Vagrant::Machine] machine # @param [Array] text_msg - One or more text configs # @return [Vagrant::Util::Mime::Multipart] msg def generate_cfg_msg(machine, text_cfgs) msg = Vagrant::Util::Mime::Multipart.new msg.headers["MIME-Version"] = "1.0" text_cfgs.each do |c| msg.add(c) end msg end # Writes the contents of the guests cloud_init config to a tmp # dir and passes that source directory along to the host cap to be # written to an iso # # @param [Vagrant::Machine] machine # @param [Vagrant::Util::Mime::Multipart] user_data # @param [Hash] meta_data def write_cfg_iso(machine, env, user_data, meta_data) 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 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) } 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 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 # to the guest later # # @param [Vagrant::Machine] machine # @param [Vagrant::Environment] env # @param [String] iso_path def attach_disk_config(machine, env, iso_path) @logger.info("Adding cloud_init iso '#{iso_path}' to disk config") machine.config.vm.disk :dvd, file: iso_path, name: "vagrant-cloud_init-disk" machine.config.vm.disks.each { |d| d.finalize! if d.type == :dvd && d.file == iso_path } end end end end end ================================================ FILE: lib/vagrant/action/builtin/cloud_init_wait.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Action module Builtin class CloudInitWait def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::cloudinitwait") end def call(env) 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 @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 end end end ================================================ FILE: lib/vagrant/action/builtin/config_validate.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/template_renderer" module Vagrant module Action module Builtin # This class validates the configuration and raises an exception # if there are any validation errors. class ConfigValidate def initialize(app, env) @app = app end def call(env) if !env.key?(:config_validate) || env[:config_validate] errors = env[:machine].config.validate(env[:machine], env[:ignore_provider]) if errors && !errors.empty? raise Errors::ConfigInvalid, errors: Util::TemplateRenderer.render( "config/validation_failed", errors: errors) end end @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/confirm.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin # This class asks the user to confirm some sort of question with # a "Y/N" question. The only parameter is the text to ask the user. # The result is placed in `env[:result]` so that it can be used # with the {Call} class. class Confirm # For documentation, read the description of the {Confirm} class. # # @param [String] message The message to ask the user. # @param [Symbol] force_key The key that if present and true in # the environment hash will skip the confirmation question. def initialize(app, env, message, force_key=nil, **opts) @app = app @message = message @force_key = force_key @allowed = opts[:allowed] end def call(env) choice = nil # If we have a force key set and we're forcing, then set # the result to "Y" choice = "Y" if @force_key && env[@force_key] if !choice while true # If we haven't chosen yes, then ask the user via TTY choice = env[:ui].ask(@message) # If we don't have an allowed set just exit break if !@allowed break if @allowed.include?(choice) end end # The result is only true if the user said "Y" env[:result] = choice && choice.upcase == "Y" env["#{@force_key}_result".to_sym] = env[:result] @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/delayed.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin # This class is used to delay execution until the end of # a configured stack class Delayed # @param [Object] callable The object to call (must respond to #call) def initialize(app, env, callable) if !callable.respond_to?(:call) raise TypeError, "Callable argument is expected to respond to `#call`" end @app = app @env = env @callable = callable end def call(env) # Allow the rest of the call stack to execute @app.call(env) # Now call our delayed stack @callable.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/destroy_confirm.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "confirm" module Vagrant module Action module Builtin # This class asks the user to confirm the destruction of a machine # that Vagrant manages. This is provided as a built-in on top of # {Confirm} because it sets up the proper keys and such so that # `vagrant destroy -f` works properly. class DestroyConfirm < Confirm def initialize(app, env) force_key = :force_confirm_destroy message = I18n.t("vagrant.commands.destroy.confirmation", name: env[:machine].name) super(app, env, message, force_key, allowed: ["y", "n", "Y", "N"]) end end end end end ================================================ FILE: lib/vagrant/action/builtin/disk.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" module Vagrant module Action module Builtin class Disk def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::disk") end def call(env) machine = env[:machine] defined_disks = get_disks(machine, env) # Call into providers machine implementation for disk management configured_disks = {} if !defined_disks.empty? if machine.provider.capability?(:configure_disks) configured_disks = machine.provider.capability(:configure_disks, defined_disks) else env[:ui].warn(I18n.t("vagrant.actions.disk.provider_unsupported", provider: machine.provider_name)) end end # 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) end def write_disk_metadata(machine, current_disks) meta_file = machine.data_dir.join("disk_meta") @logger.debug("Writing disk metadata file to #{meta_file}") File.open(meta_file.to_s, "w+") do |file| file.write(JSON.dump(current_disks)) end end def get_disks(machine, env) return @_disks if @_disks @_disks = [] @_disks = machine.config.vm.disks @_disks end end end end end ================================================ FILE: lib/vagrant/action/builtin/env_set.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin # This middleware class allows you to modify the environment hash # in the middle of a middleware sequence. The new environmental data # will take affect at this stage in the middleware and will persist # through. class EnvSet def initialize(app, env, new_env=nil) @app = app @new_env = new_env || {} end def call(env) # Merge in the new data env.merge!(@new_env) # Carry on @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/graceful_halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "timeout" module Vagrant module Action module Builtin # This middleware class will attempt to perform a graceful shutdown # of the machine using the guest implementation. This middleware is # compatible with the {Call} middleware so you can branch based on # the result, which is true if the halt succeeded and false otherwise. class GracefulHalt # Note: Any of the arguments can be arrays as well. # # @param [Symbol] target_state The target state ID that means that # the machine was properly shut down. # @param [Symbol] source_state The source state ID that the machine # must be in to be shut down. def initialize(app, env, target_state, source_state=nil) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::graceful_halt") @source_state = source_state @target_state = target_state end def call(env) graceful = true graceful = !env[:force_halt] if env.key?(:force_halt) # By default, we didn't succeed. env[:result] = false if graceful && @source_state @logger.info("Verifying source state of machine: #{@source_state.inspect}") # If we're not in the proper source state, then we don't # attempt to halt the machine current_state = env[:machine].state.id if current_state != @source_state @logger.info("Invalid source state, not halting: #{current_state}") graceful = false end end # Only attempt to perform graceful shutdown under certain cases # checked above. if graceful env[:ui].output(I18n.t("vagrant.actions.vm.halt.graceful")) begin env[:machine].guest.capability(:halt) @logger.debug("Waiting for target graceful halt state: #{@target_state}") begin Timeout.timeout(env[:machine].config.vm.graceful_halt_timeout) do while env[:machine].state.id != @target_state sleep 1 end end rescue Timeout::Error # Don't worry about it, we catch the case later. end rescue Errors::GuestCapabilityNotFound # This happens if insert_public_key is called on a guest that # doesn't support it. This will block a destroy so we let it go. rescue Errors::MachineGuestNotReady env[:ui].detail(I18n.t("vagrant.actions.vm.halt.guest_not_ready")) end # The result of this matters on whether we reached our # proper target state or not. env[:result] = env[:machine].state.id == @target_state if env[:result] @logger.info("Gracefully halted.") else @logger.info("Graceful halt failed.") end end @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/handle_box.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "thread" require "log4r" module Vagrant module Action module Builtin # This built-in middleware handles the `box` setting by verifying # the box is already installed, downloading the box if it isn't, # updating the box if it is requested, etc. class HandleBox @@big_lock = Mutex.new @@small_locks = Hash.new { |h,k| h[k] = Mutex.new } def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::handle_box") end def call(env) machine = env[:machine] if !machine.config.vm.box || machine.config.vm.box.to_s.empty? @logger.info("Skipping HandleBox because no box is set") return @app.call(env) end # Acquire a lock for this box to handle multi-threaded # environments. lock = nil @@big_lock.synchronize do lock = @@small_locks[machine.config.vm.box] end box_updated = false lock.synchronize do if machine.box @logger.info("Machine already has box. HandleBox will not run.") next end handle_box(env) box_updated = true end if box_updated # Reload the environment and set the VM to be the new loaded VM. new_machine = machine.vagrantfile.machine( machine.name, machine.provider_name, machine.env.boxes, machine.data_dir, machine.env) env[:machine].box = new_machine.box env[:machine].config = new_machine.config env[:machine].provider_config = new_machine.provider_config end @app.call(env) end def handle_box(env) machine = env[:machine] # Determine the set of formats that this box can be in box_download_ca_cert = machine.config.vm.box_download_ca_cert box_download_ca_path = machine.config.vm.box_download_ca_path box_download_client_cert = machine.config.vm.box_download_client_cert box_download_insecure = machine.config.vm.box_download_insecure box_download_checksum_type = machine.config.vm.box_download_checksum_type box_download_checksum = machine.config.vm.box_download_checksum box_download_location_trusted = machine.config.vm.box_download_location_trusted box_download_disable_ssl_revoke_best_effort = machine.config.vm.box_download_disable_ssl_revoke_best_effort box_extra_download_options = machine.config.vm.box_extra_download_options box_formats = machine.provider_options[:box_format] || machine.provider_name version_ui = machine.config.vm.box_version version_ui ||= ">= 0" env[:ui].output(I18n.t( "vagrant.box_auto_adding", name: machine.config.vm.box)) env[:ui].detail("Box Provider: #{Array(box_formats).join(", ")}") env[:ui].detail("Box Version: #{version_ui}") begin env[:action_runner].run(Vagrant::Action.action_box_add, env.merge({ box_name: machine.config.vm.box, box_url: machine.config.vm.box_url || machine.config.vm.box, box_architecture: machine.config.vm.box_architecture, box_server_url: machine.config.vm.box_server_url, box_provider: box_formats, box_version: machine.config.vm.box_version, box_download_client_cert: box_download_client_cert, box_download_ca_cert: box_download_ca_cert, box_download_ca_path: box_download_ca_path, box_download_insecure: box_download_insecure, box_checksum_type: box_download_checksum_type, box_checksum: box_download_checksum, box_download_location_trusted: box_download_location_trusted, box_download_disable_ssl_revoke_best_effort: box_download_disable_ssl_revoke_best_effort, box_extra_download_options: box_extra_download_options, })) rescue Errors::BoxAlreadyExists # Just ignore this, since it means the next part will succeed! # This can happen in a multi-threaded environment. end end end end end end ================================================ FILE: lib/vagrant/action/builtin/handle_box_url.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin class HandleBoxUrl < HandleBox def call(env) env[:ui].warn("HandleBoxUrl middleware is deprecated. Use HandleBox instead.") env[:ui].warn("This is a bug with the provider. Please contact the creator") env[:ui].warn("of the provider you use to fix this.") super end end end end end ================================================ FILE: lib/vagrant/action/builtin/handle_forwarded_port_collisions.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "set" require "log4r" require "socket" require "vagrant/util/is_port_open" require "vagrant/util/ipv4_interfaces" module Vagrant module Action module Builtin # This middleware class will detect and handle collisions with # forwarded ports, whether that means raising an error or repairing # them automatically. # # Parameters it takes from the environment hash: # # * `:port_collision_repair` - If true, it will attempt to repair # port collisions. If false, it will raise an exception when # there is a collision. # # * `:port_collision_extra_in_use` - An array of ports that are # considered in use. # # * `:port_collision_remap` - A hash remapping certain host ports # to other host ports. # class HandleForwardedPortCollisions include Util::IsPortOpen include Util::IPv4Interfaces def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::handle_port_collisions") end def call(env) @leased = [] @machine = env[:machine] # Acquire a process-level lock so that we don't choose a port # that someone else also chose. begin env[:machine].env.lock("fpcollision") do handle(env) end rescue Errors::EnvironmentLockedError sleep 1 retry end @app.call(env) # Always run the recover method so that we release leases recover(env) end def recover(env) lease_release end protected def handle(env) @logger.info("Detecting any forwarded port collisions...") # Get the extra ports we consider in use extra_in_use = env[:port_collision_extra_in_use] || {} # If extras are provided as an Array (previous behavior) convert # to Hash as expected for IP aliasing support if extra_in_use.is_a?(Array) extra_in_use = Hash[extra_in_use.map{|port| [port, Set.new(["*"])]}] end # Get the remap remap = env[:port_collision_remap] || {} # Determine the handler we'll use if we have any port collisions repair = !!env[:port_collision_repair] # The method we'll use to check if a port is open. port_checker = env[:port_collision_port_check] port_checker ||= method(:port_check) # Log out some of our parameters @logger.debug("Extra in use: #{extra_in_use.inspect}") @logger.debug("Remap: #{remap.inspect}") @logger.debug("Repair: #{repair.inspect}") # Determine a list of usable ports for repair usable_ports = Set.new(env[:machine].config.vm.usable_port_range) usable_ports.subtract(extra_in_use.keys) # Pass one, remove all defined host ports from usable ports with_forwarded_ports(env) do |options| usable_ports.delete(options[:host]) end # Pass two, detect/handle any collisions with_forwarded_ports(env) do |options| guest_port = options[:guest] host_port = options[:host] host_ip = options[:host_ip] if options[:disabled] @logger.debug("Skipping disabled port #{host_port}.") next end if options[:protocol] && options[:protocol] != "tcp" @logger.debug("Skipping #{host_port} because UDP protocol.") next end if remap[host_port] remap_port = remap[host_port] @logger.debug("Remap port override: #{host_port} => #{remap_port}") host_port = remap_port end # If the port is open (listening for TCP connections) in_use = is_forwarded_already(extra_in_use, host_port, host_ip) || call_port_checker(port_checker, host_ip, host_port) || lease_check(host_ip, host_port) if in_use if !repair || !options[:auto_correct] raise Errors::ForwardPortCollision, guest_port: guest_port.to_s, host_port: host_port.to_s end @logger.info("Attempting to repair FP collision: #{host_port}") repaired_port = nil while !usable_ports.empty? # Attempt to repair the forwarded port repaired_port = usable_ports.to_a.sort[0] usable_ports.delete(repaired_port) # If the port is in use, then we can't use this either... in_use = is_forwarded_already(extra_in_use, repaired_port, host_ip) || call_port_checker(port_checker, host_ip, repaired_port) || lease_check(host_ip, repaired_port) if in_use @logger.info("Repaired port also in use: #{repaired_port}. Trying another...") next end # We have a port so break out break end # If we have no usable ports then we can't repair if !repaired_port && usable_ports.empty? raise Errors::ForwardPortAutolistEmpty, vm_name: env[:machine].name, guest_port: guest_port.to_s, host_port: host_port.to_s end # Modify the args in place options[:host] = repaired_port @logger.info("Repaired FP collision: #{host_port} to #{repaired_port}") # Notify the user env[:ui].info(I18n.t("vagrant.actions.vm.forward_ports.fixed_collision", host_port: host_port.to_s, guest_port: guest_port.to_s, new_port: repaired_port.to_s)) end end end def lease_check(host_ip=nil, host_port) # Check if this port is "leased". We use a leasing system of # about 60 seconds to avoid any forwarded port collisions in # a highly parallelized environment. leasedir = @machine.env.data_dir.join("fp-leases") leasedir.mkpath if host_ip.nil? lease_file_name = host_port.to_s else lease_file_name = "#{host_ip.gsub('.','_')}_#{host_port.to_s}" end invalid = false oldest = Time.now.to_i - 60 leasedir.children.each do |child| # Delete old, invalid leases while we're looking if child.file? && child.mtime.to_i < oldest child.delete end if child.basename.to_s == lease_file_name invalid = true end end # If its invalid, then the port is "open" and in use return true if invalid # Otherwise, create the lease leasedir.join(lease_file_name).open("w+") do |f| f.binmode f.write(Time.now.to_i.to_s + "\n") end # Add to the leased array so we unlease it right away @leased << lease_file_name # Things look good to us! false end def lease_release leasedir = @machine.env.data_dir.join("fp-leases") @leased.each do |port| path = leasedir.join(port) path.delete if path.file? end end # This functions checks to see if the current instance's hostport and # hostip for forwarding is in use by the virtual machines created # previously. def is_forwarded_already(extra_in_use, hostport, hostip) hostip = '*' if hostip.nil? || hostip.empty? # ret. false if none of the VMs we spun up had this port forwarded. return false if not extra_in_use.has_key?(hostport) # ret. true if the user has requested to bind on all interfaces but # we already have a rule in one the VMs we spun up. if hostip == '*' if extra_in_use.fetch(hostport).size != 0 return true else return false end end return extra_in_use.fetch(hostport).include?(hostip) end def port_check(host_ip, host_port) self.class.port_check(@machine, host_ip, host_port) end def self.port_check(machine, host_ip, host_port) @logger = Log4r::Logger.new("vagrant::action::builtin::handle_port_collisions") # If no host_ip is specified, intention taken to be listen on all interfaces. test_host_ip = host_ip || "0.0.0.0" if Util::Platform.windows? && test_host_ip == "0.0.0.0" @logger.debug("Testing port #{host_port} on all IPv4 interfaces...") available_interfaces = Vagrant::Util::IPv4Interfaces.ipv4_interfaces.select do |interface| @logger.debug("Testing #{interface[0]} with IP address #{interface[1]}") !Vagrant::Util::IsPortOpen.is_port_open?(interface[1], host_port) end if available_interfaces.empty? @logger.debug("Cannot forward port #{host_port} on any interfaces.") true else @logger.debug("Port #{host_port} will forward to the guest on the following interfaces: #{available_interfaces}") false end else # Do a regular check if test_host_ip != "0.0.0.0" && !Addrinfo.ip(test_host_ip).ipv4_loopback? && Vagrant::Util::IPv4Interfaces.ipv4_interfaces.none? { |iface| iface[1] == test_host_ip } @logger.warn("host IP address is not local to this device host_ip=#{test_host_ip}") end Vagrant::Util::IsPortOpen.is_port_open?(test_host_ip, host_port) end end def with_forwarded_ports(env) env[:machine].config.vm.networks.each do |type, options| # Ignore anything but forwarded ports next if type != :forwarded_port yield options end end def call_port_checker(port_checker, host_ip, host_port) call_args = [host_ip, host_port] # Trim args if checker method does not support inclusion of host_ip call_args = call_args.slice(call_args.size - port_checker.arity.abs, port_checker.arity.abs) port_checker[*call_args] end end end end end ================================================ FILE: lib/vagrant/action/builtin/has_provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin # This middleware is used with Call to test if this machine # has available provisioners class HasProvisioner def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::has_provisioner") end def call(env) machine = env[:machine] if machine.provider.capability?(:has_communicator) has_communicator = machine.provider.capability(:has_communicator) else has_communicator = true end env[:skip] = [] if !has_communicator machine.config.vm.provisioners.each do |p| if p.communicator_required env[:skip].push(p) @logger.info("Skipping running provisioner #{p.name || 'no name'}, type: #{p.type}") p.run = :never end end end @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/is_env_set.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 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 ================================================ FILE: lib/vagrant/action/builtin/is_state.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin # This middleware is meant to be used with Call and can check if # a machine is in the given state ID. class IsState # Note: Any of the arguments can be arrays as well. # # @param [Symbol] target_state The target state ID that means that # the machine was properly shut down. # @param [Symbol] source_state The source state ID that the machine # must be in to be shut down. def initialize(app, env, check, **opts) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::is_state") @check = check @invert = !!opts[:invert] end def call(env) @logger.debug("Checking if machine state is '#{@check}'") state = env[:machine].state.id @logger.debug("-- Machine state: #{state}") env[:result] = @check == state env[:result] = !env[:result] if @invert @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/lock.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Action module Builtin # This class creates a multi-process lock using `flock`. The lock # is active for the remainder of the middleware stack. class Lock def initialize(app, env, options=nil) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::lock") @options ||= options || {} raise ArgumentError, "Please specify a lock path" if !@options[:path] raise ArgumentError, "Please specify an exception." if !@options[:exception] end def call(env) lock_path = @options[:path] lock_path = lock_path.call(env) if lock_path.is_a?(Proc) env_key = "has_lock_#{lock_path}" if !env[env_key] # If we already have the key in our environment we assume the # lock is held by our middleware stack already and we allow # nesting. File.open(lock_path, "w+") do |f| # The file locking fails only if it returns "false." If it # succeeds it returns a 0, so we must explicitly check for # the proper error case. @logger.info("Locking: #{lock_path}") if f.flock(File::LOCK_EX | File::LOCK_NB) === false exception = @options[:exception] exception = exception.call(env) if exception.is_a?(Proc) raise exception end # Set that we gained the lock and call deeper into the # middleware, but make sure we UNSET the lock when we leave. begin env[env_key] = true @app.call(env) ensure @logger.info("Unlocking: #{lock_path}") env[env_key] = false f.flock(File::LOCK_UN) end end else # Just call up the middleware because we already hold the lock @app.call(env) end end end end end end ================================================ FILE: lib/vagrant/action/builtin/message.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin # This middleware simply outputs a message to the UI. class Message def initialize(app, env, message, **opts) @app = app @message = message @opts = opts end def call(env) if !@opts[:post] env[:ui].output(@message) end @app.call(env) if @opts[:post] env[:ui].output(@message) end end end end end end ================================================ FILE: lib/vagrant/action/builtin/mixin_provisioners.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin module MixinProvisioners # This returns all the instances of the configured provisioners. # This is safe to call multiple times since it will cache the results. # # @return [Array] def provisioner_instances(env) return @_provisioner_instances if @_provisioner_instances # Make the mapping that'll keep track of provisioner => type @_provisioner_types = {} # Get all the configured provisioners @_provisioner_instances = env[:machine].config.vm.provisioners.map do |provisioner| # Instantiate the provisioner klass = Vagrant.plugin("2").manager.provisioners[provisioner.type] # This can happen in the case the configuration isn't validated. next nil if !klass result = klass.new(env[:machine], provisioner.config) # Store in the type map so that --provision-with works properly @_provisioner_types[result] = provisioner.type # Set top level provisioner name to provisioner configs name if top level name not set. # This is mostly for handling the shell provisioner, if a user has set its name like: # # config.vm.provision "shell", name: "my_provisioner" # # Where `name` is a shell config option, not a top level provisioner class option # # Note: `name` is set to a symbol, since it is converted to one via #Config::VM.provision provisioner_name = provisioner.name if !provisioner_name if provisioner.config.respond_to?(:name) && provisioner.config.name provisioner_name = provisioner.config.name.to_sym end else provisioner_name = provisioner_name.to_sym end # Build up the options options = { name: provisioner_name, run: provisioner.run, before: provisioner.before, after: provisioner.after, communicator_required: provisioner.communicator_required, } # Return the result [result, options] end @_provisioner_instances = sort_provisioner_instances(@_provisioner_instances) return @_provisioner_instances.compact end private # Sorts provisioners based on order specified with before/after options # # @return [Array] def sort_provisioner_instances(pvs) final_provs = [] root_provs = [] # extract root provisioners root_provs = pvs.find_all { |_, o| o[:before].nil? && o[:after].nil? } if root_provs.size == pvs.size # no dependencies found return pvs end # ensure placeholder variables are Arrays dep_provs = [] each_provs = [] all_provs = [] # extract dependency provisioners dep_provs = pvs.find_all { |_, o| o[:before].is_a?(String) || o[:after].is_a?(String) } # extract each provisioners each_provs = pvs.find_all { |_,o| o[:before] == :each || o[:after] == :each } # extract all provisioners all_provs = pvs.find_all { |_,o| o[:before] == :all || o[:after] == :all } # insert provisioners in order final_provs = root_provs dep_provs.each do |p,options| idx = 0 if options[:before] idx = final_provs.index { |_, o| o[:name].to_s == options[:before] } final_provs.insert(idx, [p, options]) elsif options[:after] idx = final_provs.index { |_, o| o[:name].to_s == options[:after] } idx += 1 final_provs.insert(idx, [p, options]) end end # Add :each and :all provisioners in reverse to preserve order in Vagrantfile tmp_final_provs = [] final_provs.each_with_index do |(prv,o), i| tmp_before = [] tmp_after = [] each_provs.reverse_each do |p, options| if options[:before] tmp_before << [p,options] elsif options[:after] tmp_after << [p,options] end end tmp_final_provs += tmp_before unless tmp_before.empty? tmp_final_provs += [[prv,o]] tmp_final_provs += tmp_after unless tmp_after.empty? end final_provs = tmp_final_provs # Add all to final array all_provs.reverse_each do |p,options| if options[:before] final_provs.insert(0, [p,options]) elsif options[:after] final_provs.push([p,options]) end end return final_provs end # This will return a mapping of a provisioner instance to its # type. def provisioner_type_map(env) # Call this in order to initial the map if it hasn't been already provisioner_instances(env) # Return the type map @_provisioner_types end # @private # Reset the cached values for platform. This is not considered a public # API and should only be used for testing. def self.reset! instance_variables.each(&method(:remove_instance_variable)) end end end end end ================================================ FILE: lib/vagrant/action/builtin/mixin_synced_folders.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" require "set" require 'vagrant/util/scoped_hash_override' module Vagrant module Action module Builtin module MixinSyncedFolders include Vagrant::Util::ScopedHashOverride # This goes over all the registered synced folder types and returns # the highest priority implementation that is usable for this machine. def default_synced_folder_type(machine, plugins) ordered = [] # First turn the plugins into an array plugins.each do |key, data| impl = data[0] priority = data[1] ordered << [priority, key, impl] end # Order the plugins by priority. Higher is tried before lower. ordered = ordered.sort { |a, b| b[0] <=> a[0] } allowed_types = machine.config.vm.allowed_synced_folder_types if allowed_types ordered = allowed_types.map do |type| ordered.find do |_, key, impl| key == type end end.compact end # Find the proper implementation ordered.each do |_, key, impl| return key if impl.new.usable?(machine) end return nil end # This finds the options in the env that are set for a given # synced folder type. def impl_opts(name, env) {}.tap do |result| env.each do |k, v| if k.to_s.start_with?("#{name}_") # While I generally don't like the 'rescue' syntax, # we do this to just fall back to the default value # if it isn't dup-able. k = k.dup rescue k v = v.dup rescue v result[k] = v end end end end # This returns the available synced folder implementations. This # is a separate method so that it can be easily stubbed by tests. def plugins @plugins ||= Vagrant.plugin("2").manager.synced_folders end # This saves the synced folders data to the machine data directory. # They can then be retrieved again with `synced_folders` by passing # the `cached` option to it. # # @param [Machine] machine The machine that the folders belong to # @param [Hash] folders The result from a {#synced_folders} call. def save_synced_folders(machine, folders, opts={}) if opts[:merge] existing = cached_synced_folders(machine) if existing if opts[:vagrantfile] # Go through and find any cached that were from the # Vagrantfile itself. We remove those if it was requested. existing.each do |impl, fs| fs.each do |id, data| fs.delete(id) if data[:__vagrantfile] end end end folders.each do |impl, fs| existing[impl] ||= {} fs.each do |id, data| existing[impl][id] = data end end folders = existing end end # Remove implementation instances folder_data = JSON.dump(folders.to_h) machine.data_dir.join("synced_folders").open("w") do |f| f.write(folder_data) end end # This returns the set of shared folders that should be done for # this machine. It returns the folders in a hash keyed by the # implementation class for the synced folders. # # @return [Hash>] def synced_folders(machine, **opts) return cached_synced_folders(machine) if opts[:cached] config = opts[:config] root = false if !config config = machine.config.vm root = true end config_folders = config.synced_folders folders = Vagrant::Plugin::V2::SyncedFolder::Collection.new # Determine all the synced folders as well as the implementation # they're going to use. config_folders.each do |id, data| # Ignore disabled synced folders next if data[:disabled] impl = "" impl = data[:type].to_sym if data[:type] && !data[:type].empty? if impl != "" impl_class = plugins[impl] if !impl_class # This should never happen because configuration validation # should catch this case. But we put this here as an assert raise "Internal error. Report this as a bug. Invalid: #{data[:type]}" end if !opts[:disable_usable_check] if !impl_class[0].new.usable?(machine, true) # Verify that explicitly defined shared folder types are # actually usable. raise Errors::SyncedFolderUnusable, type: data[:type].to_s end end end # Get the data to store data = data.dup if root # If these are the root synced folders (attached directly) # to the Vagrantfile, then we mark it as such. data[:__vagrantfile] = true end # Keep track of this shared folder by the implementation. folders[impl] ||= {} folders[impl][id] = data end # If we have folders with the "default" key, then determine the # most appropriate implementation for this. if folders.key?("") && !folders[""].empty? default_impl = default_synced_folder_type(machine, plugins) if !default_impl types = plugins.to_hash.keys.map { |t| t.to_s }.sort.join(", ") raise Errors::NoDefaultSyncedFolderImpl, types: types end folders[default_impl] ||= {} folders[default_impl].merge!(folders[""]) folders.delete("") end # Apply the scoped hash overrides to get the options folders.dup.each do |impl_name, fs| impl = plugins[impl_name].first.new._initialize(machine, impl_name) new_fs = {} fs.each do |id, data| data[:plugin] = impl id = data[:id] if data[:id] new_fs[id] = scoped_hash_override(data, impl_name) end folders[impl_name] = new_fs end folders end # This finds the difference between two lists of synced folder # definitions. # # This will return a hash with three keys: "added", "removed", # and "modified". These will contain a set of IDs of folders # that were added, removed, or modified, respectively. # # The parameters should be results from the {#synced_folders} call. # # @return [hash] def synced_folders_diff(one, two) existing_ids = {} one.each do |impl, fs| fs.each do |id, data| existing_ids[id] = data end end result = Hash.new { |h, k| h[k] = Set.new } two.each do |impl, fs| fs.each do |id, data| existing = existing_ids.delete(id) if !existing result[:added] << id next end # Exists, so we have to compare the host and guestpath, which # is most important... if existing[:hostpath] != data[:hostpath] || existing[:guestpath] != data[:guestpath] result[:modified] << id end end end existing_ids.each do |k, _| result[:removed] << k end result end protected def cached_synced_folders(machine) import = JSON.parse(machine.data_dir.join("synced_folders").read) import.each do |type, folders| impl = plugins[type.to_sym].first.new._initialize(machine, type.to_sym) folders.each { |_, v| v[:plugin] = impl } end # Symbolize the keys we want as symbols import.keys.dup.each do |k| import[k].values.each do |item| item.keys.dup.each do |ik| item[ik.to_sym] = item.delete(ik) end end import[k.to_sym] = import.delete(k) end Vagrant::Plugin::V2::SyncedFolder::Collection[import] rescue Errno::ENOENT # If the file doesn't exist, we probably just have a machine created # by a version of Vagrant that didn't cache shared folders. Report no # shared folders to be safe. Vagrant::Plugin::V2::SyncedFolder::Collection.new end end end end end ================================================ FILE: lib/vagrant/action/builtin/prepare_clone.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Action module Builtin class PrepareClone def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::vm::prepare_clone") 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 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 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 env[:clone_machine] = clone_machine # Continue @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/provision.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require_relative "mixin_provisioners" module Vagrant module Action module Builtin # This class will run the configured provisioners against the # machine. # # This action should be placed BEFORE the machine is booted so it # can do some setup, and then run again (on the return path) against # a running machine. class Provision include MixinProvisioners def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::provision") end def call(env) @env = env # Tracks whether we were configured to provision config_enabled = true config_enabled = env[:provision_enabled] if env.key?(:provision_enabled) # Check if we already provisioned, and if so, disable the rest provision_enabled = true ignore_sentinel = true if env.key?(:provision_ignore_sentinel) ignore_sentinel = env[:provision_ignore_sentinel] end if ignore_sentinel @logger.info("Ignoring sentinel check, forcing provision") end @logger.info("Checking provisioner sentinel file...") sentinel_path = env[:machine].data_dir.join("action_provision") update_sentinel = false if sentinel_path.file? # The sentinel file is in the format of "version:data" so that # we can remain backwards compatible with previous sentinels. # Versions so far: # # Vagrant < 1.5.0: A timestamp. The weakness here was that # if it wasn't cleaned up, it would incorrectly not provision # new machines. # # Vagrant >= 1.5.0: "1.5:ID", where ID is the machine ID. # We compare both so we know whether it is a new machine. # contents = sentinel_path.read.chomp parts = contents.split(":", 2) if parts.length == 1 @logger.info("Old-style sentinel found! Not provisioning.") provision_enabled = false if !ignore_sentinel update_sentinel = true elsif parts[0] == "1.5" && parts[1] == env[:machine].id.to_s @logger.info("Sentinel found! Not provisioning.") provision_enabled = false if !ignore_sentinel else @logger.info("Sentinel found with another machine ID. Removing.") sentinel_path.unlink end end # Store the value so that other actions can use it env[:provision_enabled] = provision_enabled if !env.key?(:provision_enabled) # Ask the provisioners to modify the configuration if needed provisioner_instances(env).each do |p, _| p.configure(env[:machine].config) end # Continue, we need the VM to be booted. @app.call(env) # If we're configured to not provision, notify the user and stop if !config_enabled env[:ui].info(I18n.t("vagrant.actions.vm.provision.disabled_by_config")) return end # If we're not provisioning because of the sentinel, tell the user # but continue trying for the "always" provisioners if !provision_enabled env[:ui].info(I18n.t("vagrant.actions.vm.provision.disabled_by_sentinel")) end # Write the sentinel if we have to if update_sentinel || !sentinel_path.file? @logger.info("Writing provisioning sentinel so we don't provision again") sentinel_path.open("w") do |f| f.write("1.5:#{env[:machine].id}") end end type_map = provisioner_type_map(env) provisioner_instances(env).each do |p, options| type_name = type_map[p] if options[:run] == :never next if env[:provision_types].nil? || !env[:provision_types].include?(options[:name]) else next if env[:provision_types] && \ !env[:provision_types].include?(type_name) && \ !env[:provision_types].include?(options[:name]) # Don't run if sentinel is around and we're not always running next if !provision_enabled && options[:run] != :always end name = type_name if options[:name] name = "#{options[:name]} (#{type_name})" end env[:ui].info(I18n.t( "vagrant.actions.vm.provision.beginning", provisioner: name)) env[:hook].call(:provisioner_run, env.merge( callable: method(:run_provisioner), provisioner: p, provisioner_name: type_name, )) end end # This is pulled out into a separate method so that users can # subclass and implement custom behavior if they'd like to work around # this step. def run_provisioner(env) env[:provisioner].provision end end end end end ================================================ FILE: lib/vagrant/action/builtin/provisioner_cleanup.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require_relative "mixin_provisioners" module Vagrant module Action module Builtin # This action will run the cleanup methods on provisioners and should # be used as part of any Destroy action. class ProvisionerCleanup include MixinProvisioners def initialize(app, env, place=nil) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::provision_cleanup") place ||= :after @place = place.to_sym end def call(env) do_cleanup(env) if @place == :before # Continue, we need the VM to be booted. @app.call(env) do_cleanup(env) if @place == :after end def do_cleanup(env) type_map = provisioner_type_map(env) # Ask the provisioners to modify the configuration if needed provisioner_instances(env).each do |p, _| name = type_map[p].to_s # Check if the subclass defined a cleanup method. The parent # provisioning class defines a `cleanup` method, so we cannot use # `respond_to?` here. Instead, we have to check if _this_ instance # defines a cleanup task. if p.public_methods(false).include?(:cleanup) env[:ui].info(I18n.t("vagrant.provisioner_cleanup", name: name, )) p.cleanup else @logger.debug("Skipping cleanup tasks for `#{name}' - not defined") end end end end end end end ================================================ FILE: lib/vagrant/action/builtin/set_hostname.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Action module Builtin # This middleware sets the hostname of the guest according to the # "vm.hostname" configuration parameter if it is set. This middleware # should be placed such that the after the @app.call, a booted machine # is available (this generally means BEFORE the boot middleware). class SetHostname def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::set_hostname") end def call(env) @app.call(env) hostname = env[:machine].config.vm.hostname allow_hosts_modification = env[:machine].config.vm.allow_hosts_modification if !hostname.nil? && allow_hosts_modification env[:ui].info I18n.t("vagrant.actions.vm.hostname.setting") env[:machine].guest.capability(:change_host_name, hostname) else @logger.info("`allow_hosts_modification` set to false. Hosts modification has been disabled, skiping changing hostname.") end end end end end end ================================================ FILE: lib/vagrant/action/builtin/ssh_exec.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "vagrant/util/ssh" module Vagrant module Action module Builtin # This class will exec into a full fledged SSH console into the # remote machine. This middleware assumes that the VM is running and # ready for SSH, and uses the {Machine#ssh_info} method to retrieve # SSH information necessary to connect. # # Note: If there are any middleware after `SSHExec`, they will **not** # run, since exec replaces the currently running process. class SSHExec # For quick access to the `SSH` class. include Vagrant::Util def initialize(app, env) @app = app end def call(env) # Grab the SSH info from the machine or the environment info = env[:ssh_info] info ||= env[:machine].ssh_info # If the result is nil, then the machine is telling us that it is # not yet ready for SSH, so we raise this exception. raise Errors::SSHNotReady if info.nil? info[:private_key_path] ||= [] if info[:private_key_path].empty? && info[:password] env[:ui].warn(I18n.t("vagrant.ssh_exec_password")) end # Exec! SSH.exec(info, env[:ssh_opts]) end end end end end ================================================ FILE: lib/vagrant/action/builtin/ssh_run.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/util/platform" require "vagrant/util/ssh" require "vagrant/util/shell_quote" module Vagrant module Action module Builtin # This class will run a single command on the remote machine and will # mirror the output to the UI. The resulting exit status of the command # will exist in the `:ssh_run_exit_status` key in the environment. class SSHRun # For quick access to the `SSH` class. include Vagrant::Util def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::ssh_run") end def call(env) # Grab the SSH info from the machine or the environment info = env[:ssh_info] info ||= env[:machine].ssh_info # If the result is nil, then the machine is telling us that it is # not yet ready for SSH, so we raise this exception. raise Errors::SSHNotReady if info.nil? info[:private_key_path] ||= [] if info[:keys_only] && info[:private_key_path].empty? raise Errors::SSHRunRequiresKeys end # Get the command and wrap it in a login shell command = ShellQuote.escape(env[:ssh_run_command], "'") if env[:machine].config.vm.communicator == :winssh shell = env[:machine].config.winssh.shell else shell = env[:machine].config.ssh.shell end if shell == "cmd" # Add an extra space to the command so cmd.exe quoting works # properly command = "#{shell} /C #{command} " elsif shell == "powershell" command = "$ProgressPreference = \"SilentlyContinue\"; #{command}" command = Base64.strict_encode64(command.encode("UTF-16LE", "UTF-8")) command = "#{shell} -encodedCommand #{command}" else command = "#{shell} -c '#{command}'" end # Execute! opts = env[:ssh_opts] || {} opts[:extra_args] ||= [] # Allow the user to specify a tty or non-tty manually, but if they # don't then we default to a TTY unless they are using WinSSH if !opts[:extra_args].include?("-t") && !opts[:extra_args].include?("-T") && env[:tty] && env[:machine].config.vm.communicator != :winssh opts[:extra_args] << "-t" end opts[:extra_args] << command opts[:subprocess] = true env[:ssh_run_exit_status] = _raw_ssh_exec(env, info, opts) # Call the next middleware @app.call(env) end def _raw_ssh_exec(env, info, opts) Util::SSH.exec(info, opts) end end end end end ================================================ FILE: lib/vagrant/action/builtin/synced_folder_cleanup.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require_relative "mixin_synced_folders" module Vagrant module Action module Builtin # This middleware will run cleanup tasks for synced folders using # the appropriate synced folder plugin. class SyncedFolderCleanup include MixinSyncedFolders def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::synced_folder_cleanup") end def call(env) folders = synced_folders(env[:machine]) # Go through each folder and do cleanup folders.each_key do |impl_name| @logger.info("Invoking synced folder cleanup for: #{impl_name}") plugins[impl_name.to_sym][0].new.cleanup( env[:machine], impl_opts(impl_name, env)) end @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/synced_folders.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require 'vagrant/util/platform' require_relative "mixin_synced_folders" module Vagrant module Action module Builtin # This middleware will setup the synced folders for the machine using # the appropriate synced folder plugin. class SyncedFolders include MixinSyncedFolders def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::builtin::synced_folders") end def call(env) opts = { cached: !!env[:synced_folders_cached], config: env[:synced_folders_config], } @logger.info("SyncedFolders loading from cache: #{opts[:cached]}") folders = synced_folders(env[:machine], **opts) original_folders = folders folders.each do |impl_name, fs| @logger.info("Synced Folder Implementation: #{impl_name}") fs.each do |id, data| # Log every implementation and their paths @logger.info(" - #{id}: #{data[:hostpath]} => #{data[:guestpath]}") end end # Go through each folder and make sure to create it if # it does not exist on host folders.each do |_, fs| fs.each do |id, data| next if data[:hostpath_exact] data[:hostpath] = File.expand_path( data[:hostpath], env[:root_path]) # Expand the symlink if this is a path that exists if File.file?(data[:hostpath]) data[:hostpath] = File.realpath(data[:hostpath]) end # Create the hostpath if it doesn't exist and we've been told to if !File.directory?(data[:hostpath]) && data[:create] @logger.info("Creating shared folder host directory: #{data[:hostpath]}") begin Pathname.new(data[:hostpath]).mkpath rescue Errno::EACCES raise Vagrant::Errors::SharedFolderCreateFailed, path: data[:hostpath] end end if File.directory?(data[:hostpath]) data[:hostpath] = File.realpath(data[:hostpath]) data[:hostpath] = Util::Platform.fs_real_path(data[:hostpath]).to_s end end end # Build up the instances of the synced folders. We do this once # so that they can store state. folders = folders.map do |impl_name, fs| instance = plugins[impl_name.to_sym][0].new [instance, impl_name, fs] end # Go through each folder and prepare the folders folders.each do |impl, impl_name, fs| if !env[:synced_folders_disable] @logger.info("Invoking synced folder prepare for: #{impl_name}") impl.prepare(env[:machine], fs, impl_opts(impl_name, env)) end end # Continue, we need the VM to be booted. @app.call(env) # Once booted, setup the folder contents folders.each do |impl, impl_name, fs| if !env[:synced_folders_disable] @logger.info("Invoking synced folder enable: #{impl_name}") impl.enable(env[:machine], fs, impl_opts(impl_name, env)) next end # We're disabling synced folders to_disable = {} fs.each do |id, data| next if !env[:synced_folders_disable].include?(id) to_disable[id] = data end @logger.info("Invoking synced folder disable: #{impl_name}") to_disable.each do |id, _| @logger.info(" - Disabling: #{id}") end impl.disable(env[:machine], to_disable, impl_opts(impl_name, env)) end # If we disabled folders, we have to delete some from the # save, so we load the entire cached thing, and delete them. if env[:synced_folders_disable] all = synced_folders(env[:machine], cached: true) all.each do |impl, fs| fs.keys.each do |id| if env[:synced_folders_disable].include?(id) fs.delete(id) end end end save_synced_folders(env[:machine], all) else save_opts = { merge: true } save_opts[:vagrantfile] = true if !opts[:config] # Save the synced folders save_synced_folders(env[:machine], original_folders, **save_opts) end # Persist the mounts by adding them to fstab (only if the guest is available) begin persist_mount = env[:machine].guest.capability?(:persist_mount_shared_folder) rescue Errors::MachineGuestNotReady persist_mount = false end if persist_mount # Persist the mounts by adding them to fstab if env[:machine].config.vm.allow_fstab_modification fstab_folders = original_folders else fstab_folders = nil end env[:machine].guest.capability(:persist_mount_shared_folder, fstab_folders) end end end end end end ================================================ FILE: lib/vagrant/action/builtin/trigger.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin # This class is used within the Builder class for injecting triggers into # different parts of the call stack. class Trigger # @param [Class, String, Symbol] name Name of trigger to fire # @param [Vagrant::Plugin::V2::Triger] triggers Trigger object # @param [Symbol] timing When trigger should fire (:before/:after) # @param [Symbol] type Type of trigger def initialize(app, env, name, triggers, timing, type=:action, all: false) @app = app @env = env @triggers = triggers @name = name @timing = timing @type = type @all = all if ![:before, :after].include?(timing) raise ArgumentError, "Invalid value provided for `timing` (allowed: :before or :after)" end end def call(env) machine = env[:machine] machine_name = machine.name if machine @triggers.fire(@name, @timing, machine_name, @type, all: @all) # Carry on @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/builtin/wait_for_communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module Builtin # This waits for the communicator to be ready for a set amount of # time. class WaitForCommunicator def initialize(app, env, states=nil) @app = app @states = states end def call(env) # Wait for ready in a thread so that we can continually check # for interrupts. ready_thr = Thread.new do Thread.current[:result] = env[:machine].communicate.wait_for_ready( env[:machine].config.vm.boot_timeout) end # Start a thread that verifies the VM stays in a good state. states_thr = Thread.new do Thread.current[:result] = true # Otherwise, periodically verify the VM isn't in a bad state. while true state = env[:machine].state.id # Used to report invalid states Thread.current[:last_known_state] = state # Check if we have the proper state so we can break out if @states && !@states.include?(state) Thread.current[:result] = false break end # Sleep a bit so we don't hit 100% CPU constantly. sleep 1 end end # Wait for a result or an interrupt env[:ui].output(I18n.t("vagrant.boot_waiting")) while ready_thr.alive? && states_thr.alive? sleep 1 return if env[:interrupted] end # Join so that they can raise exceptions if there were any ready_thr.join if !ready_thr.alive? states_thr.join if !states_thr.alive? # If it went into a bad state, then raise an error if !states_thr[:result] raise Errors::VMBootBadState, valid: @states.join(", "), invalid: states_thr[:last_known_state] end # If it didn't boot, raise an error if !ready_thr[:result] raise Errors::VMBootTimeout end env[:ui].output(I18n.t("vagrant.boot_completed")) # Make sure our threads are all killed ready_thr.kill states_thr.kill @app.call(env) ensure ready_thr.kill states_thr.kill end end end end end ================================================ FILE: lib/vagrant/action/general/package.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'fileutils' require "pathname" require 'vagrant/util/safe_chdir' require 'vagrant/util/subprocess' require 'vagrant/util/presence' module Vagrant module Action module General # A general packaging (tar) middleware. Given the following options, # it will do the right thing: # # * package.output - The filename of the outputted package. # * package.include - An array of files to include in the package. # * package.info - Path of desired info.json file to include # * package.directory - The directory which contains the contents to # compress into the package. # # This middleware always produces the final file in the current working # directory (FileUtils.pwd) class Package include Util # Perform sanity validations that the provided output filepath is sane. # In particular, this function validates: # # - The output path is a regular file (not a directory or symlink) # - No file currently exists at the given path # - A directory of package files was actually provided (internal) # # @param [String] output path to the output file # @param [String] directory path to a directory containing the files def self.validate!(output, directory) filename = File.basename(output.to_s) output = fullpath(output) if File.directory?(output) raise Vagrant::Errors::PackageOutputDirectory end if File.exist?(output) raise Vagrant::Errors::PackageOutputExists, filename: filename end if !Vagrant::Util::Presence.present?(directory) || !File.directory?(directory) raise Vagrant::Errors::PackageRequiresDirectory end end # Calculate the full path of the given path, relative to the current # working directory (where the command was run). # # @param [String] output the relative path def self.fullpath(output) File.expand_path(output, Dir.pwd) end # The path to the final output file. # @return [String] attr_reader :fullpath def initialize(app, env) @app = app env["package.files"] ||= {} env["package.info"] ||= "" env["package.output"] ||= "package.box" @fullpath = self.class.fullpath(env["package.output"]) end def call(env) @env = env self.class.validate!(env["package.output"], env["package.directory"]) package_with_folder_path if env["package.output"].include?(File::SEPARATOR) raise Errors::PackageOutputDirectory if File.directory?(fullpath) raise Errors::PackageInvalidInfo if invalid_info? @app.call(env) @env[:ui].info I18n.t("vagrant.actions.general.package.compressing", fullpath: fullpath) copy_include_files copy_info setup_private_key write_metadata_json compress end def package_with_folder_path folder_path = File.expand_path("..", @fullpath) create_box_folder(folder_path) unless File.directory?(folder_path) end def create_box_folder(folder_path) @env[:ui].info(I18n.t("vagrant.actions.general.package.box_folder", folder_path: folder_path)) FileUtils.mkdir_p(folder_path) end def recover(env) @env = env # There are certain exceptions that we don't delete the file for. ignore_exc = [Errors::PackageOutputDirectory, Errors::PackageOutputExists] ignore_exc.each do |exc| return if env["vagrant.error"].is_a?(exc) end # Cleanup any packaged files if the packaging failed at some point. File.delete(fullpath) if File.exist?(fullpath) end # This method copies the include files (passed in via command line) # to the temporary directory so they are included in a sub-folder within # the actual box def copy_include_files include_directory = Pathname.new(@env["package.directory"]).join("include") @env["package.files"].each do |from, dest| # We place the file in the include directory to = include_directory.join(dest) @env[:ui].info I18n.t("vagrant.actions.general.package.packaging", file: from) FileUtils.mkdir_p(to.parent) # Copy directory contents recursively. if File.directory?(from) FileUtils.cp_r(Dir.glob(from), to.parent, preserve: true) else FileUtils.cp(from, to, preserve: true) end end rescue Errno::EEXIST => e raise if !e.to_s.include?("symlink") # The directory contains symlinks. Show a nicer error. raise Errors::PackageIncludeSymlink end # This method copies the specified info.json file to the temporary directory # so that it is accessible via the 'box list -i' command def copy_info info_path = Pathname.new(@env["package.info"]) if info_path.file? FileUtils.cp(info_path, @env["package.directory"], preserve: true) end end # Compress the exported file into a package def compress # Get the output path. We have to do this up here so that the # pwd returns the proper thing. output_path = fullpath.to_s # Switch into that directory and package everything up Util::SafeChdir.safe_chdir(@env["package.directory"]) do # Find all the files in our current directory and tar it up! files = Dir.glob(File.join(".", "*")) # Package! Util::Subprocess.execute("bsdtar", "-czf", output_path, *files) end end # Write the metadata file into the box so that the provider # can be automatically detected when adding the box def write_metadata_json meta_path = File.join(@env["package.directory"], "metadata.json") return if File.exist?(meta_path) if @env[:machine] && @env[:machine].provider_name provider_name = @env[:machine].provider_name elsif @env[:env] && @env[:env].default_provider provider_name = @env[:env].default_provider else return end File.write(meta_path, {provider: provider_name}.to_json) end # This will copy the generated private key into the box and use # it for SSH by default. We have to do this because we now generate # random keypairs on boot, so packaged boxes would stop working # without this. def setup_private_key # If we don't have machine, we do nothing (weird) return if !@env[:machine] # If we don't have a data dir, we also do nothing (base package) return if !@env[:machine].data_dir # If we don't have a generated private key, we do nothing path = @env[:machine].data_dir.join("private_key") if !path.file? # If we have a private key that was copied into this box, # then we copy that. This is a bit of a heuristic and can be a # security risk if the key is named the correct thing, but # we'll take that risk for dev environments. (@env[:machine].config.ssh.private_key_path || []).each do |p| # If we have the correctly named key, copy it if File.basename(p) == "vagrant_private_key" path = Pathname.new(p) break end end end # If we still have no matching key, do nothing return if !path.file? # Copy it into our box directory dir = Pathname.new(@env["package.directory"]) new_path = dir.join("vagrant_private_key") FileUtils.cp(path, new_path) # Append it to the Vagrantfile (or create a Vagrantfile) vf_path = dir.join("Vagrantfile") mode = "w+" mode = "a" if vf_path.file? vf_path.open(mode) do |f| f.binmode f.puts f.puts %Q[Vagrant.configure("2") do |config|] f.puts %Q[ config.ssh.private_key_path = File.expand_path("../vagrant_private_key", __FILE__)] f.puts %Q[end] end end # Check to see if package.info is a valid file and titled info.json def invalid_info? if @env["package.info"] != "" info_path = Pathname.new(@env["package.info"]) return !info_path.file? || File.basename(info_path) != "info.json" end end end end end end ================================================ FILE: lib/vagrant/action/general/package_setup_files.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action module General class PackageSetupFiles def initialize(app, env) @app = app env["package.include"] ||= [] env["package.vagrantfile"] ||= nil end def call(env) files = {} env["package.include"].each do |file| source = Pathname.new(file) dest = nil # If the source is relative then we add the file as-is to the include # directory. Otherwise, we copy only the file into the root of the # include directory. Kind of strange, but seems to match what people # expect based on history. if source.relative? dest = source else dest = source.basename end # Assign the mapping files[file] = dest end if env["package.vagrantfile"] # Vagrantfiles are treated special and mapped to a specific file files[env["package.vagrantfile"]] = "_Vagrantfile" end # Verify the mapping files.each do |from, _| raise Vagrant::Errors::PackageIncludeMissing, file: from if !File.exist?(from) end # Save the mapping env["package.files"] = files @app.call(env) end end end end end ================================================ FILE: lib/vagrant/action/general/package_setup_folders.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require_relative "package" module Vagrant module Action module General class PackageSetupFolders include Vagrant::Util::Presence def initialize(app, env) @app = app end def call(env) env["package.output"] ||= "package.box" env["package.directory"] ||= Dir.mktmpdir("vagrant-package-", env[:tmp_path]) # Match up a couple environmental variables so that the other parts of # Vagrant will do the right thing. env["export.temp_dir"] = env["package.directory"] Vagrant::Action::General::Package.validate!( env["package.output"], env["package.directory"]) @app.call(env) end def recover(env) dir = env["package.directory"] if File.exist?(dir) FileUtils.rm_rf(dir) end end end end end end ================================================ FILE: lib/vagrant/action/hook.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action # This class manages hooks into existing {Builder} stacks, and lets you # add and remove middleware classes. This is the primary method by which # plugins can hook into built-in middleware stacks. class Hook # This is a hash of the middleware to prepend to a certain # other middleware. # # @return [Hash>] attr_reader :before_hooks # This is a hash of the middleware to append to a certain other # middleware. # # @return [Hash>] attr_reader :after_hooks # This is a list of the hooks to just prepend to the beginning # # @return [Array] attr_reader :prepend_hooks # This is a list of the hooks to just append to the end # # @return [Array] attr_reader :append_hooks def initialize @before_hooks = Hash.new { |h, k| h[k] = [] } @after_hooks = Hash.new { |h, k| h[k] = [] } @prepend_hooks = [] @append_hooks = [] end # Add a middleware before an existing middleware. # # @param [Class] existing The existing middleware. # @param [Class] new The new middleware. def before(existing, new, *args, **keywords, &block) item = Builder::StackItem.new( middleware: new, arguments: Builder::MiddlewareArguments.new( parameters: args, keywords: keywords, block: block ) ) @before_hooks[existing] << item end # Add a middleware after an existing middleware. # # @param [Class] existing The existing middleware. # @param [Class] new The new middleware. def after(existing, new, *args, **keywords, &block) item = Builder::StackItem.new( middleware: new, arguments: Builder::MiddlewareArguments.new( parameters: args, keywords: keywords, block: block ) ) @after_hooks[existing] << item end # Append a middleware to the end of the stack. Note that if the # middleware sequence ends early, then the new middleware won't # be run. # # @param [Class] new The middleware to append. def append(new, *args, **keywords, &block) item = Builder::StackItem.new( middleware: new, arguments: Builder::MiddlewareArguments.new( parameters: args, keywords: keywords, block: block ) ) @append_hooks << item end # Prepend a middleware to the beginning of the stack. # # @param [Class] new The new middleware to prepend. def prepend(new, *args, **keywords, &block) item = Builder::StackItem.new( middleware: new, arguments: Builder::MiddlewareArguments.new( parameters: args, keywords: keywords, block: block ) ) @prepend_hooks << item end # @return [Boolean] def empty? before_hooks.empty? && after_hooks.empty? && prepend_hooks.empty? && append_hooks.empty? end # This applies the given hook to a builder. This should not be # called directly. # # @param [Builder] builder def apply(builder, options={}) if !options[:no_prepend_or_append] # Prepends first @prepend_hooks.each do |item| if options[:root] idx = builder.index(options[:root]) else idx = 0 end builder.insert(idx, item.middleware, *item.arguments.parameters, **item.arguments.keywords, &item.arguments.block) end # Appends @append_hooks.each do |item| if options[:root] idx = builder.index(options[:root]) builder.insert(idx + 1, item.middleware, *item.arguments.parameters, **item.arguments.keywords, &item.arguments.block) else builder.use(item.middleware, *item.arguments.parameters, **item.arguments.keywords, &item.arguments.block) end end end # Before hooks @before_hooks.each do |key, list| next if !builder.index(key) list.each do |item| builder.insert_before(key, item.middleware, *item.arguments.parameters, **item.arguments.keywords, &item.arguments.block) end end # After hooks @after_hooks.each do |key, list| next if !builder.index(key) list.each do |item| builder.insert_after(key, item.middleware, *item.arguments.parameters, **item.arguments.keywords, &item.arguments.block) end end end end end end ================================================ FILE: lib/vagrant/action/primary_runner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Action # A PrimaryRunner is a special kind of "top-level" Action::Runner - it # informs any Action::Builders it interacts with that they are also # primary. This allows Builders to distinguish whether or not they are # nested, which they need to know for proper action_hook handling. # # @see Vagrant::Action::Builder#primary class PrimaryRunner < Runner def primary? true end end end end ================================================ FILE: lib/vagrant/action/runner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' require 'vagrant/action/hook' require 'vagrant/util/busy' require 'vagrant/util/experimental' module Vagrant module Action class Runner @@reported_interrupt = false # @param globals [Hash] variables for the env to be passed to the action # @yieldreturn [Hash] lazy-loaded vars merged into the env before action run def initialize(globals=nil, &block) @globals = globals || {} @lazy_globals = block @logger = Log4r::Logger.new("vagrant::action::runner") end # @see PrimaryRunner # @see Vagrant::Action::Builder#primary def primary? false end def run(callable_id, options=nil) callable = callable_id if !callable.kind_of?(Builder) if callable_id.kind_of?(Class) || callable_id.respond_to?(:call) callable = Builder.build(callable_id) end end if !callable || !callable.respond_to?(:call) raise ArgumentError, "Argument to run must be a callable object or registered action." end if callable.is_a?(Builder) callable.primary = self.primary? end # Create the initial environment with the options given environment = {} environment.merge!(@globals) environment.merge!(@lazy_globals.call) if @lazy_globals environment.merge!(options || {}) # NOTE: Triggers are initialized later in the Action::Runer because of # how `@lazy_globals` are evaluated. Rather than trying to guess where # the `env` is coming from, we can wait until they're merged into a single # hash above. env = environment[:env] machine = environment[:machine] environment[:triggers] = machine.triggers if machine if env ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant") environment[:triggers] ||= Vagrant::Plugin::V2::Trigger. new(env, env.vagrantfile.config.trigger, machine, ui) end # Run the action chain in a busy block, marking the environment as # interrupted if a SIGINT occurs, and exiting cleanly once the # chain has been run. ui = environment[:ui] if environment.key?(:ui) int_callback = lambda do if environment[:interrupted] if ui begin ui.error I18n.t("vagrant.actions.runner.exit_immediately") rescue ThreadError # We're being called in a trap-context. Wrap in a thread. Thread.new { ui.error I18n.t("vagrant.actions.runner.exit_immediately") }.join(THREAD_MAX_JOIN_TIMEOUT) end end abort end if ui && !@@reported_interrupt begin ui.warn I18n.t("vagrant.actions.runner.waiting_cleanup") rescue ThreadError # We're being called in a trap-context. Wrap in a thread. Thread.new { ui.warn I18n.t("vagrant.actions.runner.waiting_cleanup") }.join(THREAD_MAX_JOIN_TIMEOUT) end end environment[:interrupted] = true @@reported_interrupt = true end action_name = environment[:action_name] # We place a process lock around every action that is called @logger.info("Running action: #{action_name} #{callable_id}") Util::Busy.busy(int_callback) { callable.call(environment) } # Return the environment in case there are things in there that # the caller wants to use. environment end end end end ================================================ FILE: lib/vagrant/action/warden.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require 'vagrant/util/experimental' module Vagrant module Action # The action warden is a middleware which injects itself between # every other middleware, watching for exceptions which are raised # and performing proper cleanup on every action by calling the `recover` # method. The warden therefore allows middlewares to not worry about # exceptional events, and by providing a simple callback, can clean up # in any erroneous case. # # Warden will "just work" behind the scenes, and is not of particular # interest except to those who are curious about the internal workings # of Vagrant. class Warden attr_accessor :actions, :stack def initialize(actions, env) @stack = [] @actions = actions.map { |m| finalize_action(m, env) }.flatten @logger = Log4r::Logger.new("vagrant::action::warden") @last_error = nil end def call(env) return if @actions.empty? begin # Call the next middleware in the sequence, appending to the stack # of "recoverable" middlewares in case something goes wrong! raise Errors::VagrantInterrupt if env[:interrupted] action = @actions.shift @logger.info("Calling IN action: #{action}") @stack.unshift(action).first.call(env) raise Errors::VagrantInterrupt if env[:interrupted] @logger.info("Calling OUT action: #{action}") rescue SystemExit, NoMemoryError # This means that an "exit" or "abort" was called, or we have run out # of memory. In these cases, we just exit immediately. raise rescue Exception => e # We guard this so that the Warden only outputs this once for # an exception that bubbles up. if e != @last_error @logger.error("Error occurred: #{e}") @last_error = e end env["vagrant.error"] = e # Something went horribly wrong. Start the rescue chain then # reraise the exception to properly kick us out of limbo here. recover(env) raise end end # We implement the recover method ourselves in case a Warden is # embedded within another Warden. To recover, we just do our own # recovery process on our stack. def recover(env) @logger.info("Beginning recovery process...") @stack.each do |act| if act.respond_to?(:recover) @logger.info("Calling recover: #{act}") act.recover(env) end end @logger.info("Recovery complete.") # Clear stack so that warden down the middleware chain doesn't # rescue again. @stack.clear end # A somewhat confusing function which simply initializes each # middleware properly to call the next middleware in the sequence. def finalize_action(action, env) if action.is_a?(Builder::StackItem) klass = action.middleware args = action.arguments.parameters keywords = action.arguments.keywords block = action.arguments.block else klass = action args = [] keywords = {} end args = nil if args.empty? keywords = nil if keywords.empty? if klass.is_a?(Class) # NOTE: We need to detect if we are passing args and/or # keywords and do it explicitly. Earlier versions # are not as lax about splatting keywords when the # target method is not expecting them. if args && keywords klass.new(self, env, *args, **keywords, &block) elsif args klass.new(self, env, *args, &block) elsif keywords klass.new(self, env, **keywords, &block) else klass.new(self, env, &block) end elsif klass.respond_to?(:call) # Make it a lambda which calls the item then forwards # up the chain lambda do |e| klass.call(e) self.call(e) end else raise "Invalid action: #{action.inspect}" end end end end end ================================================ FILE: lib/vagrant/action.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/action/builder' module Vagrant module Action autoload :Builder, 'vagrant/action/builder' autoload :Hook, 'vagrant/action/hook' autoload :Runner, 'vagrant/action/runner' autoload :PrimaryRunner, 'vagrant/action/primary_runner' autoload :Warden, 'vagrant/action/warden' # Builtin contains middleware classes that are shipped with Vagrant-core # and are thus available to all plugins as a "standard library" of sorts. module Builtin autoload :BoxAdd, "vagrant/action/builtin/box_add" autoload :BoxCheckOutdated, "vagrant/action/builtin/box_check_outdated" autoload :BoxRemove, "vagrant/action/builtin/box_remove" autoload :BoxUpdate, "vagrant/action/builtin/box_update" autoload :Call, "vagrant/action/builtin/call" autoload :CleanupDisks, "vagrant/action/builtin/cleanup_disks" autoload :CloudInitSetup, "vagrant/action/builtin/cloud_init_setup" autoload :CloudInitWait, "vagrant/action/builtin/cloud_init_wait" autoload :ConfigValidate, "vagrant/action/builtin/config_validate" autoload :Confirm, "vagrant/action/builtin/confirm" autoload :Delayed, "vagrant/action/builtin/delayed" autoload :DestroyConfirm, "vagrant/action/builtin/destroy_confirm" autoload :Disk, "vagrant/action/builtin/disk" autoload :EnvSet, "vagrant/action/builtin/env_set" autoload :GracefulHalt, "vagrant/action/builtin/graceful_halt" 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 :HasProvisioner, "vagrant/action/builtin/has_provisioner" 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" autoload :MixinProvisioners, "vagrant/action/builtin/mixin_provisioners" autoload :MixinSyncedFolders, "vagrant/action/builtin/mixin_synced_folders" 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" autoload :SSHExec, "vagrant/action/builtin/ssh_exec" autoload :SSHRun, "vagrant/action/builtin/ssh_run" autoload :SyncedFolderCleanup, "vagrant/action/builtin/synced_folder_cleanup" autoload :SyncedFolders, "vagrant/action/builtin/synced_folders" autoload :Trigger, "vagrant/action/builtin/trigger" autoload :WaitForCommunicator, "vagrant/action/builtin/wait_for_communicator" end module General autoload :Package, 'vagrant/action/general/package' autoload :PackageSetupFiles, 'vagrant/action/general/package_setup_files' autoload :PackageSetupFolders, 'vagrant/action/general/package_setup_folders' end # This is the action that will add a box from a URL. This middleware # sequence is built-in to Vagrant. Plugins can hook into this like any # other middleware sequence. This is particularly useful for provider # plugins, which can hook in to do things like verification of boxes # that are downloaded. def self.action_box_add Builder.new.tap do |b| b.use Builtin::BoxAdd end end # This actions checks if a box is outdated in a given Vagrant # environment for a single machine. def self.action_box_outdated Builder.new.tap do |b| b.use Builtin::BoxCheckOutdated end end # This is the action that will remove a box given a name (and optionally # a provider). This middleware sequence is built-in to Vagrant. Plugins # can hook into this like any other middleware sequence. def self.action_box_remove Builder.new.tap do |b| b.use Builtin::BoxRemove end end end end ================================================ FILE: lib/vagrant/alias.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/registry" module Vagrant # This class imports and processes CLI aliases stored in ~/.vagrant.d/aliases class Alias def initialize(env) @aliases = Registry.new @env = env if env.aliases_path.file? env.aliases_path.readlines.each do |line| # separate keyword-command pairs keyword, command = interpret(line) if keyword && command register(keyword, command) end end end end # This returns all the registered alias commands. def commands @aliases end # This interprets a raw line from the aliases file. def interpret(line) # is it a comment? return nil if line.strip.start_with?("#") keyword, command = line.split("=", 2).collect(&:strip) # validate the keyword if keyword.match(/\s/i) raise Errors::AliasInvalidError, alias: line, message: "Alias keywords must not contain any whitespace." end [keyword, command] end # This registers an alias. def register(keyword, command) @aliases.register(keyword.to_sym) do lambda do |args| # directly execute shell commands if command.start_with?("!") return Util::SafeExec.exec "#{command[1..-1]} #{args.join(" ")}".strip end return CLI.new(command.split.concat(args), @env).execute end end end end end ================================================ FILE: lib/vagrant/batch_action.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'thread' require "log4r" module Vagrant # This class executes multiple actions as a single batch, parallelizing # the action calls if possible. class BatchAction def initialize(allow_parallel=true) @actions = [] @allow_parallel = allow_parallel @logger = Log4r::Logger.new("vagrant::batch_action") end # Add an action to the batch of actions that will be run. # # This will **not** run the action now. The action will be run # when {#run} is called. # # @param [Machine] machine The machine to run the action on # @param [Symbol] action The action to run # @param [Hash] options Any additional options to send in. def action(machine, action, options=nil) @actions << [machine, action, options] end # Custom runs a custom proc against a machine. # # @param [Machine] machine The machine to run against. def custom(machine, &block) @actions << [machine, block, nil] end # Run all the queued up actions, parallelizing if possible. # # This will parallelize if and only if the provider of every machine # supports parallelization and parallelization is possible from # initialization of the class. def run par = false if @allow_parallel par = true @logger.info("Enabling parallelization by default.") end if par @actions.each do |machine, _, _| if !machine.provider_options[:parallel] @logger.info("Disabling parallelization because provider doesn't support it: #{machine.provider_name}") par = false break end end end if par && @actions.length <= 1 @logger.info("Disabling parallelization because only executing one action") par = false end @logger.info("Batch action will parallelize: #{par.inspect}") threads = [] @actions.each do |machine, action, options| @logger.info("Starting action: #{machine} #{action} #{options}") # Create the new thread to run our action. This is basically just # calling the action but also contains some error handling in it # as well. thread = Thread.new do Thread.current[:error] = nil # Note that this thread is being used for running # a batch action Thread.current[:batch_parallel_action] = par # Record our pid when we started in order to figure out if # we've forked... start_pid = Process.pid begin if action.is_a?(Proc) action.call(machine) else machine.send(:action, action, options) end rescue Exception => e # If we're not parallelizing, then raise the error. We also # don't raise the error if we've forked, because it'll hang # the process. raise if !par && Process.pid == start_pid # Store the exception that will be processed later Thread.current[:error] = e # We can only do the things below if we do not fork, otherwise # it'll hang the process. if Process.pid == start_pid # Let the user know that this process had an error early # so that they see it while other things are happening. machine.ui.error(I18n.t("vagrant.general.batch_notify_error")) end end # If we forked during the process run, we need to do a hard # exit here. Ruby's fork only copies the running process (which # would be us), so if we return from this thread, it results # in a zombie Ruby process. if Process.pid != start_pid # We forked. exit_status = true if Thread.current[:error] # We had an error, print the stack trace and exit immediately. exit_status = false error = Thread.current[:error] @logger.error(error.inspect) @logger.error(error.message) @logger.error(error.backtrace.join("\n")) end Process.exit!(exit_status) end end # Set some attributes on the thread for later thread[:machine] = machine if !par thread.join(THREAD_MAX_JOIN_TIMEOUT) while thread.alive? end threads << thread end errors = [] threads.each do |thread| # Wait for the thread to complete thread.join(THREAD_MAX_JOIN_TIMEOUT) while thread.alive? # If the thread had an error, then store the error to show later if thread[:error] e = thread[:error] # If the error isn't a Vagrant error, then store the backtrace # as well. if !thread[:error].is_a?(Errors::VagrantError) e = thread[:error] message = e.message message += "\n" message += "\n#{e.backtrace.join("\n")}" errors << I18n.t("vagrant.general.batch_unexpected_error", machine: thread[:machine].name, message: message) else errors << I18n.t("vagrant.general.batch_vagrant_error", machine: thread[:machine].name, message: thread[:error].message) end end end if !errors.empty? raise Errors::BatchMultiError, message: errors.join("\n\n") end # Check if any threads set an exit code and exit if found. If # multiple threads have exit code values set, the first encountered # will be the value used. threads.each do |thread| if thread[:exit_code] @logger.debug("Found exit code set within batch action thread. Exiting") Process.exit!(thread[:exit_code]) end end end end end ================================================ FILE: lib/vagrant/box.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'fileutils' require "tempfile" require "json" require "log4r" require "vagrant/box_metadata" require "vagrant/util/downloader" require "vagrant/util/platform" require "vagrant/util/safe_chdir" require "vagrant/util/subprocess" module Vagrant # Represents a "box," which is a package Vagrant environment that is used # as a base image when creating a new guest machine. class Box include Comparable # The required fields in a boxes `metadata.json` file REQUIRED_METADATA_FIELDS = ["provider"] # Number of seconds to wait between checks for box updates BOX_UPDATE_CHECK_INTERVAL = 3600 # The box name. This is the logical name used when adding the box. # # @return [String] attr_reader :name # This is the provider that this box is built for. # # @return [Symbol] attr_reader :provider # This is the architecture that this box is build for. # # @return [String] attr_reader :architecture # The version of this box. # # @return [String] attr_reader :version # This is the directory on disk where this box exists. # # @return [Pathname] attr_reader :directory # This is the metadata for the box. This is read from the "metadata.json" # file that all boxes require. # # @return [Hash] attr_reader :metadata # This is the URL to the version info and other metadata for this # box. # # @return [String] attr_reader :metadata_url # This is used to initialize a box. # # @param [String] name Logical name of the box. # @param [Symbol] provider The provider that this box implements. # @param [Pathname] directory The directory where this box exists on # disk. # @param [String] architecture Architecture the box was built for # @param [String] metadata_url Metadata URL for box # @param [Hook] hook A hook to apply to the box downloader, for example, for authentication def initialize(name, provider, version, directory, architecture: nil, metadata_url: nil, hook: nil) @name = name @version = version @provider = provider @directory = directory @architecture = architecture @metadata_url = metadata_url @hook = hook metadata_file = directory.join("metadata.json") raise Errors::BoxMetadataFileNotFound, name: @name if !metadata_file.file? begin @metadata = JSON.parse(directory.join("metadata.json").read) validate_metadata_json(@metadata) rescue JSON::ParserError raise Errors::BoxMetadataCorrupted, name: @name end @logger = Log4r::Logger.new("vagrant::box") end def validate_metadata_json(metadata) metatdata_fields = metadata.keys REQUIRED_METADATA_FIELDS.each do |field| if !metatdata_fields.include?(field) raise Errors::BoxMetadataMissingRequiredFields, name: @name, required_field: field, all_fields: REQUIRED_METADATA_FIELDS.join(", ") end end end # This deletes the box. This is NOT undoable. def destroy! # Delete the directory to delete the box. FileUtils.rm_r(@directory) # Just return true always true rescue Errno::ENOENT # This means the directory didn't exist. Not a problem. return true end # Checks if this box is in use according to the given machine # index and returns the entries that appear to be using the box. # # The entries returned, if any, are not tested for validity # with {MachineIndex::Entry#valid?}, so the caller should do that # if the caller cares. # # @param [MachineIndex] index # @return [Array] def in_use?(index) results = [] index.each do |entry| box_data = entry.extra_data["box"] next if !box_data # If all the data matches, record it if box_data["name"] == self.name && box_data["provider"] == self.provider.to_s && box_data["architecture"] == self.architecture && box_data["version"] == self.version.to_s results << entry end end return nil if results.empty? results end # 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(download_options={}) tf = Tempfile.new("vagrant-load-metadata") tf.close url = @metadata_url if File.file?(url) || url !~ /^[a-z0-9]+:.*$/i url = File.expand_path(url) url = Util::Platform.cygwin_windows_path(url) url = "file:#{url}" end opts = { headers: ["Accept: application/json"] }.merge(download_options) d = Util::Downloader.new(url, tf.path, opts) if @hook @hook.call(:authenticate_box_downloader, downloader: d) end d.download! BoxMetadata.new(File.open(tf.path, "r"), url: url) rescue Errors::DownloaderError => e raise Errors::BoxMetadataDownloadError, message: e.extra_data[:message] ensure tf.unlink if tf end # Checks if the box has an update and returns the metadata, version, # and provider. If the box doesn't have an update that satisfies the # constraints, it will return nil. # # This will potentially make a network call if it has to load the # metadata from the network. # # @param [String] version Version constraints the update must # satisfy. If nil, the version constrain defaults to being a # larger version than this box. # @return [Array] def has_update?(version=nil, download_options: {}) if !@metadata_url raise Errors::BoxUpdateNoMetadata, name: @name end if download_options.delete(:automatic_check) && !automatic_update_check_allowed? @logger.info("Skipping box update check") return end version += ", " if version version ||= "" version += "> #{@version}" md = self.load_metadata(download_options) newer = md.version(version, provider: @provider, architecture: @architecture) return nil if newer == nil || !md.compatible_version_update?(@version, newer.version, provider: @provider, architecture: @architecture) [md, newer, newer.provider(@provider, @architecture)] end # Check if a box update check is allowed. Uses a file # in the box data directory to track when the last auto # update check was performed and returns true if the # BOX_UPDATE_CHECK_INTERVAL has passed. # # @return [Boolean] def automatic_update_check_allowed? check_path = directory.join("box_update_check") if check_path.exist? last_check_span = Time.now.to_i - check_path.mtime.to_i if last_check_span < BOX_UPDATE_CHECK_INTERVAL @logger.info("box update check is under the interval threshold") return false end end FileUtils.touch(check_path) true end # This repackages this box and outputs it to the given path. # # @param [Pathname] path The full path (filename included) of where # to output this box. # @return [Boolean] true if this succeeds. def repackage(path) @logger.debug("Repackaging box '#{@name}' to: #{path}") Util::SafeChdir.safe_chdir(@directory) do # Find all the files in our current directory and tar it up! files = Dir.glob(File.join(".", "**", "*")).select { |f| File.file?(f) } # Package! Util::Subprocess.execute("bsdtar", "-czf", path.to_s, *files) end @logger.info("Repackaged box '#{@name}' successfully: #{path}") true end # Implemented for comparison with other boxes. Comparison is # implemented by comparing names, providers, and architectures. def <=>(other) return super if !other.is_a?(self.class) # Comparison is done by composing the name and provider "#{@name}-#{@version}-#{@provider}-#{@architecture}" <=> "#{other.name}-#{other.version}-#{other.provider}-#{other.architecture}" end end end ================================================ FILE: lib/vagrant/box_collection.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/sha1" require "fileutils" require "monitor" require "tmpdir" require "log4r" require "vagrant/util/platform" require "vagrant/util/subprocess" module Vagrant # Represents a collection a boxes found on disk. This provides methods # for accessing/finding individual boxes, adding new boxes, or deleting # boxes. class BoxCollection TEMP_PREFIX = "vagrant-box-add-temp-".freeze VAGRANT_SLASH = "-VAGRANTSLASH-".freeze VAGRANT_COLON = "-VAGRANTCOLON-".freeze # The directory where the boxes in this collection are stored. # # A box collection matches a very specific folder structure that Vagrant # expects in order to easily manage and modify boxes. The folder structure # is the following: # # COLLECTION_ROOT/BOX_NAME/PROVIDER/[ARCHITECTURE]/metadata.json # # Where: # # * COLLECTION_ROOT - This is the root of the box collection, and is # the directory given to the initializer. # * BOX_NAME - The name of the box. This is a logical name given by # the user of Vagrant. # * PROVIDER - The provider that the box was built for (VirtualBox, # VMware, etc.). # * ARCHITECTURE - Optional. The architecture that the box was built # for (amd64, arm64, 386, etc.). # * metadata.json - A simple JSON file that at the bare minimum # contains a "provider" key that matches the provider for the # box. This metadata JSON, however, can contain anything. # # @return [Pathname] attr_reader :directory # Initializes the collection. # # @param [Pathname] directory The directory that contains the collection # of boxes. def initialize(directory, options=nil) options ||= {} @directory = directory @hook = options[:hook] @lock = Monitor.new @temp_root = options[:temp_dir_root] @logger = Log4r::Logger.new("vagrant::box_collection") end # This adds a new box to the system. # # There are some exceptional cases: # * BoxAlreadyExists - The box you're attempting to add already exists. # * BoxProviderDoesntMatch - If the given box provider doesn't match the # actual box provider in the untarred box. # * BoxUnpackageFailure - An invalid tar file. # # Preconditions: # * File given in `path` must exist. # # @param [Pathname] path Path to the box file on disk. # @param [String] name Logical name for the box. # @param [String] version The version of this box. # @param [Array] providers The providers that this box can # be a part of. This will be verified with the `metadata.json` and is # meant as a basic check. If this isn't given, then whatever provider # the box represents will be added. # @param [Boolean] force If true, any existing box with the same name # and provider will be replaced. def add(path, name, version, **opts) architecture = opts[:architecture] providers = opts[:providers] providers = Array(providers) if providers provider = nil # A helper to check if a box exists. We store this in a variable # since we call it multiple times. check_box_exists = lambda do |box_formats, box_architecture| box = find(name, box_formats, version, box_architecture) next if !box if !opts[:force] @logger.error( "Box already exists, can't add: #{name} v#{version} #{box_formats.join(", ")}") raise Errors::BoxAlreadyExists, name: name, provider: box_formats.join(", "), version: version end # We're forcing, so just delete the old box @logger.info( "Box already exists, but forcing so removing: " + "#{name} v#{version} #{box_formats.join(", ")}") box.destroy! end with_collection_lock do log_provider = providers ? providers.join(", ") : "any provider" @logger.debug("Adding box: #{name} (#{log_provider} - #{architecture.inspect}) from #{path}") # Verify the box doesn't exist early if we're given a provider. This # can potentially speed things up considerably since we don't need # to unpack any files. check_box_exists.call(providers, architecture) if providers # Create a temporary directory since we're not sure at this point if # the box we're unpackaging already exists (if no provider was given) with_temp_dir do |temp_dir| # Extract the box into a temporary directory. @logger.debug("Unpacking box into temporary directory: #{temp_dir}") result = Util::Subprocess.execute( "bsdtar", "--no-same-owner", "--no-same-permissions", "-v", "-x", "-m", "-S", "-s", "|\\\\\|/|", "-C", temp_dir.to_s, "-f", path.to_s) if result.exit_code != 0 raise Errors::BoxUnpackageFailure, output: result.stderr.to_s end # If we get a V1 box, we want to update it in place if v1_box?(temp_dir) @logger.debug("Added box is a V1 box. Upgrading in place.") temp_dir = v1_upgrade(temp_dir) end # We re-wrap ourselves in the safety net in case we upgraded. # If we didn't upgrade, then this is still safe because the # helper will only delete the directory if it exists with_temp_dir(temp_dir) do |final_temp_dir| # Get an instance of the box we just added before it is finalized # in the system so we can inspect and use its metadata. box = Box.new(name, nil, version, final_temp_dir) # Get the provider, since we'll need that to at the least add it # to the system or check that it matches what is given to us. box_provider = box.metadata["provider"] if providers found = providers.find { |p| p.to_sym == box_provider.to_sym } if !found @logger.error("Added box provider doesnt match expected: #{log_provider}") raise Errors::BoxProviderDoesntMatch, expected: log_provider, actual: box_provider end else # Verify the box doesn't already exist check_box_exists.call([box_provider], architecture) end # We weren't given a provider, so store this one. provider = box_provider.to_sym # Create the directory for this box, not including the provider root_box_dir = @directory.join(dir_name(name)) box_dir = root_box_dir.join(version) box_dir.mkpath @logger.debug("Box directory: #{box_dir}") # This is the final directory we'll move it to if architecture arch = architecture arch = Util::Platform.architecture if architecture == :auto box_dir = box_dir.join(arch) end provider_dir = box_dir.join(provider.to_s) @logger.debug("Provider directory: #{provider_dir}") if provider_dir.exist? @logger.debug("Removing existing provider directory...") provider_dir.rmtree end # Move to final destination provider_dir.mkpath # Recursively move individual files from the temporary directory # to the final location. We do this instead of moving the entire # directory to avoid issues on Windows. [GH-1424] copy_pairs = [[final_temp_dir, provider_dir]] while !copy_pairs.empty? from, to = copy_pairs.shift from.children(true).each do |f| dest = to.join(f.basename) # We don't copy entire directories, so create the # directory and then add to our list to copy. if f.directory? dest.mkpath copy_pairs << [f, dest] next end # Copy the single file @logger.debug("Moving: #{f} => #{dest}") FileUtils.mv(f, dest) end end if opts[:metadata_url] root_box_dir.join("metadata_url").open("w") do |f| f.write(opts[:metadata_url]) end end end end end # Return the box find(name, provider, version, architecture) end # This returns an array of all the boxes on the system, given by # their name and their provider. # # @return [Array] Array of `[name, version, provider, architecture]` of the boxes # installed on this system. def all results = [] with_collection_lock do @logger.debug("Finding all boxes in: #{@directory}") @directory.children(true).each do |child| # Ignore non-directories, since files are not interesting to # us in our folder structure. next if !child.directory? box_name = undir_name(child.basename.to_s) # Otherwise, traverse the subdirectories and see what versions # we have. child.children(true).each do |versiondir| next if !versiondir.directory? next if versiondir.basename.to_s.start_with?(".") version = versiondir.basename.to_s # Ensure version of box is correct before continuing if !Gem::Version.correct?(version) ui = Vagrant::UI::Prefixed.new(Vagrant::UI::Colored.new, "vagrant") ui.warn(I18n.t("vagrant.box_version_malformed", version: version, box_name: box_name)) @logger.debug("Invalid version #{version} for box #{box_name}") next end versiondir.children(true).each do |architecture_or_provider| # If the entry is not a directory, it is invalid and should be ignored if !architecture_or_provider.directory? @logger.debug("Invalid box #{box_name} (v#{version}) - invalid item: #{architecture_or_provider}") next end # Now the directory can be assumed to be the architecture architecture_name = architecture_or_provider.basename.to_s.to_sym # Cycle through directories to find providers architecture_or_provider.children(true).each do |provider| if !provider.directory? @logger.debug("Invalid box #{box_name} (v#{version}, #{architecture_name}) - invalid item: #{provider}") next end # If the entry contains a metadata file, add it if provider.join("metadata.json").file? provider_name = provider.basename.to_s.to_sym @logger.debug("Box: #{box_name} (#{provider_name} (#{architecture_name}), #{version})") results << [box_name, version, provider_name, architecture_name] end end # If the base entry contains a metadata file, then it was # added prior to architecture support and is a provider directory. # If it contains a metadata file, include it with the results only # if an entry hasn't already been included for the local system's # architecture if architecture_or_provider.join("metadata.json").file? provider_name = architecture_or_provider.basename.to_s.to_sym if results.include?([box_name, version, provider_name, Util::Platform.architecture.to_sym]) next end @logger.debug("Box: #{box_name} (#{provider_name}, #{version})") results << [box_name, version, provider_name, nil] end end end end end # Sort the list to group like providers and properly ordered versions results.sort_by! do |box_result| [ box_result[0], box_result[2], Gem::Version.new(box_result[1]), box_result[3] || :"" ] end results end # Find a box in the collection with the given name and provider. # # @param [String] name Name of the box (logical name). # @param [Array] providers Providers that the box implements. # @param [String] version Version constraints to adhere to. Example: # "~> 1.0" or "= 1.0, ~> 1.1" # @return [Box] The box found, or `nil` if not found. def find(name, providers, version, box_architecture=:auto) providers = Array(providers) architecture = box_architecture architecture = Util::Platform.architecture if architecture == :auto # Build up the requirements we have requirements = version.to_s.split(",").map do |v| begin Gem::Requirement.new(v.strip) rescue Gem::Requirement::BadRequirementError raise Errors::BoxVersionInvalid, version: v.strip end end with_collection_lock do box_directory = @directory.join(dir_name(name)) if !box_directory.directory? @logger.info("Box not found: #{name} (#{providers.join(", ")})") return nil end # Keep a mapping of Gem::Version mangled versions => directories. # ie. 0.1.0.pre.alpha.2 => 0.1.0-alpha.2 # This is so we can sort version numbers properly here, but still # refer to the real directory names in path checks below and pass an # unmangled version string to Box.new version_dir_map = {} versions = box_directory.children(true).map do |versiondir| next if !versiondir.directory? next if versiondir.basename.to_s.start_with?(".") version = Gem::Version.new(versiondir.basename.to_s) version_dir_map[version.to_s] = versiondir.basename.to_s version end.compact # Traverse through versions with the latest version first versions.sort.reverse.each do |v| if !requirements.all? { |r| r.satisfied_by?(v) } # Unsatisfied version requirements next end versiondir = box_directory.join(version_dir_map[v.to_s]) providers.each do |provider| providerdir = versiondir.join(architecture.to_s).join(provider.to_s) # If the architecture was automatically set to the host # architecture, then a match on the architecture subdirectory # or the provider directory (which is a box install prior to # architecture support) is valid if box_architecture == :auto if !providerdir.directory? providerdir = versiondir.join(provider.to_s) end if providerdir.join("metadata.json").file? @logger.info("Box found: #{name} (#{provider})") metadata_url = nil metadata_url_file = box_directory.join("metadata_url") metadata_url = metadata_url_file.read if metadata_url_file.file? if metadata_url && @hook hook_env = @hook.call( :authenticate_box_url, box_urls: [metadata_url]) metadata_url = hook_env[:box_urls].first end return Box.new( name, provider, version_dir_map[v.to_s], providerdir, architecture: box_architecture, metadata_url: metadata_url, hook: @hook ) end end # If there is no metadata file found, skip next if !providerdir.join("metadata.json").file? @logger.info("Box found: #{name} (#{provider})") metadata_url = nil metadata_url_file = box_directory.join("metadata_url") metadata_url = metadata_url_file.read if metadata_url_file.file? if metadata_url && @hook hook_env = @hook.call( :authenticate_box_url, box_urls: [metadata_url]) metadata_url = hook_env[:box_urls].first end return Box.new( name, provider, version_dir_map[v.to_s], providerdir, architecture: box_architecture, metadata_url: metadata_url, hook: @hook ) end end end nil end # This upgrades a v1.1 - v1.4 box directory structure up to a v1.5 # directory structure. This will raise exceptions if it fails in any # way. def upgrade_v1_1_v1_5 with_collection_lock do temp_dir = Pathname.new(Dir.mktmpdir(TEMP_PREFIX, @temp_root)) @directory.children(true).each do |boxdir| # Ignore all non-directories because they can't be boxes next if !boxdir.directory? box_name = boxdir.basename.to_s # If it is a v1 box, then we need to upgrade it first if v1_box?(boxdir) upgrade_dir = v1_upgrade(boxdir) FileUtils.mv(upgrade_dir, boxdir.join("virtualbox")) end # Create the directory for this box new_box_dir = temp_dir.join(dir_name(box_name), "0") new_box_dir.mkpath # Go through each provider and move it boxdir.children(true).each do |providerdir| FileUtils.cp_r(providerdir, new_box_dir.join(providerdir.basename)) end end # Move the folder into place @directory.rmtree FileUtils.mv(temp_dir.to_s, @directory.to_s) end end # Cleans the directory for a box by removing the folders that are # empty. def clean(name) return false if exists?(name) path = File.join(directory, dir_name(name)) FileUtils.rm_rf(path) end protected # Returns the directory name for the box of the given name. # # @param [String] name # @return [String] def dir_name(name) name = name.dup 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!(VAGRANT_COLON, ":") name.gsub!(VAGRANT_SLASH, "/") name end # This checks if the given directory represents a V1 box on the # system. # # @param [Pathname] dir Directory where the box is unpacked. # @return [Boolean] def v1_box?(dir) # We detect a V1 box given by whether there is a "box.ovf" which # is a heuristic but is pretty accurate. dir.join("box.ovf").file? end # This upgrades the V1 box contained unpacked in the given directory # and returns the directory of the upgraded version. This is # _destructive_ to the contents of the old directory. That is, the # contents of the old V1 box will be destroyed or moved. # # Preconditions: # * `dir` is a valid V1 box. Verify with {#v1_box?} # # @param [Pathname] dir Directory where the V1 box is unpacked. # @return [Pathname] Path to the unpackaged V2 box. def v1_upgrade(dir) @logger.debug("Upgrading box in directory: #{dir}") temp_dir = Pathname.new(Dir.mktmpdir(TEMP_PREFIX, @temp_root)) @logger.debug("Temporary directory for upgrading: #{temp_dir}") # Move all the things into the temporary directory dir.children(true).each do |child| # Don't move the temp_dir next if child == temp_dir # Move every other directory into the temporary directory @logger.debug("Copying to upgrade directory: #{child}") FileUtils.mv(child, temp_dir.join(child.basename)) end # If there is no metadata.json file, make one, since this is how # we determine if the box is a V2 box. metadata_file = temp_dir.join("metadata.json") if !metadata_file.file? metadata_file.open("w") do |f| f.write(JSON.generate({ provider: "virtualbox" })) end end # Return the temporary directory temp_dir end # This locks the region given by the block with a lock on this # collection. def with_collection_lock @lock.synchronize do return yield end end # This is a helper that makes sure that our temporary directories # are cleaned up no matter what. # # @param [String] dir Path to a temporary directory # @return [Object] The result of whatever the yield is def with_temp_dir(dir=nil) dir ||= Dir.mktmpdir(TEMP_PREFIX, @temp_root) dir = Pathname.new(dir) yield dir ensure FileUtils.rm_rf(dir.to_s) 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 ================================================ FILE: lib/vagrant/box_metadata.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" module Vagrant # BoxMetadata represents metadata about a box, including the name # it should have, a description of it, the versions it has, and # more. class BoxMetadata # The name that the box should be if it is added. # # @return [String] attr_accessor :name # The long-form human-readable description of a box. # # @return [String] attr_accessor :description # Loads the metadata associated with the box from the given # IO. # # @param [IO] io An IO object to read the metadata from. def initialize(io, **_) begin @raw = JSON.load(io) rescue JSON::ParserError => e raise Errors::BoxMetadataMalformed, error: e.to_s end @raw ||= {} @name = @raw["name"] @description = @raw["description"] @version_map = (@raw["versions"] || []).map do |v| begin [Gem::Version.new(v["version"]), Version.new(v)] rescue ArgumentError raise Errors::BoxMetadataMalformedVersion, version: v["version"].to_s end end @version_map = Hash[@version_map] end # Returns data about a single version that is included in this # metadata. # # @param [String] version The version to return, this can also # be a constraint. # @option [Symbol, Array] :provider Provider filter # @option [Symbol] :architecture Architecture filter # # @return [Version] The matching version or nil if a matching # version was not found. def version(version, **opts) requirements = version.split(",").map do |v| Gem::Requirement.new(v.strip) end providers = nil providers = Array(opts[:provider]).map(&:to_sym) if opts[:provider] # NOTE: The :auto value is not expanded here since no architecture # value comparisons are being done within this method architecture = opts.fetch(:architecture, :auto) @version_map.keys.sort.reverse.each do |v| next if !requirements.all? { |r| r.satisfied_by?(v) } version = @version_map[v] valid_providers = version.providers # If filtering by provider(s), apply filter valid_providers &= providers if providers # Skip if no valid providers are found next if valid_providers.empty? # Skip if no valid provider includes support # the desired architecture next if architecture && valid_providers.none? { |p| version.provider(p, architecture) } return version end nil end # Returns all the versions supported by this metadata. These # versions are sorted so the last element of the list is the # latest version. Optionally filter versions by a matching # provider. # # @option [Symbol, Array] :provider Provider filter # @option [Symbol] :architecture Architecture filter # # @return[Array] def versions(**opts) architecture = opts[:architecture] provider = Array(opts[:provider]).map(&:to_sym) if opts[:provider] # Return full version list if no filters provided if provider.nil? && architecture.nil? return @version_map.keys.sort.map(&:to_s) end # If a specific provider is not provided, filter # only on architecture if provider.nil? return @version_map.select { |_, version| !version.providers(architecture).empty? }.keys.sort.map(&:to_s) end @version_map.select { |_, version| provider.any? { |pv| version.provider(pv, architecture) } }.keys.sort.map(&:to_s) end def compatible_version_update?(current_version, new_version, **opts) return false if Gem::Version.new(new_version) <= Gem::Version.new(current_version) architecture = opts[:architecture] provider = opts[:provider] # If the specific provider isn't available, there is nothing further to check for compatibility return true if provider.nil? # If the current_provider isn't found in the metadata, it cannot be compared against. Default to allowing updates current_provider = version(current_version.to_s, provider: provider, architecture: architecture)&.provider(provider, architecture) return true if current_provider.nil? # If the new provider isn't found, the new version isn't compatible new_provider = version(new_version.to_s, provider: provider, architecture: architecture)&.provider(provider, architecture) return false if new_provider.nil? # Disallow updates from a known architecture to an unknown architecture, because it can not be verified, unless architecture is explicitly set to `nil` return false if !architecture.nil? && new_provider.architecture == "unknown" && current_provider.architecture != "unknown" # New version is compatible return true end # Represents a single version within the metadata. class Version # The version that this Version object represents. # # @return [String] attr_accessor :version def initialize(raw=nil, **_) return if !raw @version = raw["version"] @providers = raw.fetch("providers", []).map do |data| Provider.new(data) end @provider_map = @providers.group_by(&:name) @provider_map = Util::HashWithIndifferentAccess.new(@provider_map) end # Returns a [Provider] for the given name, or nil if it isn't # supported by this version. def provider(name, architecture=nil) name = name.to_sym arch_name = architecture arch_name = Util::Platform.architecture if arch_name == :auto arch_name = arch_name.to_s if arch_name # If the provider doesn't exist in the map, return immediately return if !@provider_map.key?(name) # If the arch_name value is set, filter based # on architecture and return match if found. If # no match is found and architecture wasn't automatically # detected, return nil as an explicit match is # being requested if arch_name match = @provider_map[name].detect do |p| p.architecture == arch_name end return match if match || architecture != :auto end # If the passed architecture value was :auto and no explicit # match for the architecture was found, check for a provider # that is flagged as the default architecture, and has an # architecture value of "unknown" # # NOTE: This preserves expected behavior with legacy boxes if architecture == :auto match = @provider_map[name].detect do |p| p.architecture == "unknown" && p.default_architecture end return match if match end # If the architecture value is set to nil, then just return # whatever is defined as the default architecture if architecture.nil? match = @provider_map[name].detect(&:default_architecture) return match if match end # The metadata consumed may not include architecture information, # in which case the match would just be the single provider # defined within the provider map for the name if @provider_map[name].size == 1 && !@provider_map[name].first.architecture_support? return @provider_map[name].first end # Otherwise, there is no match nil end # Returns the providers that are available for this version # of the box. # # @return [Array] def providers(architecture=nil) return @provider_map.keys.map(&:to_sym) if architecture.nil? @provider_map.keys.find_all { |k| provider(k, architecture) }.map(&:to_sym) end end # Provider represents a single provider-specific box available # for a version for a box. class Provider # The name of the provider. # # @return [String] attr_accessor :name # The URL of the box. # # @return [String] attr_accessor :url # The checksum value for this box, if any. # # @return [String] attr_accessor :checksum # The type of checksum (if any) associated with this provider. # # @return [String] attr_accessor :checksum_type # The architecture of the box # # @return [String] attr_accessor :architecture # Marked as the default architecture # # @return [Boolean, NilClass] attr_accessor :default_architecture def initialize(raw, **_) @name = raw["name"] @url = raw["url"] @checksum = raw["checksum"] @checksum_type = raw["checksum_type"] @architecture = raw["architecture"] @default_architecture = raw["default_architecture"] end def architecture_support? !@default_architecture.nil? end end end end ================================================ FILE: lib/vagrant/bundler.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "monitor" require "pathname" require "set" require "tempfile" require "fileutils" require "uri" require "rubygems/package" require "rubygems/uninstaller" require "rubygems/name_tuple" require_relative "shared_helpers" require_relative "version" require_relative "util/safe_env" module Vagrant # This class manages Vagrant's interaction with Bundler. Vagrant uses # Bundler as a way to properly resolve all dependencies of Vagrant and # all Vagrant-installed plugins. class Bundler class SolutionFile # @return [Pathname] path to plugin file attr_reader :plugin_file # @return [Pathname] path to solution file attr_reader :solution_file # @return [Array] list of required dependencies attr_reader :dependency_list # @param [Pathname] plugin_file Path to plugin file # @param [Pathname] solution_file Custom path to solution file def initialize(plugin_file:, solution_file: nil) @logger = Log4r::Logger.new("vagrant::bundler::solution_file") @plugin_file = Pathname.new(plugin_file.to_s) if solution_file @solution_file = Pathname.new(solution_file.to_s) else @solution_file = Pathname.new(@plugin_file.to_s + ".sol") end @valid = false @dependency_list = [].freeze @logger.debug("new solution file instance plugin_file=#{plugin_file} " \ "solution_file=#{solution_file}") load end # Set the list of dependencies for this solution # # @param [Array] dependency_list List of dependencies for the solution # @return [Array] def dependency_list=(dependency_list) Array(dependency_list).each do |d| if !d.is_a?(Gem::Dependency) raise TypeError, "Expected `Gem::Dependency` but received `#{d.class}`" end end @dependency_list = dependency_list.map do |d| Gem::Resolver::DependencyRequest.new(d, nil).freeze end.freeze end # @return [Boolean] contained solution is valid def valid? @valid end # @return [FalseClass] invalidate this solution file def invalidate! @valid = false @logger.debug("manually invalidating solution file #{self}") @valid end # Delete the solution file # # @return [Boolean] true if file was deleted def delete! if !solution_file.exist? @logger.debug("solution file does not exist. nothing to delete.") return false end @logger.debug("deleting solution file - #{solution_file}") solution_file.delete true end # Store the solution file def store! if !plugin_file.exist? @logger.debug("plugin file does not exist, not storing solution") return end if !solution_file.dirname.exist? @logger.debug("creating directory for solution file: #{solution_file.dirname}") solution_file.dirname.mkpath end @logger.debug("writing solution file contents to disk") solution_file.write({ dependencies: dependency_list.map { |d| [d.dependency.name, d.dependency.requirements_list] }, checksum: plugin_file_checksum, vagrant_version: Vagrant::VERSION }.to_json) @valid = true end def to_s # :nodoc: "" end protected # Load the solution file for the plugin path provided # if it exists. Validate solution is still applicable # before injecting dependencies. def load if !plugin_file.exist? || !solution_file.exist? @logger.debug("missing file so skipping loading") return end solution = read_solution || return return if !valid_solution?( checksum: solution[:checksum], version: solution[:vagrant_version] ) @logger.debug("loading solution dependency list") @dependency_list = Array(solution[:dependencies]).map do |name, requirements| gd = Gem::Dependency.new(name, requirements) Gem::Resolver::DependencyRequest.new(gd, nil).freeze end.freeze @logger.debug("solution dependency list: #{dependency_list}") @valid = true end # Validate the given checksum matches the plugin file # checksum # # @param [String] checksum Checksum value to validate # @return [Boolean] def valid_solution?(checksum:, version:) file_checksum = plugin_file_checksum @logger.debug("solution validation check CHECKSUM #{file_checksum} <-> #{checksum}" \ " VERSION #{Vagrant::VERSION} <-> #{version}") plugin_file_checksum == checksum && Vagrant::VERSION == version end # @return [String] checksum of plugin file def plugin_file_checksum digest = Digest::SHA256.new digest.file(plugin_file.to_s) digest.hexdigest end # Read contents of solution file and parse # # @return [Hash] def read_solution @logger.debug("reading solution file - #{solution_file}") begin hash = JSON.load(solution_file.read) Vagrant::Util::HashWithIndifferentAccess.new(hash) rescue => err @logger.warn("failed to load solution file, ignoring (error: #{err})") nil end end end # Location of HashiCorp gem repository HASHICORP_GEMSTORE = "https://gems.hashicorp.com/".freeze # Default gem repositories DEFAULT_GEM_SOURCES = [ HASHICORP_GEMSTORE, "https://rubygems.org/".freeze ].freeze def self.instance @bundler ||= self.new end # @return [Pathname] Global plugin path attr_reader :plugin_gem_path # @return [Pathname] Global plugin solution set path attr_reader :plugin_solution_path # @return [Pathname] Vagrant environment specific plugin path attr_reader :env_plugin_gem_path # @return [Pathname] Vagrant environment data path attr_reader :environment_data_path # @return [Array, nil] List of builtin specs attr_accessor :builtin_specs def initialize @builtin_specs = [] @plugin_gem_path = Vagrant.user_data_path.join("gems", RUBY_VERSION).freeze @logger = Log4r::Logger.new("vagrant::bundler") end # Enable Vagrant environment specific plugins at given data path # # @param [Pathname] Path to Vagrant::Environment data directory # @return [Pathname] Path to environment specific gem directory def environment_path=(env_data_path) if !env_data_path.is_a?(Pathname) raise TypeError, "Expected `Pathname` but received `#{env_data_path.class}`" end @env_plugin_gem_path = env_data_path.join("plugins", "gems", RUBY_VERSION).freeze @environment_data_path = env_data_path end # Use the given options to create a solution file instance # for use during initialization. When a Vagrant environment # is in use, solution files will be stored within the environment's # data directory. This is because the solution for loading global # plugins is dependent on any solution generated for local plugins. # When no Vagrant environment is in use (running Vagrant without a # Vagrantfile), the Vagrant user data path will be used for solution # storage since only the global plugins will be used. # # @param [Hash] opts Options passed to #init! # @return [SolutionFile] def load_solution_file(opts={}) return if !opts[:local] && !opts[:global] return if opts[:local] && opts[:global] return if opts[:local] && environment_data_path.nil? solution_path = (environment_data_path || Vagrant.user_data_path) + "bundler" solution_path += opts[:local] ? "local.sol" : "global.sol" SolutionFile.new( plugin_file: opts[:local] || opts[:global], solution_file: solution_path ) end # Initializes Bundler and the various gem paths so that we can begin # loading gems. def init!(plugins, repair=false, **opts) if !@initial_specifications @initial_specifications = Gem::Specification.find_all{true} else Gem::Specification.all = @initial_specifications Gem::Specification.reset end solution_file = load_solution_file(opts) @logger.debug("solution file in use for init: #{solution_file}") solution = nil composed_set = generate_vagrant_set # Force the composed set to allow prereleases if Vagrant.allow_prerelease_dependencies? @logger.debug("enabling prerelease dependency matching due to user request") composed_set.prerelease = true end if solution_file&.valid? @logger.debug("loading cached solution set") solution = solution_file.dependency_list.map do |dep| spec = composed_set.find_all(dep).select do |dep_spec| next(true) unless Gem.loaded_specs.has_key?(dep_spec.name) Gem.loaded_specs[dep_spec.name].version.eql?(dep_spec.version) end.first if !spec @logger.warn("failed to locate specification for dependency - #{dep}") @logger.warn("invalidating solution file - #{solution_file}") solution_file.invalidate! break end dep_r = Gem::Resolver::DependencyRequest.new(dep, nil) Gem::Resolver::ActivationRequest.new(spec, dep_r) end end if !solution_file&.valid? @logger.debug("generating solution set for configured plugins") # Add HashiCorp RubyGems source if !Gem.sources.include?(HASHICORP_GEMSTORE) sources = [HASHICORP_GEMSTORE] + Gem.sources.sources Gem.sources.replace(sources) end # Generate dependencies for all registered plugins plugin_deps = plugins.map do |name, info| Gem::Dependency.new(name, info['installed_gem_version'].to_s.empty? ? '> 0' : info['installed_gem_version']) end @logger.debug("Current generated plugin dependency list: #{plugin_deps}") # Load dependencies into a request set for resolution request_set = Gem::RequestSet.new(*plugin_deps) # Never allow dependencies to be remotely satisfied during init request_set.remote = false begin @logger.debug("resolving solution from available specification set") # Resolve the request set to ensure proper activation order solution = request_set.resolve(composed_set) @logger.debug("solution set for configured plugins has been resolved") rescue Gem::UnsatisfiableDependencyError => failure if repair raise failure if @init_retried @logger.debug("Resolution failed but attempting to repair. Failure: #{failure}") install(plugins) @init_retried = true retry else raise end end end # Activate the gems @logger.debug("activating solution set") activate_solution(solution) if solution_file && !solution_file.valid? solution_file.dependency_list = solution.map do |activation| activation.request.dependency end solution_file.store! @logger.debug("solution set stored to - #{solution_file}") end full_vagrant_spec_list = @initial_specifications + solution.map(&:full_spec) if(defined?(::Bundler)) @logger.debug("Updating Bundler with full specification list") ::Bundler.rubygems.replace_entrypoints(full_vagrant_spec_list) end Gem.post_reset do Gem::Specification.all = full_vagrant_spec_list end Gem::Specification.reset nil end # Removes any temporary files created by init def deinit # no-op end # Installs the list of plugins. # # @param [Hash] plugins # @param [Boolean] env_local Environment local plugin install # @return [Array] def install(plugins, env_local=false) internal_install(plugins, nil, env_local: env_local) end # Installs a local '*.gem' file so that Bundler can find it. # # @param [String] path Path to a local gem file. # @return [Gem::Specification] def install_local(path, opts={}) plugin_source = Gem::Source::SpecificFile.new(path) plugin_info = { plugin_source.spec.name => { "gem_version" => plugin_source.spec.version.to_s, "local_source" => plugin_source, "sources" => opts.fetch(:sources, []) } } @logger.debug("Installing local plugin - #{plugin_info}") internal_install(plugin_info, nil, env_local: opts[:env_local]) plugin_source.spec end # Update updates the given plugins, or every plugin if none is given. # # @param [Hash] plugins # @param [Array] specific Specific plugin names to update. If # empty or nil, all plugins will be updated. def update(plugins, specific, **opts) specific ||= [] update = opts.merge({gems: specific.empty? ? true : specific}) internal_install(plugins, update) end # Clean removes any unused gems. def clean(plugins, **opts) @logger.debug("Cleaning Vagrant plugins of stale gems.") # Generate dependencies for all registered plugins plugin_deps = plugins.map do |name, info| gem_version = info['installed_gem_version'] gem_version = info['gem_version'] if gem_version.to_s.empty? gem_version = "> 0" if gem_version.to_s.empty? Gem::Dependency.new(name, gem_version) end @logger.debug("Current plugin dependency list: #{plugin_deps}") # Load dependencies into a request set for resolution request_set = Gem::RequestSet.new(*plugin_deps) # Never allow dependencies to be remotely satisfied during cleaning request_set.remote = false # Sets that we can resolve our dependencies from. Note that we only # resolve from the current set as all required deps are activated during # init. current_set = generate_vagrant_set # Collect all plugin specifications plugin_specs = Dir.glob(plugin_gem_path.join('specifications/*.gemspec').to_s).map do |spec_path| Gem::Specification.load(spec_path) end # Include environment specific specification if enabled if env_plugin_gem_path plugin_specs += Dir.glob(env_plugin_gem_path.join('specifications/*.gemspec').to_s).map do |spec_path| Gem::Specification.load(spec_path) end end @logger.debug("Generating current plugin state solution set.") # Resolve the request set to ensure proper activation order solution = request_set.resolve(current_set) solution_specs = solution.map(&:full_spec) solution_full_names = solution_specs.map(&:full_name) # Find all specs installed to plugins directory that are not # found within the solution set. plugin_specs.delete_if do |spec| solution_full_names.include?(spec.full_name) end if env_plugin_gem_path # If we are cleaning locally, remove any global specs. If # not, remove any local specs if opts[:env_local] @logger.debug("Removing specifications that are not environment local") plugin_specs.delete_if do |spec| spec.full_gem_path.to_s.include?(plugin_gem_path.realpath.to_s) end else @logger.debug("Removing specifications that are environment local") plugin_specs.delete_if do |spec| spec.full_gem_path.to_s.include?(env_plugin_gem_path.realpath.to_s) end end end @logger.debug("Specifications to be removed - #{plugin_specs.map(&:full_name)}") # Now delete all unused specs plugin_specs.each do |spec| @logger.debug("Uninstalling gem - #{spec.full_name}") Gem::Uninstaller.new(spec.name, version: spec.version, install_dir: plugin_gem_path, all: true, executables: true, force: true, ignore: true, ).uninstall_gem(spec) end solution.find_all do |spec| plugins.keys.include?(spec.name) end end # During the duration of the yielded block, Bundler loud output # is enabled. def verbose if block_given? initial_state = @verbose @verbose = true yield @verbose = initial_state else @verbose = true end end protected def internal_install(plugins, update, **extra) update = {} if !update.is_a?(Hash) skips = [] source_list = {} system_plugins = plugins.map do |plugin_name, plugin_info| plugin_name if plugin_info["system"] end.compact installer_set = VagrantSet.new(:both) installer_set.system_plugins = system_plugins # Generate all required plugin deps plugin_deps = plugins.map do |name, info| gem_version = info['gem_version'].to_s.empty? ? '> 0' : info['gem_version'] if update[:gems] == true || (update[:gems].respond_to?(:include?) && update[:gems].include?(name)) if Gem::Requirement.new(gem_version).exact? gem_version = "> 0" @logger.debug("Detected exact version match for `#{name}` plugin update. Reset to loosen constraint #{gem_version.inspect}.") end skips << name end source_list[name] ||= [] if plugin_source = info.delete("local_source") installer_set.add_local(plugin_source.spec.name, plugin_source.spec, plugin_source) source_list[name] << plugin_source.path end Array(info["sources"]).each do |source| if !source.end_with?("/") source = source + "/" end source_list[name] << source end Gem::Dependency.new(name, *gem_version.split(",")) end if Vagrant.strict_dependency_enforcement @logger.debug("Enabling strict dependency enforcement") plugin_deps += vagrant_internal_specs.map do |spec| # NOTE: When working within bundler, skip any system plugins and # default gems. However, when not within bundler (in the installer) # include them as strict dependencies to prevent the resolver from # attempting to create a solution with a newer version. The request # set does allow for resolving conservatively but it can't be set # from the public API (requires an instance variable set on the resolver # instance) so strict dependencies are used instead. if Vagrant.in_bundler? next if system_plugins.include?(spec.name) # # If this spec is for a default plugin included in # # the ruby stdlib, ignore it next if spec.default_gem? end # If we are not running within the installer and # we are not within a bundler environment then we # only want activated specs if !Vagrant.in_installer? && !Vagrant.in_bundler? next if !spec.activated? end Gem::Dependency.new(spec.name, spec.version) end.compact else @logger.debug("Disabling strict dependency enforcement") end dep_list = plugin_deps.sort_by(&:name).map { |d| "#{d.name} #{d.requirement}" }.join("\n - ") @logger.debug("Dependency list for installation:\n - #{dep_list}") all_sources = source_list.values.flatten.uniq default_sources = DEFAULT_GEM_SOURCES & all_sources all_sources -= DEFAULT_GEM_SOURCES # Only allow defined Gem sources Gem.sources.clear @logger.debug("Enabling user defined remote RubyGems sources") all_sources.each do |src| begin next if File.file?(src) || URI.parse(src).scheme.nil? rescue URI::InvalidURIError next end @logger.debug("Adding RubyGems source #{src}") Gem.sources << src end @logger.debug("Enabling default remote RubyGems sources") default_sources.each do |src| @logger.debug("Adding source - #{src}") Gem.sources << src end validate_configured_sources! source_list.values.each{|srcs| srcs.delete_if{|src| default_sources.include?(src)}} installer_set.prefer_sources = source_list @logger.debug("Current source list for install: #{Gem.sources.to_a}") # Create the request set for the new plugins request_set = Gem::RequestSet.new(*plugin_deps) installer_set = Gem::Resolver.compose_sets( installer_set, generate_builtin_set(system_plugins), generate_plugin_set(skips) ) if Vagrant.allow_prerelease_dependencies? @logger.debug("enabling prerelease dependency matching based on user request") request_set.prerelease = true installer_set.prerelease = true end @logger.debug("Generating solution set for installation.") # Generate the required solution set for new plugins solution = request_set.resolve(installer_set) activate_solution(solution) # Remove gems which are already installed request_set.sorted_requests.delete_if do |act_req| rs = act_req.spec if vagrant_internal_specs.detect{ |i| i.name == rs.name && i.version == rs.version } @logger.debug("Removing activation request from install. Already installed. (#{rs.spec.full_name})") true end end @logger.debug("Installing required gems.") # Install all remote gems into plugin path. Set the installer to ignore dependencies # as we know the dependencies are satisfied and it will attempt to validate a gem's # dependencies are satisfied by gems in the install directory (which will likely not # be true) install_path = extra[:env_local] ? env_plugin_gem_path : plugin_gem_path result = request_set.install_into(install_path.to_s, true, ignore_dependencies: true, prerelease: Vagrant.prerelease? || Vagrant.allow_prerelease_dependencies?, wrappers: true, document: [] ) result = result.map(&:full_spec) result.each do |spec| existing_paths = $LOAD_PATH.find_all{|s| s.include?(spec.full_name) } if !existing_paths.empty? @logger.debug("Removing existing LOAD_PATHs for #{spec.full_name} - " + existing_paths.join(", ")) existing_paths.each{|s| $LOAD_PATH.delete(s) } end spec.full_require_paths.each do |r_path| if !$LOAD_PATH.include?(r_path) @logger.debug("Adding path to LOAD_PATH - #{r_path}") $LOAD_PATH.unshift(r_path) end end end result end # Generate the composite resolver set totally all of vagrant (builtin + plugin set) def generate_vagrant_set sets = [generate_builtin_set, generate_plugin_set] if env_plugin_gem_path && env_plugin_gem_path.exist? sets << generate_plugin_set(env_plugin_gem_path) end Gem::Resolver.compose_sets(*sets) end # @return [Array<[Gem::Specification]>] spec list def vagrant_internal_specs # activate any dependencies up front so we can always # pin them when resolving self_spec = Gem::Specification.find { |s| s.name == "vagrant" && s.activated? } if !self_spec @logger.warn("Failed to locate activated vagrant specification. Activating...") self_spec = Gem::Specification.find { |s| s.name == "vagrant" } if !self_spec @logger.error("Failed to locate Vagrant RubyGem specification") raise Vagrant::Errors::SourceSpecNotFound end self_spec.activate @logger.info("Activated vagrant specification version - #{self_spec.version}") end # discover all the gems we have available list = {} if Gem.respond_to?(:default_specifications_dir) spec_dir = Gem.default_specifications_dir else spec_dir = Gem::Specification.default_specifications_dir end directories = [spec_dir] if Vagrant.in_bundler? Gem::Specification.find_all{true}.each do |spec| list[spec.name] = spec end else builtin_specs.each do |spec| list[spec.name] = spec end end if Vagrant.in_installer? directories += Gem::Specification.dirs.find_all do |path| !path.start_with?(Gem.user_dir) end end Gem::Specification.each_spec(directories) do |spec| if !list[spec.name] || list[spec.name].version < spec.version list[spec.name] = spec end end list.values end # Iterates each configured RubyGem source to validate that it is properly # available. If source is unavailable an exception is raised. def validate_configured_sources! Gem.sources.each_source do |src| begin src.load_specs(:released) rescue Gem::Exception => source_error if ENV["VAGRANT_ALLOW_PLUGIN_SOURCE_ERRORS"] @logger.warn("Failed to load configured plugin source: #{src}!") @logger.warn("Error received attempting to load source (#{src}): #{source_error}") @logger.warn("Ignoring plugin source load failure due user request via env variable") else @logger.error("Failed to load configured plugin source `#{src}`: #{source_error}") raise Vagrant::Errors::PluginSourceError, source: src.uri.to_s, error_msg: source_error.message end end end end # Generate the builtin resolver set def generate_builtin_set(system_plugins=[]) builtin_set = BuiltinSet.new @logger.debug("Generating new builtin set instance.") vagrant_internal_specs.each do |spec| if !system_plugins.include?(spec.name) builtin_set.add_builtin_spec(spec) end end builtin_set end # Generate the plugin resolver set. Optionally provide specification names (short or # full) that should be ignored # # @param [Pathname] path to plugins # @param [Array] gems to skip # @return [PluginSet] def generate_plugin_set(*args) plugin_path = args.detect{|i| i.is_a?(Pathname) } || plugin_gem_path skip = args.detect{|i| i.is_a?(Array) } || [] plugin_set = PluginSet.new @logger.debug("Generating new plugin set instance. Skip gems - #{skip}") Dir.glob(plugin_path.join('specifications/*.gemspec').to_s).each do |spec_path| spec = Gem::Specification.load(spec_path) desired_spec_path = File.join(spec.gem_dir, "#{spec.name}.gemspec") # Vendor set requires the spec to be within the gem directory. Some gems will package their # spec file, and that's not what we want to load. if !File.exist?(desired_spec_path) || !FileUtils.cmp(spec.spec_file, desired_spec_path) File.write(desired_spec_path, spec.to_ruby) end next if skip.include?(spec.name) || skip.include?(spec.full_name) plugin_set.add_vendor_gem(spec.name, spec.gem_dir) end plugin_set end # Activate a given solution def activate_solution(solution) retried = false begin @logger.debug("Activating solution set: #{solution.map(&:full_name)}") solution.each do |activation_request| unless activation_request.full_spec.activated? @logger.debug("Activating gem #{activation_request.full_spec.full_name}") activation_request.full_spec.activate if(defined?(::Bundler)) @logger.debug("Marking gem #{activation_request.full_spec.full_name} loaded within Bundler.") ::Bundler.rubygems.mark_loaded activation_request.full_spec end end end rescue Gem::LoadError => e # Depending on the version of Ruby, the ordering of the solution set # will be either 0..n (molinillo) or n..0 (pre-molinillo). Instead of # attempting to determine what's in use, or if it has some how changed # again, just reverse order on failure and attempt again. if retried @logger.error("Failed to load solution set - #{e.class}: #{e}") matcher = e.message.match(/Could not find '(?[^']+)'/) if matcher && !matcher["gem_name"].empty? desired_activation_request = solution.detect do |request| request.name == matcher["gem_name"] end if desired_activation_request && !desired_activation_request.full_spec.activated? @logger.warn("Found misordered activation request for #{desired_activation_request.full_name}. Moving to solution HEAD.") solution.delete(desired_activation_request) solution.unshift(desired_activation_request) retry end end raise else @logger.debug("Failed to load solution set. Retrying with reverse order.") retried = true solution.reverse! retry end end end # This is a custom Gem::Resolver::InstallerSet. It will prefer sources which are # explicitly provided over default sources when matches are found. This is generally # the entire set used for performing full resolutions on install. class VagrantSet < Gem::Resolver::InstallerSet attr_accessor :prefer_sources attr_accessor :system_plugins def initialize(domain, defined_sources={}) @prefer_sources = defined_sources @system_plugins = [] super(domain) end # Allow InstallerSet to find matching specs, then filter # for preferred sources def find_all(req) result = super if system_plugins.include?(req.name) result.delete_if do |spec| spec.is_a?(Gem::Resolver::InstalledSpecification) end end subset = result.find_all do |idx_spec| preferred = false if prefer_sources[req.name] if idx_spec.source.respond_to?(:path) preferred = prefer_sources[req.name].include?(idx_spec.source.path.to_s) end if !preferred preferred = prefer_sources[req.name].include?(idx_spec.source.uri.to_s) end end preferred end subset.empty? ? result : subset end end # This is a custom Gem::Resolver::Set for use with vagrant "system" gems. It # allows the installed set of gems to be used for providing a solution while # enforcing strict constraints. This ensures that plugins cannot "upgrade" # gems that are builtin to vagrant itself. class BuiltinSet < Gem::Resolver::Set def initialize super @remote = false @specs = [] end def add_builtin_spec(spec) @specs.push(spec).uniq! end def find_all(req) r = @specs.select do |spec| # When matching requests against builtin specs, we _always_ enable # prerelease matching since any prerelease that's found in this # set has been added explicitly and should be available for all # plugins to resolve against. This includes Vagrant itself since # it is considered a prerelease when in development mode req.match?(spec, true) end.map do |spec| Gem::Resolver::InstalledSpecification.new(self, spec) end # If any of the results are a prerelease, we need to mark the request # to allow prereleases so the solution can be properly fulfilled if r.any? { |x| x.version.prerelease? } req.dependency.prerelease = true end r end end # This is a custom Gem::Resolver::Set for use with Vagrant plugins. It is # a modified Gem::Resolver::VendorSet that supports multiple versions of # a specific gem class PluginSet < Gem::Resolver::VendorSet ## # Adds a specification to the set with the given +name+ which has been # unpacked into the given +directory+. def add_vendor_gem(name, directory) gemspec = File.join(directory, "#{name}.gemspec") spec = Gem::Specification.load(gemspec) if !spec raise Gem::GemNotFoundException, "unable to find #{gemspec} for gem #{name}" end spec.full_gem_path = File.expand_path(directory) spec.base_dir = File.dirname(spec.base_dir) @specs[spec.name] ||= [] @specs[spec.name] << spec @directories[spec] = directory spec end ## # Returns an Array of VendorSpecification objects matching the # DependencyRequest +req+. def find_all(req) @specs.values.flatten.select do |spec| req.match?(spec, prerelease) end.map do |spec| source = Gem::Source::Vendor.new(@directories[spec]) Gem::Resolver::VendorSpecification.new(self, spec, source) end end ## # Loads a spec with the given +name+. +version+, +platform+ and +source+ are # ignored. def load_spec(name, version, platform, source) version = Gem::Version.new(version) if !version.is_a?(Gem::Version) @specs.fetch(name, []).detect{|s| s.name == name && s.version == version} end end end end # Patch for Ruby 2.2 and Bundler to behave properly when uninstalling plugins if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3') if defined?(::Bundler) && !::Bundler::SpecSet.instance_methods.include?(:delete) class Gem::Specification def self.remove_spec(spec) Gem::Specification.reset end end end end ================================================ FILE: lib/vagrant/capability_host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant # This module enables a class to host capabilities. Prior to being able # to use any capabilities, the `initialize_capabilities!` method must be # called. # # Capabilities allow small pieces of functionality to be plugged in using # the Vagrant plugin model. Capabilities even allow for a certain amount # of inheritance, where only a subset of capabilities may be implemented but # a parent implements the rest. # # Capabilities are used heavily in Vagrant for host/guest interactions. For # example, "mount_nfs_folder" is a guest-OS specific operation, so capabilities # defer these operations to the guest. module CapabilityHost # Initializes the capability system by detecting the proper capability # host to execute on and building the chain of capabilities to execute. # # @param [Symbol] host The host to use for the capabilities, or nil if # we should auto-detect it. # @param [Hash>] hosts Potential capability # hosts. The key is the name of the host, value[0] is a class that # implements `#detect?` and value[1] is a parent host (if any). # @param [Hash>] capabilities The capabilities # that are supported. The key is the host of the capability. Within that # is a hash where the key is the name of the capability and the value # is the class/module implementing it. def initialize_capabilities!(host, hosts, capabilities, *args) @cap_logger = Log4r::Logger.new( "vagrant::capability_host::#{self.class.to_s.downcase}") if host && !hosts[host] raise Errors::CapabilityHostExplicitNotDetected, value: host.to_s end if !host host = autodetect_capability_host(hosts, *args) if !host raise Errors::CapabilityHostNotDetected if !host end if !hosts[host] # This should never happen because the autodetect above uses the # hosts hash to look up hosts. And if an explicit host is specified, # we do another check higher up. raise "Internal error. Host not found: #{host}" end name = host host_info = hosts[name] host = host_info[0].new chain = [] chain << [name, host] # Build the proper chain of parents if there are any. # This allows us to do "inheritance" of capabilities later if host_info[1] parent_name = host_info[1] parent_info = hosts[parent_name] while parent_info chain << [parent_name, parent_info[0].new] parent_name = parent_info[1] parent_info = hosts[parent_name] end end @cap_host_chain = chain @cap_args = args @cap_caps = capabilities true end # Returns the chain of hosts that will be checked for capabilities. # # @return [Array>] def capability_host_chain @cap_host_chain end # Tests whether the given capability is possible. # # @param [Symbol] cap_name Capability name # @return [Boolean] def capability?(cap_name) !capability_module(cap_name.to_sym).nil? end # Executes the capability with the given name, optionally passing more # arguments onwards to the capability. If the capability returns a value, # it will be returned. # # @param [Symbol] cap_name Name of the capability def capability(cap_name, *args) cap_mod = capability_module(cap_name.to_sym) if !cap_mod raise Errors::CapabilityNotFound, cap: cap_name.to_s, host: @cap_host_chain[0][0].to_s end cap_method = nil begin cap_method = cap_mod.method(cap_name) rescue NameError raise Errors::CapabilityInvalid, cap: cap_name.to_s, host: @cap_host_chain[0][0].to_s end args = @cap_args + args @cap_logger.info( "Execute capability: #{cap_name} #{args.inspect} (#{@cap_host_chain[0][0]})") cap_method.call(*args) end protected def autodetect_capability_host(hosts, *args) @cap_logger.info("Autodetecting host type for #{args.inspect}") # Get the mapping of hosts with the most parents. We start searching # with the hosts with the most parents first. parent_count = {} hosts.each do |name, parts| parent_count[name] = 0 parent = parts[1] while parent parent_count[name] += 1 parent = hosts[parent] parent = parent[1] if parent end end # Now swap around the mapping so that it is a mapping of # count to the actual list of host names parent_count_to_hosts = {} parent_count.each do |name, count| parent_count_to_hosts[count] ||= [] parent_count_to_hosts[count] << name end sorted_counts = parent_count_to_hosts.keys.sort.reverse sorted_counts.each do |count| parent_count_to_hosts[count].each do |name| @cap_logger.debug("Trying: #{name}") host_info = hosts[name] host = host_info[0].new if host.detect?(*args) @cap_logger.info("Detected: #{name}!") return name end end end return nil end # Returns the registered module for a capability with the given name. # # @param [Symbol] cap_name # @return [Module] def capability_module(cap_name) @cap_logger.debug("Searching for cap: #{cap_name}") @cap_host_chain.each do |host_name, host| @cap_logger.debug("Checking in: #{host_name}") caps = @cap_caps[host_name] if caps && caps.key?(cap_name) @cap_logger.debug("Found cap: #{cap_name} in #{host_name}") return caps[cap_name] end end nil end end end ================================================ FILE: lib/vagrant/cli.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' require 'optparse' require 'vagrant/util/experimental' module Vagrant # Manages the command line interface to Vagrant. class CLI < Vagrant.plugin("2", :command) def initialize(argv, env) super @logger = Log4r::Logger.new("vagrant::cli") @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant") @triggers = Vagrant::Plugin::V2::Trigger.new(env, env.vagrantfile.config.trigger, nil, ui) Util::CheckpointClient.instance.setup(env).check @logger.info("CLI: #{@main_args.inspect} #{@sub_command.inspect} #{@sub_args.inspect}") end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Help is next in short-circuiting everything. Print # the help and exit. help return 0 end if @sub_command == "login" $stderr.puts "WARNING: This command has been deprecated and aliased to `vagrant cloud auth login`" end # If we reached this far then we must have a subcommand. If not, # then we also just print the help and exit. command_plugin = nil if @sub_command command_plugin = Vagrant.plugin("2").manager.commands[@sub_command.to_sym] if !command_plugin alias_command = Alias.new(@env).commands[@sub_command.to_sym] if alias_command return alias_command.call(@sub_args) end end end if !command_plugin || !@sub_command help return 1 end command_class = command_plugin[0].call @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") Util::CheckpointClient.instance.display # Initialize and execute the command class, returning the exit status. result = 0 begin @triggers.fire(@sub_command, :before, nil, :command) result = command_class.new(@sub_args, @env).execute @triggers.fire(@sub_command, :after, nil, :command) rescue Interrupt @env.ui.info(I18n.t("vagrant.cli_interrupt")) result = 1 end result = 0 if !result.is_a?(Integer) return result end # This prints out the help for the CLI. def help # We use the optionparser for this. Its just easier. We don't use # an optionparser above because I don't think the performance hits # of creating a whole object are worth checking only a couple flags. opts = OptionParser.new do |o| o.banner = "Usage: vagrant [options] []" o.separator "" o.on("-v", "--version", "Print the version and exit.") o.on("-h", "--help", "Print this help.") o.separator "" o.separator "Common commands:" # Add the available subcommands as separators in order to print them # out as well. commands = {} longest = 0 Vagrant.plugin("2").manager.commands.each do |key, data| # Skip non-primary commands. These only show up in extended # help output. next if !data[1][:primary] key = key.to_s klass = data[0].call commands[key] = klass.synopsis longest = key.length if key.length > longest end commands.keys.sort.each do |key| o.separator " #{key.ljust(longest+2)} #{commands[key]}" @env.ui.machine("cli-command", key.dup) end o.separator "" o.separator "For help on any individual command run `vagrant COMMAND -h`" o.separator "" o.separator "Additional subcommands are available, but are either more advanced" o.separator "or not commonly used. To see all subcommands, run the command" o.separator "`vagrant list-commands`." end @env.ui.info(opts.help, prefix: false) end end end ================================================ FILE: lib/vagrant/config/loader.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "log4r" module Vagrant module Config # This class is responsible for loading Vagrant configuration, # usually in the form of Vagrantfiles. # # Loading works by specifying the sources for the configuration # as well as the order the sources should be loaded. Configuration # set later always overrides those set earlier; this is how # configuration "scoping" is implemented. class Loader # Initializes a configuration loader. # # @param [Registry] versions A registry of the available versions and # their associated loaders. # @param [Array] version_order An array of the order of the versions # in the registry. This is used to determine if upgrades are # necessary. Additionally, the last version in this order is always # considered the "current" version. def initialize(versions, version_order) @logger = Log4r::Logger.new("vagrant::config::loader") @config_cache = {} @proc_cache = {} @sources = {} @versions = versions @version_order = version_order end # Set the configuration data for the given name. # # The `name` should be a symbol and must uniquely identify the data # being given. # # `data` can either be a path to a Ruby Vagrantfile or a `Proc` directly. # `data` can also be an array of such values. # # At this point, no configuration is actually loaded. Note that calling # `set` multiple times with the same name will override any previously # set values. In this way, the last set data for a given name wins. def set(name, sources) # Sources should be an array sources = [sources] if !sources.kind_of?(Array) reliably_inspected_sources = sources.reduce({}) { |accum, source| begin accum[source] = source.inspect rescue Encoding::CompatibilityError accum[source] = "" end accum } @logger.info("Set #{name.inspect} = #{reliably_inspected_sources.values}") # Gather the procs for every source, since that is what we care about. procs = [] sources.each do |source| if !@proc_cache.key?(source) # Load the procs for this source and cache them. This caching # avoids the issue where a file may have side effects when loading # and loading it multiple times causes unexpected behavior. @logger.debug("Populating proc cache for #{reliably_inspected_sources[source]}") @proc_cache[source] = procs_for_source(source, reliably_inspected_sources) end # Add on to the array of procs we're going to use procs.concat(@proc_cache[source]) end # Set this source by name. @sources[name] = procs end # This loads the configuration sources in the given order and returns # an actual configuration object that is ready to be used. # # @param [Array] order The order of configuration to load. # @return [Object] The configuration object. This is different for # each configuration version. def load(order) @logger.info("Loading configuration in order: #{order.inspect}") unknown_sources = @sources.keys - order if !unknown_sources.empty? @logger.warn("Unknown config sources: #{unknown_sources.inspect}") end # Get the current version config class to use current_version = @version_order.last current_config_klass = @versions.get(current_version) # This will hold our result result = current_config_klass.init # Keep track of the warnings and errors that may come from # upgrading the Vagrantfiles warnings = [] errors = [] if !@sources[:root].nil? && @sources[:root].eql?(@sources[:home]) # Vagrants home dir is set to the same dir as its project directory # so we don't want to load and merge the same Vagrantfile config # and execute its settings/procs twice # # Note: This protection won't work if there are two separate but # identical Vagrantfiles in the home and project dir @logger.info("Duplicate Vagrantfile config objects detected in :root and :home.") @sources.delete(:home) @logger.info("Removed :home config from being loaded") end order.each do |key| next if !@sources.key?(key) @sources[key].each do |version, proc| if !@config_cache.key?(proc) @logger.debug("Loading from: #{key} (evaluating)") # Get the proper version loader for this version and load version_loader = @versions.get(version) begin version_config = version_loader.load(proc) rescue NameError => e line = "(unknown)" path = "(unknown)" if e.backtrace && e.backtrace[0] backtrace_tokens = e.backtrace[0].split(":") path = e.backtrace.first.slice(0, e.backtrace.first.rindex(':')).rpartition(':').first backtrace_tokens.each do |part| if part =~ /\d+/ line = part.to_i break end end end raise Errors::VagrantfileNameError, path: path, line: line, message: e.message.sub(/' for .*$/, "'") end # Store the errors/warnings associated with loading this # configuration. We'll store these for later. version_warnings = [] version_errors = [] # If this version is not the current version, then we need # to upgrade to the latest version. if version != current_version @logger.debug("Upgrading config from version #{version} to #{current_version}") version_index = @version_order.index(version) current_index = @version_order.index(current_version) (version_index + 1).upto(current_index) do |index| next_version = @version_order[index] @logger.debug("Upgrading config to version #{next_version}") # Get the loader of this version and ask it to upgrade loader = @versions.get(next_version) upgrade_result = loader.upgrade(version_config) this_warnings = upgrade_result[1] this_errors = upgrade_result[2] @logger.debug("Upgraded to version #{next_version} with " + "#{this_warnings.length} warnings and " + "#{this_errors.length} errors") # Append loading this to the version warnings and errors version_warnings += this_warnings version_errors += this_errors # Store the new upgraded version version_config = upgrade_result[0] end end # Cache the loaded configuration along with any warnings # or errors so that they can be retrieved later. @config_cache[proc] = [version_config, version_warnings, version_errors] else @logger.debug("Loading from: #{key} (cache)") end # Merge the configurations cache_data = @config_cache[proc] result = current_config_klass.merge(result, cache_data[0]) # Append the total warnings/errors warnings += cache_data[1] errors += cache_data[2] end end @logger.debug("Configuration loaded successfully, finalizing and returning") [current_config_klass.finalize(result), warnings, errors] end # This method is used for doing partial loads of the # Vagrantfile. It will load the contents of a single # location and return the config. No merging is performed # and no finalization is applied. # # @param key [Symbol] name of location # @return [Object] configuration # @note: This will load either version, but we assume a v2 result # @todo(spox): check version and raise error on v1 def partial_load(key) raise KeyError, "Unknown path key provided (#{key})" if !@sources.key?(key) version, proc = @sources[key].first @logger.debug("Loading from: #{key} (evaluating)") # Get the proper version loader for this version and load version_loader = @versions.get(version) raise KeyError, "Failed to create loader for requested version: #{version}" if version_loader.nil? begin version_config = version_loader.load(proc) rescue NameError => e line = "(unknown)" path = "(unknown)" if e.backtrace && e.backtrace[0] backtrace_tokens = e.backtrace[0].split(":") path = e.backtrace.first.slice(0, e.backtrace.first.rindex(':')).rpartition(':').first backtrace_tokens.each do |part| if part =~ /\d+/ line = part.to_i break end end end raise Errors::VagrantfileNameError, path: path, line: line, message: e.message.sub(/' for .*$/, "'") + "\n#{e.backtrace.join("\n")}" + "\nVersion #{version.inspect} loader: #{version_loader.inspect} versions: #{@versions.inspect}" end version_config end protected # This returns an array of `Proc` objects for the given source. # The `Proc` objects returned will expect a single argument for # the configuration object and are expected to mutate this # configuration object. def procs_for_source(source, reliably_inspected_sources) # Convert all pathnames to strings so we just have their path source = source.to_s if source.is_a?(Pathname) if source.is_a?(Array) # An array must be formatted as [version, proc], so verify # that and then return it raise ArgumentError, "String source must have format [version, proc]" if source.length != 2 # Return it as an array since we're expected to return an array # of [version, proc] pairs, but an array source only has one. return [source] elsif source.is_a?(String) # Strings are considered paths, so load them return procs_for_path(source) else raise ArgumentError, "Unknown configuration source: #{reliably_inspected_sources[source]}" end end # This returns an array of `Proc` objects for the given path source. # # @param [String] path Path to the file which contains the proper # `Vagrant.configure` calls. # @return [Array] def procs_for_path(path) @logger.debug("Load procs for pathname: #{path}") return Config.capture_configures do begin Kernel.load path rescue SyntaxError => e # Report syntax errors in a nice way. raise Errors::VagrantfileSyntaxError, file: e.message rescue SystemExit # Continue raising that exception... raise rescue Vagrant::Errors::VagrantError # Continue raising known Vagrant errors since they already # contain well worded error messages and context. raise rescue Exception => e @logger.error("Vagrantfile load error: #{e.message}") @logger.error(e.backtrace.join("\n")) line = "(unknown)" if e.backtrace && e.backtrace[0] e.backtrace[0].split(":").each do |part| if part =~ /\d+/ line = part.to_i break end end end # Report the generic exception raise Errors::VagrantfileLoadError, path: path, line: line, exception_class: e.class, message: e.message end end end end end end ================================================ FILE: lib/vagrant/config/v1/dummy_config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Config module V1 # This is a configuration object that can have anything done # to it. Anything, and it just appears to keep working. class DummyConfig def method_missing(name, *args, &block) DummyConfig.new end end end end end ================================================ FILE: lib/vagrant/config/v1/loader.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/config/v1/root" module Vagrant module Config module V1 # This is the loader that handles configuration loading for V1 # configurations. class Loader < VersionBase # Returns a bare empty configuration object. # # @return [V1::Root] def self.init new_root_object end # Finalizes the configuration by making sure there is at least # one VM defined in it. def self.finalize(config) # Call the `#finalize` method on each of the configuration keys. # They're expected to modify themselves in our case. config.finalize! # Return the object config end # Loads the configuration for the given proc and returns a configuration # object. # # @param [Proc] config_proc # @return [Object] def self.load(config_proc) # Create a root configuration object root = new_root_object # Call the proc with the root config_proc.call(root) # Return the root object, which doubles as the configuration object # we actually use for accessing as well. root end # Merges two configuration objects. # # @param [V1::Root] old The older root config. # @param [V1::Root] new The newer root config. # @return [V1::Root] def self.merge(old, new) # Grab the internal states, we use these heavily throughout the process old_state = old.__internal_state new_state = new.__internal_state # The config map for the new object is the old one merged with the # new one. config_map = old_state["config_map"].merge(new_state["config_map"]) # Merge the keys. old_keys = old_state["keys"] new_keys = new_state["keys"] keys = {} old_keys.each do |key, old_value| if new_keys.key?(key) # We need to do a merge, which we expect to be available # on the config class itself. keys[key] = old_value.merge(new_keys[key]) else # We just take the old value, but dup it so that we can modify. keys[key] = old_value.dup end end new_keys.each do |key, new_value| # Add in the keys that the new class has that we haven't merged. if !keys.key?(key) keys[key] = new_value.dup end end # Return the final root object V1::Root.new(config_map, keys) end protected def self.new_root_object # Get all the registered configuration objects and use them. If # we're currently on version 1, then we load all the config objects, # otherwise we load only the upgrade safe ones, since we're # obviously being loaded for an upgrade. config_map = nil plugin_manager = Vagrant.plugin("1").manager if Config::CURRENT_VERSION == "1" config_map = plugin_manager.config else config_map = plugin_manager.config_upgrade_safe end # Create the configuration root object V1::Root.new(config_map) end end end end end ================================================ FILE: lib/vagrant/config/v1/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "set" module Vagrant module Config module V1 # This is the root configuration class. An instance of this is what # is passed into version 1 Vagrant configuration blocks. class Root # Initializes a root object that maps the given keys to specific # configuration classes. # # @param [Hash] config_map Map of key to config class. def initialize(config_map, keys=nil) @keys = keys || {} @config_map = config_map @missing_key_calls = Set.new end # We use method_missing as a way to get the configuration that is # used for Vagrant and load the proper configuration classes for # each. def method_missing(name, *args) return @keys[name] if @keys.key?(name) config_klass = @config_map[name.to_sym] if config_klass # Instantiate the class and return the instance @keys[name] = config_klass.new return @keys[name] else # Record access to a missing key as an error @missing_key_calls.add(name.to_s) return DummyConfig.new end end # Called to finalize this object just prior to it being used by # the Vagrant system. The "!" signifies that this is expected to # mutate itself. def finalize! @keys.each do |_key, instance| instance.finalize! end end # Returns the internal state of the root object. This is used # by outside classes when merging, and shouldn't be called directly. # Note the strange method name is to attempt to avoid any name # clashes with potential configuration keys. def __internal_state { "config_map" => @config_map, "keys" => @keys, "missing_key_calls" => @missing_key_calls } end end end end end ================================================ FILE: lib/vagrant/config/v1.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Config module V1 autoload :DummyConfig, "vagrant/config/v1/dummy_config" autoload :Loader, "vagrant/config/v1/loader" autoload :Root, "vagrant/config/v1/root" end end end ================================================ FILE: lib/vagrant/config/v2/dummy_config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Config module V2 # This is a configuration object that can have anything done # to it. Anything, and it just appears to keep working. class DummyConfig < Vagrant::Plugin::V2::Config LOG = Log4r::Logger.new("vagrant::config::v2::dummy_config") def method_missing(name, *args, &block) # There are a few scenarios where ruby will attempt to implicity # coerce a given object into a certain type. DummyConfigs can end up # in some of these scenarios when they're being shipped around in # callbacks with splats. If method_missing allows these methods to be # called but continues to return DummyConfig back, Ruby will raise a # TypeError. Doing the normal thing of raising NoMethodError allows # DummyConfig to behave normally as its being passed through splats. # # For a bit more detail and some keywords for further searching, see: # https://ruby-doc.org/core-2.7.2/doc/implicit_conversion_rdoc.html if [:to_hash, :to_ary].include?(name) return super end # Trying to define a variable if name.to_s.match(/^[\w]*=/) LOG.debug("found name #{name}") LOG.debug("setting instance variable name #{name.to_s.split("=")[0]}") var_name = "@#{name.to_s.split("=")[0]}" self.instance_variable_set(var_name, args[0]) else DummyConfig.new end end def merge(c) c end def set_options(options) options.each do |key, value| if key.to_s.match(/^[\w]*=/) var_name = "@#{key.to_s}" self.instance_variable_set(var_name, value) end end end def instance_variables_hash instance_variables.inject({}) do |acc, iv| acc[iv.to_s[1..-1]] = instance_variable_get(iv) acc end end end end end end ================================================ FILE: lib/vagrant/config/v2/loader.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/config/v2/root" module Vagrant module Config module V2 # This is the loader that handles configuration loading for V2 # configurations. class Loader < VersionBase # Returns a bare empty configuration object. # # @return [V2::Root] def self.init new_root_object end # Finalizes the configuration by making sure there is at least # one VM defined in it. def self.finalize(config) # Call the `#finalize` method on each of the configuration keys. # They're expected to modify themselves in our case. config.finalize! # Return the object config end # Loads the configuration for the given proc and returns a configuration # object. # # @param [Proc] config_proc # @return [Object] def self.load(config_proc) # Create a root configuration object root = new_root_object # Call the proc with the root config_proc.call(root) # Return the root object, which doubles as the configuration object # we actually use for accessing as well. root end # Merges two configuration objects. # # @param [V2::Root] old The older root config. # @param [V2::Root] new The newer root config. # @return [V2::Root] def self.merge(old, new) # Grab the internal states, we use these heavily throughout the process old_state = old.__internal_state new_state = new.__internal_state # Make sure we instantiate every key in the config so that we # merge every key. This avoids issues with the same reference # being part of the config. old_state["config_map"].each do |k, _| old.public_send(k) end new_state["config_map"].each do |k, _| new.public_send(k) end # The config map for the new object is the old one merged with the # new one. config_map = old_state["config_map"].merge(new_state["config_map"]) # Merge the keys. old_keys = old_state["keys"] new_keys = new_state["keys"] keys = {} old_keys.each do |key, old_value| if new_keys.key?(key) # We need to do a merge, which we expect to be available # on the config class itself. keys[key] = old_value.merge(new_keys[key]) else # We just take the old value, but dup it so that we can modify. keys[key] = old_value.dup end end new_keys.each do |key, new_value| # Add in the keys that the new class has that we haven't merged. if !keys.key?(key) keys[key] = new_value.dup end end # Merge the missing keys new_missing_key_calls = old_state["missing_key_calls"] + new_state["missing_key_calls"] # Return the final root object V2::Root.new(config_map).tap do |result| result.__set_internal_state({ "config_map" => config_map, "keys" => keys, "missing_key_calls" => new_missing_key_calls }) end end # Upgrade a V1 configuration to a V2 configuration. We do this by # creating a V2 configuration, and calling "upgrade" on each of the # V1 configurations, expecting them to set the right settings on the # new root. # # @param [V1::Root] old # @return [Array] A 3-tuple result. def self.upgrade(old) # Get a new root root = new_root_object # Store the warnings/errors warnings = [] errors = [] # Go through the old keys and upgrade them if they can be old.__internal_state["keys"].each do |_, old_value| if old_value.respond_to?(:upgrade) result = old_value.upgrade(root) # Sanity check to guard against random return values if result.is_a?(Array) warnings += result[0] errors += result[1] end end end old.__internal_state["missing_key_calls"].to_a.sort.each do |key| warnings << I18n.t("vagrant.config.loader.bad_v1_key", key: key) end [root, warnings, errors] end protected def self.new_root_object # Get all the registered plugins for V2 config_map = Vagrant.plugin("2").manager.config # Create the configuration root object V2::Root.new(config_map) end end end end end ================================================ FILE: lib/vagrant/config/v2/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "set" require "vagrant/config/v2/util" module Vagrant module Config module V2 # This is the root configuration class. An instance of this is what # is passed into version 2 Vagrant configuration blocks. class Root # Initializes a root object that maps the given keys to specific # configuration classes. # # @param [Hash] config_map Map of key to config class. def initialize(config_map, keys=nil) @keys = keys || {} @config_map = config_map @missing_key_calls = Set.new @logger = Log4r::Logger.new("vagrant::config") end # We use method_missing as a way to get the configuration that is # used for Vagrant and load the proper configuration classes for # each. def method_missing(name, *args) return @keys[name] if @keys.key?(name) config_klass = @config_map[name.to_sym] if config_klass # Instantiate the class and return the instance @keys[name] = config_klass.new else @logger.debug("missing key request name=#{name} loc=#{caller.first}") # Record access to a missing key as an error @missing_key_calls.add(name.to_s) @keys[name] = DummyConfig.new end @keys[name] end # Called to finalize this object just prior to it being used by # the Vagrant system. The "!" signifies that this is expected to # mutate itself. def finalize! @config_map.each do |key, klass| if !@keys.key?(key) @keys[key] = klass.new end end @keys.each do |_key, instance| instance.finalize! instance._finalize! end end # This validates the configuration and returns a hash of error # messages by section. If there are no errors, an empty hash # is returned. # # @param [Environment] env # @return [Hash] def validate(machine, ignore_provider=nil) # Go through each of the configuration keys and validate errors = {} @keys.each do |_key, instance| if instance.respond_to?(:validate) # Validate this single item, and if we have errors then # we merge them into our total errors list. if _key == :vm result = instance.validate(machine, ignore_provider) else result = instance.validate(machine) end if result && !result.empty? errors = Util.merge_errors(errors, result) end end end # Go through and delete empty keys errors.keys.each do |key| errors.delete(key) if errors[key].empty? end # If we have missing keys, record those as errors if !@missing_key_calls.empty? errors["Vagrant"] = @missing_key_calls.to_a.sort.map do |key| I18n.t("vagrant.config.root.bad_key", key: key) end end errors end # Returns the internal state of the root object. This is used # by outside classes when merging, and shouldn't be called directly. # Note the strange method name is to attempt to avoid any name # clashes with potential configuration keys. def __internal_state { "config_map" => @config_map, "keys" => @keys, "missing_key_calls" => @missing_key_calls } end # This sets the internal state. This is used by the core to do some # merging logic and shouldn't be used by the general public. def __set_internal_state(state) @config_map = state["config_map"] if state.key?("config_map") @keys = state["keys"] if state.key?("keys") @missing_key_calls = state["missing_key_calls"] if state.key?("missing_key_calls") end end end end end ================================================ FILE: lib/vagrant/config/v2/util.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Config module V2 class Util # This merges two error hashes from validate methods. # # @param [Hash] first # @param [Hash] second # @return [Hash] Merged result def self.merge_errors(first, second) first.dup.tap do |result| second.each do |key, value| result[key] ||= [] result[key] += value end end end end end end end ================================================ FILE: lib/vagrant/config/v2.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Config module V2 autoload :DummyConfig, "vagrant/config/v2/dummy_config" autoload :Loader, "vagrant/config/v2/loader" autoload :Root, "vagrant/config/v2/root" autoload :Util, "vagrant/config/v2/util" end end end ================================================ FILE: lib/vagrant/config/version_base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Config # This is the base class for any configuration versions, and includes # the stub methods that configuration versions must implement. Vagrant # supports configuration versioning so that backwards compatibility can be # maintained for past Vagrantfiles while newer configurations are added. # Vagrant only introduces new configuration versions for major versions # of Vagrant. class VersionBase # Returns an empty configuration object. This can be any kind of object, # since it is treated as an opaque value on the other side, used only # for things like calling into {merge}. # # @return [Object] def self.init raise NotImplementedError end # This is called just before configuration loading is complete of # a potentially completely-merged value to perform final touch-ups # to the configuration, if required. # # This is an optional method to implement. The default implementation # will simply return the same object. # # This will ONLY be called if this is the version that is being # used. In the case that an `upgrade` is called, this will never # be called. # # @param [Object] obj Final configuration object. # @param [Object] Finalized configuration object. def self.finalize(obj) obj end # Loads the configuration for the given proc and returns a configuration # object. The return value is treated as an opaque object, so it can be # anything you'd like. The return value is the object that is passed # into methods like {merge}, so it should be something you expect. # # @param [Proc] proc The proc that is to be configured. # @return [Object] def self.load(proc) raise NotImplementedError end # Merges two configuration objects, returning the merged object. # The values of `old` and `new` are the opaque objects returned by # {load} or {init}. # # Once again, the return object is treated as an opaque value by # the Vagrant configuration loader, so it can be anything you'd like. # # @param [Object] old Old configuration object. # @param [Object] new New configuration object. # @return [Object] The merged configuration object. def self.merge(old, new) raise NotImplementedError end # This is called if a previous version of configuration needs to be # upgraded to this version. Each version of configuration should know # how to upgrade the version immediately prior to it. This should be # a best effort upgrade that makes many assumptions. The goal is for # this to work in almost every case, but perhaps with some warnings. # The return value for this is a 3-tuple: `[object, warnings, errors]`, # where `object` is the upgraded configuration object, `warnings` is # an array of warning messages, and `errors` is an array of error # messages. # # @param [Object] old The version of the configuration object just # prior to this one. # @return [Array] The 3-tuple result. Please see the above documentation # for more information on the exact structure of this object. def self.upgrade(old) raise NotImplementedError end end end end ================================================ FILE: lib/vagrant/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/registry" module Vagrant module Config autoload :Loader, 'vagrant/config/loader' autoload :VersionBase, 'vagrant/config/version_base' autoload :V1, 'vagrant/config/v1' autoload :V2, 'vagrant/config/v2' # This is a mutex used to guarantee that only one thread can load # procs at any given time. CONFIGURE_MUTEX = Mutex.new # This is the registry which keeps track of what configuration # versions are available, mapped by the version string used in # `Vagrant.configure` calls. VERSIONS = Registry.new VERSIONS.register("1") { V1::Loader } VERSIONS.register("2") { V2::Loader } # This is the order of versions. This is used by the loader to figure out # how to "upgrade" versions up to the desired (current) version. The # current version is always considered to be the last version in this # list. VERSIONS_ORDER = ["1", "2"] CURRENT_VERSION = VERSIONS_ORDER.last # This is the method which is called by all Vagrantfiles to configure Vagrant. # This method expects a block which accepts a single argument representing # an instance of the {Config::Top} class. # # Note that the block is not run immediately. Instead, it's proc is stored # away for execution later. def self.run(version="1", &block) # Store it for later @last_procs ||= [] @last_procs << [version.to_s, block] end # This is a method which will yield to a block and will capture all # ``Vagrant.configure`` calls, returning an array of `Proc`s. # # Wrapping this around anytime you call code which loads configurations # will force a mutex so that procs never get mixed up. This keeps # the configuration loading part of Vagrant thread-safe. def self.capture_configures CONFIGURE_MUTEX.synchronize do # Reset the last procs so that we start fresh @last_procs = [] # Yield to allow the caller to do whatever loading needed yield # Return the last procs we've seen while still in the mutex, # knowing we're safe. return @last_procs end end end end ================================================ FILE: lib/vagrant/environment.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'fileutils' require 'json' require 'pathname' require 'set' require 'thread' require 'log4r' require 'vagrant/util/file_mode' require 'vagrant/util/platform' require 'vagrant/util/hash_with_indifferent_access' require "vagrant/util/silence_warnings" require "vagrant/vagrantfile" require "vagrant/version" module Vagrant # A "Vagrant environment" represents a configuration of how Vagrant # should behave: data directories, working directory, UI output, # etc. In day-to-day usage, every `vagrant` invocation typically # leads to a single Vagrant environment. class Environment # This is the current version that this version of Vagrant is # compatible with in the home directory. # # @return [String] CURRENT_SETUP_VERSION = "1.5" DEFAULT_LOCAL_DATA = ".vagrant" # The `cwd` that this environment represents attr_reader :cwd # The persistent data directory where global data can be stored. It # is up to the creator of the data in this directory to properly # remove it when it is no longer needed. # # @return [Pathname] attr_reader :data_dir # The valid name for a Vagrantfile for this environment. attr_reader :vagrantfile_name # The {UI} object to communicate with the outside world. attr_reader :ui # This is the UI class to use when creating new UIs. attr_reader :ui_class # The directory to the "home" folder that Vagrant will use to store # global state. attr_reader :home_path # The directory to the directory where local, environment-specific # data is stored. attr_reader :local_data_path # The directory where temporary files for Vagrant go. attr_reader :tmp_path # File where command line aliases go. attr_reader :aliases_path # The directory where boxes are stored. attr_reader :boxes_path # The path where the plugins are stored (gems) attr_reader :gems_path # The path to the default private keys directory attr_reader :default_private_keys_directory # The paths for each of the default private keys attr_reader :default_private_key_paths # Initializes a new environment with the given options. The options # is a hash where the main available key is `cwd`, which defines where # the environment represents. There are other options available but # they shouldn't be used in general. If `cwd` is nil, then it defaults # to the `Dir.pwd` (which is the cwd of the executing process). def initialize(opts=nil) opts = { cwd: nil, home_path: nil, local_data_path: nil, ui_class: nil, ui_opts: nil, vagrantfile_name: nil, }.merge(opts || {}) # Set the default working directory to look for the vagrantfile opts[:cwd] ||= ENV["VAGRANT_CWD"] if ENV.key?("VAGRANT_CWD") opts[:cwd] ||= Dir.pwd opts[:cwd] = Pathname.new(opts[:cwd]) if !opts[:cwd].directory? raise Errors::EnvironmentNonExistentCWD, cwd: opts[:cwd].to_s end opts[:cwd] = opts[:cwd].expand_path # Set the default ui class opts[:ui_class] ||= UI::Silent # Set the Vagrantfile name up. We append "Vagrantfile" and "vagrantfile" so that # those continue to work as well, but anything custom will take precedence. opts[:vagrantfile_name] ||= ENV["VAGRANT_VAGRANTFILE"] if \ ENV.key?("VAGRANT_VAGRANTFILE") opts[:vagrantfile_name] = [opts[:vagrantfile_name]] if \ opts[:vagrantfile_name] && !opts[:vagrantfile_name].is_a?(Array) # Set instance variables for all the configuration parameters. @cwd = opts[:cwd] @home_path = opts[:home_path] @vagrantfile_name = opts[:vagrantfile_name] @ui = opts.fetch(:ui, opts[:ui_class].new) @ui_class = opts[:ui_class] if @ui.nil? if opts[:ui_opts].nil? @ui = opts[:ui_class].new else @ui = opts[:ui_class].new(*opts[:ui_opts]) end end # This is the batch lock, that enforces that only one {BatchAction} # runs at a time from {#batch}. @batch_lock = Mutex.new @locks = {} @logger = Log4r::Logger.new("vagrant::environment") @logger.info("Environment initialized (#{self})") @logger.info(" - cwd: #{cwd}") # Setup the home directory @home_path ||= Vagrant.user_data_path @home_path = Util::Platform.fs_real_path(@home_path) @boxes_path = @home_path.join("boxes") @data_dir = @home_path.join("data") @gems_path = Vagrant::Bundler.instance.plugin_gem_path @tmp_path = @home_path.join("tmp") @machine_index_dir = @data_dir.join("machine-index") @aliases_path = Pathname.new(ENV["VAGRANT_ALIAS_FILE"]).expand_path if ENV.key?("VAGRANT_ALIAS_FILE") @aliases_path ||= @home_path.join("aliases") # Prepare the directories setup_home_path # Setup the local data directory. If a configuration path is given, # it is expanded relative to the root path. Otherwise, we use the # default (which is also expanded relative to the root path). if !root_path.nil? if !ENV["VAGRANT_DOTFILE_PATH"].to_s.empty? && !opts[:child] opts[:local_data_path] ||= Pathname.new(File.expand_path(ENV["VAGRANT_DOTFILE_PATH"], root_path)) else opts[:local_data_path] ||= root_path.join(DEFAULT_LOCAL_DATA) end end if opts[:local_data_path] @local_data_path = Pathname.new(File.expand_path(opts[:local_data_path], @cwd)) end @logger.debug("Effective local data path: #{@local_data_path}") # If we have a root path, load the ".vagrantplugins" file. if root_path plugins_file = root_path.join(".vagrantplugins") if plugins_file.file? @logger.info("Loading plugins file: #{plugins_file}") load plugins_file end end setup_local_data_path # Setup the default private key @default_private_key_path = @home_path.join("insecure_private_key") @default_private_keys_directory = @home_path.join("insecure_private_keys") if !@default_private_keys_directory.directory? @default_private_keys_directory.mkdir end @default_private_key_paths = [] copy_insecure_private_keys # Initialize localized plugins plugins = Vagrant::Plugin::Manager.instance.localize!(self) # Load any environment local plugins Vagrant::Plugin::Manager.instance.load_plugins(plugins) # Initialize globalize plugins plugins = Vagrant::Plugin::Manager.instance.globalize! # Load any global plugins Vagrant::Plugin::Manager.instance.load_plugins(plugins) plugins = process_configured_plugins # Call the hooks that does not require configurations to be loaded # by using a "clean" action runner hook(:environment_plugins_loaded, runner: Action::PrimaryRunner.new(env: self)) # Call the environment load hooks hook(:environment_load, runner: Action::PrimaryRunner.new(env: self)) end # The path to the default private key # NOTE: deprecated, used default_private_keys_directory instead def default_private_key_path # TODO(spox): Add deprecation warning @default_private_key_path end # Return a human-friendly string for pretty printed or inspected # instances. # # @return [String] def inspect "#<#{self.class}: #{@cwd}>".encode('external') end # Action runner for executing actions in the context of this environment. # # @return [Action::Runner] def action_runner @action_runner ||= Action::PrimaryRunner.new do { action_runner: action_runner, box_collection: boxes, hook: method(:hook), host: host, machine_index: machine_index, gems_path: gems_path, home_path: home_path, root_path: root_path, tmp_path: tmp_path, ui: @ui, env: self } end end # Returns a list of machines that this environment is currently # managing that physically have been created. # # An "active" machine is a machine that Vagrant manages that has # been created. The machine itself may be in any state such as running, # suspended, etc. but if a machine is "active" then it exists. # # Note that the machines in this array may no longer be present in # the Vagrantfile of this environment. In this case the machine can # be considered an "orphan." Determining which machines are orphan # and which aren't is not currently a supported feature, but will # be in a future version. # # @return [Array] def active_machines # We have no active machines if we have no data path return [] if !@local_data_path machine_folder = @local_data_path.join("machines") # If the machine folder is not a directory then we just return # an empty array since no active machines exist. return [] if !machine_folder.directory? # Traverse the machines folder accumulate a result result = [] machine_folder.children(true).each do |name_folder| # If this isn't a directory then it isn't a machine next if !name_folder.directory? name = name_folder.basename.to_s.to_sym name_folder.children(true).each do |provider_folder| # If this isn't a directory then it isn't a provider next if !provider_folder.directory? # If this machine doesn't have an ID, then ignore next if !provider_folder.join("id").file? provider = provider_folder.basename.to_s.to_sym result << [name, provider] end end # Return the results result end # This creates a new batch action, yielding it, and then running it # once the block is called. # # This handles the case where batch actions are disabled by the # VAGRANT_NO_PARALLEL environmental variable. def batch(parallel=true) parallel = false if ENV["VAGRANT_NO_PARALLEL"] @batch_lock.synchronize do BatchAction.new(parallel).tap do |b| # Yield it so that the caller can setup actions yield b # And run it! b.run end end end # Makes a call to the CLI with the given arguments as if they # came from the real command line (sometimes they do!). An example: # # env.cli("package", "--vagrantfile", "Vagrantfile") # def cli(*args) CLI.new(args.flatten, self).execute end # This returns the provider name for the default provider for this # environment. # # @param check_usable [Boolean] (true) whether to filter for `.usable?` providers # @param exclude [Array] ([]) list of provider names to exclude from # consideration # @param force_default [Boolean] (true) whether to prefer the value of # VAGRANT_DEFAULT_PROVIDER over other strategies if it is set # @param machine [Symbol] (nil) a machine name to scope this lookup # @return [Symbol] Name of the default provider. 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) # Implement the algorithm from # https://www.vagrantup.com/docs/providers/basic_usage.html#default-provider # with additional steps 2.5 and 3.5 from # https://bugzilla.redhat.com/show_bug.cgi?id=1444492 # to allow system-configured provider priorities. # # 1. The --provider flag on a vagrant up is chosen above all else, if it is # present. # # (Step 1 is done by the caller; this method is only called if --provider # wasn't given.) # # 2. If the VAGRANT_DEFAULT_PROVIDER environmental variable is set, it # takes next priority and will be the provider chosen. default = ENV["VAGRANT_DEFAULT_PROVIDER"].to_s if default.empty? default = nil else default = default.to_sym @logger.debug("Default provider: `#{default}`") end # If we're forcing the default, just short-circuit and return # that (the default behavior) if default && opts[:force_default] @logger.debug("Using forced default provider: `#{default}`") return default end # Determine the config to use to look for provider definitions. By # default it is the global but if we're targeting a specific machine, # then look there. root_config = vagrantfile.config if opts[:machine] machine_info = vagrantfile.machine_config(opts[:machine], nil, nil, nil) root_config = machine_info[:config] end # Get the list of providers within our configuration, in order. config = root_config.vm.__providers # Get the list of usable providers with their internally-declared # priorities. usable = [] Vagrant.plugin("2").manager.providers.each do |key, data| impl = data[0] popts = data[1] # Skip excluded providers next if opts[:exclude] && opts[:exclude].include?(key) # Skip providers that can't be defaulted, unless they're in our # config, in which case someone made our decision for us. if !config.include?(key) next if popts.key?(:defaultable) && !popts[:defaultable] end # Skip providers that aren't usable. next if opts[:check_usable] && !impl.usable?(false) # Each provider sets its own priority, defaulting to 5 so we can trust # it's always set. usable << [popts[:priority], key] end @logger.debug("Initial usable provider list: #{usable}") # Sort the usable providers by priority. Higher numbers are higher # priority, otherwise alpha sort. usable = usable.sort {|a, b| a[0] == b[0] ? a[1] <=> b[1] : b[0] <=> a[0]} .map {|prio, key| key} @logger.debug("Priority sorted usable provider list: #{usable}") # If we're not forcing the default, but it's usable and hasn't been # otherwise excluded, return it now. if usable.include?(default) @logger.debug("Using default provider `#{default}` as it was found in usable list.") return default end # 2.5. Vagrant will go through all of the config.vm.provider calls in the # Vagrantfile and try each in order. It will choose the first # provider that is usable and listed in VAGRANT_PREFERRED_PROVIDERS. preferred = ENV.fetch('VAGRANT_PREFERRED_PROVIDERS', '') .split(',') .map {|s| s.strip} .select {|s| !s.empty?} .map {|s| s.to_sym} @logger.debug("Preferred provider list: #{preferred}") config.each do |key| if usable.include?(key) && preferred.include?(key) @logger.debug("Using preferred provider `#{key}` detected in configuration and usable.") return key end end # 3. Vagrant will go through all of the config.vm.provider calls in the # Vagrantfile and try each in order. It will choose the first provider # that is usable. For example, if you configure Hyper-V, it will never # be chosen on Mac this way. It must be both configured and usable. config.each do |key| if usable.include?(key) @logger.debug("Using provider `#{key}` detected in configuration and usable.") return key end end # 3.5. Vagrant will go through VAGRANT_PREFERRED_PROVIDERS and find the # first plugin that reports it is usable. preferred.each do |key| if usable.include?(key) @logger.debug("Using preferred provider `#{key}` found in usable list.") return key end end # 4. Vagrant will go through all installed provider plugins (including the # ones that come with Vagrant), and find the first plugin that reports # it is usable. There is a priority system here: systems that are known # better have a higher priority than systems that are worse. For # example, if you have the VMware provider installed, it will always # take priority over VirtualBox. if !usable.empty? @logger.debug("Using provider `#{usable[0]}` as it is the highest priority in the usable list.") return usable[0] end # 5. If Vagrant still has not found any usable providers, it will error. # No providers available is a critical error for 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] def boxes @_boxes ||= BoxCollection.new( boxes_path, hook: method(:hook), temp_dir_root: tmp_path) end # Returns the {Config::Loader} that can be used to load Vagrantfiles # given the settings of this environment. # # @return [Config::Loader] def config_loader return @config_loader if @config_loader home_vagrantfile = nil root_vagrantfile = nil home_vagrantfile = find_vagrantfile(home_path) if home_path if root_path root_vagrantfile = find_vagrantfile(root_path, @vagrantfile_name) end @config_loader = Config::Loader.new( Config::VERSIONS, Config::VERSIONS_ORDER) @config_loader.set(:home, home_vagrantfile) if home_vagrantfile @config_loader.set(:root, root_vagrantfile) if root_vagrantfile @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 overridden 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({ child: true, 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. # # @param [Symbol] name Name of the hook. # @param [Action::Runner] action_runner A custom action runner for running hooks. def hook(name, opts=nil) @logger.info("Running hook: #{name}") opts ||= {} opts[:callable] ||= Action::Builder.new opts[:runner] ||= action_runner opts[:action_name] = name opts[:env] = self opts.delete(:runner).run(opts.delete(:callable), opts) end # Returns the host object associated with this environment. # # @return [Class] def host return @host if defined?(@host) # Determine the host class to use. ":detect" is an old Vagrant config # that shouldn't be valid anymore, but we respect it here by assuming # its old behavior. No need to deprecate this because I think it is # fairly harmless. host_klass = vagrantfile.config.vagrant.host host_klass = nil if host_klass == :detect begin @host = Host.new( host_klass, Vagrant.plugin("2").manager.hosts, Vagrant.plugin("2").manager.host_capabilities, self) rescue Errors::CapabilityHostNotDetected # If the auto-detect failed, then we create a brand new host # with no capabilities and use that. This should almost never happen # since Vagrant works on most host OS's now, so this is a "slow path" klass = Class.new(Vagrant.plugin("2", :host)) do def detect?(env); true; end end hosts = { generic: [klass, nil] } host_caps = {} @host = Host.new(:generic, hosts, host_caps, self) rescue Errors::CapabilityHostExplicitNotDetected => e raise Errors::HostExplicitNotDetected, e.extra_data end end # This acquires a process-level lock with the given name. # # The lock file is held within the data directory of this environment, # so make sure that all environments that are locking are sharing # the same data directory. # # This will raise Errors::EnvironmentLockedError if the lock can't # be obtained. # # @param [String] name Name of the lock, since multiple locks can # be held at one time. def lock(name="global", **opts) f = nil # If we don't have a block, then locking is useless, so ignore it return if !block_given? # This allows multiple locks in the same process to be nested return yield if @locks[name] || opts[:noop] # The path to this lock lock_path = data_dir.join("lock.#{name}.lock") @logger.debug("Attempting to acquire process-lock: #{name}") lock("dotlock", noop: name == "dotlock", retry: true) do f = File.open(lock_path, "w+") end # The file locking fails only if it returns "false." If it # succeeds it returns a 0, so we must explicitly check for # the proper error case. while f.flock(File::LOCK_EX | File::LOCK_NB) === false @logger.warn("Process-lock in use: #{name}") if !opts[:retry] raise Errors::EnvironmentLockedError, name: name end sleep 0.2 end @logger.info("Acquired process lock: #{name}") result = nil begin # Mark that we have a lock @locks[name] = true result = yield ensure # We need to make sure that no matter what this is always # reset to false so we don't think we have a lock when we # actually don't. @locks.delete(name) @logger.info("Released process lock: #{name}") end # Clean up the lock file, this requires another lock if name != "dotlock" lock("dotlock", retry: true) do f.close begin File.delete(lock_path) rescue @logger.error( "Failed to delete lock file #{lock_path} - some other thread " + "might be trying to acquire it. ignoring this error") end end end # Return the result return result ensure begin f.close if f rescue IOError end end # This executes the push with the given name, raising any exceptions that # occur. # # @param name [String] Push plugin name # @param manager [Vagrant::Plugin::Manager] Plugin Manager to use, # defaults to the primary one registered but parameterized so it can be # overridden in server mode # # @see VagrantPlugins::CommandServe::Service::PushService Server mode behavior # # Precondition: the push is not nil and exists. def push(name, manager: Vagrant.plugin("2").manager) @logger.info("Getting push: #{name}") name = name.to_sym pushes = self.vagrantfile.config.push.__compiled_pushes if !pushes.key?(name) raise Vagrant::Errors::PushStrategyNotDefined, name: name, pushes: pushes.keys end strategy, config = pushes[name] push_registry = manager.pushes klass, _ = push_registry.get(strategy) if klass.nil? raise Vagrant::Errors::PushStrategyNotLoaded, name: strategy, pushes: push_registry.keys end klass.new(self, config).push end # The list of pushes defined in this Vagrantfile. # # @return [Array] def pushes self.vagrantfile.config.push.__compiled_pushes.keys end # This returns a machine with the proper provider for this environment. # The machine named by `name` must be in this environment. # # @param [Symbol] name Name of the machine (as configured in the # Vagrantfile). # @param [Symbol] provider The provider that this machine should be # backed by. # @param [Boolean] refresh If true, then if there is a cached version # it is reloaded. # @return [Machine] def machine(name, provider, refresh=false) @logger.info("Getting machine: #{name} (#{provider})") # Compose the cache key of the name and provider, and return from # the cache if we have that. cache_key = [name, provider] @machines ||= {} if refresh @logger.info("Refreshing machine (busting cache): #{name} (#{provider})") @machines.delete(cache_key) end if @machines.key?(cache_key) @logger.info("Returning cached machine: #{name} (#{provider})") return @machines[cache_key] end @logger.info("Uncached load of machine.") # Determine the machine data directory and pass it to the machine. machine_data_path = @local_data_path.join( "machines/#{name}/#{provider}") # Create the machine and cache it for future calls. This will also # return the machine from this method. @machines[cache_key] = vagrantfile.machine( name, provider, boxes, machine_data_path, self) end # The {MachineIndex} to store information about the machines. # # @return [MachineIndex] def machine_index @machine_index ||= MachineIndex.new(@machine_index_dir) end # This returns a list of the configured machines for this environment. # Each of the names returned by this method is valid to be used with # the {#machine} method. # # @return [Array] Configured machine names. def machine_names vagrantfile.machine_names end # This returns the name of the machine that is the "primary." In the # case of a single-machine environment, this is just the single machine # name. In the case of a multi-machine environment, then this can # potentially be nil if no primary machine is specified. # # @return [Symbol] def primary_machine_name vagrantfile.primary_machine_name end # The root path is the path where the top-most (loaded last) # Vagrantfile resides. It can be considered the project root for # this environment. # # @return [String] def root_path return @root_path if defined?(@root_path) root_finder = lambda do |path| # Note: To remain compatible with Ruby 1.8, we have to use # a `find` here instead of an `each`. vf = find_vagrantfile(path, @vagrantfile_name) return path if vf return nil if path.root? || !File.exist?(path) root_finder.call(path.parent) end @root_path = root_finder.call(cwd) end # Unload the environment, running completion hooks. The environment # should not be used after this (but CAN be, technically). It is # recommended to always immediately set the variable to `nil` after # running this so you can't accidentally run any more methods. Example: # # env.unload # env = nil # def unload hook(:environment_unload) end # Represents the default Vagrantfile, or the Vagrantfile that is # in the working directory or a parent of the working directory # of this environment. # # The existence of this function is primarily a convenience. There # is nothing stopping you from instantiating your own {Vagrantfile} # and loading machines in any way you see fit. Typical behavior of # Vagrant, however, loads this Vagrantfile. # # This Vagrantfile is comprised of two major sources: the Vagrantfile # in the user's home directory as well as the "root" Vagrantfile or # the Vagrantfile in the working directory (or parent). # # @return [Vagrantfile] def vagrantfile @vagrantfile ||= Vagrantfile.new(config_loader, [:home, :root]) end #--------------------------------------------------------------- # Load Methods #--------------------------------------------------------------- # This sets the `@home_path` variable properly. # # @return [Pathname] def setup_home_path @logger.info("Home path: #{@home_path}") # Setup the list of child directories that need to be created if they # don't already exist. dirs = [ @home_path, @home_path.join("rgloader"), @boxes_path, @data_dir, @gems_path, @tmp_path, @machine_index_dir, ] # Go through each required directory, creating it if it doesn't exist dirs.each do |dir| next if File.directory?(dir) begin @logger.info("Creating: #{dir}") FileUtils.mkdir_p(dir) rescue Errno::EACCES, Errno::EROFS raise Errors::HomeDirectoryNotAccessible, home_path: @home_path.to_s end end # Attempt to write into the home directory to verify we can begin # Append a random suffix to avoid race conditions if Vagrant # is running in parallel with other Vagrant processes. suffix = (0...32).map { (65 + rand(26)).chr }.join path = @home_path.join("perm_test_#{suffix}") path.open("w") do |f| f.write("hello") end path.unlink rescue Errno::EACCES raise Errors::HomeDirectoryNotAccessible, home_path: @home_path.to_s end # Create the version file that we use to track the structure of # the home directory. If we have an old version, we need to explicitly # upgrade it. Otherwise, we just mark that it's the current version. version_file = @home_path.join("setup_version") if version_file.file? version = version_file.read.chomp if version > CURRENT_SETUP_VERSION raise Errors::HomeDirectoryLaterVersion end case version when CURRENT_SETUP_VERSION # We're already good, at the latest version. when "1.1" # We need to update our directory structure upgrade_home_path_v1_1 # Delete the version file so we put our latest version in version_file.delete else raise Errors::HomeDirectoryUnknownVersion, path: @home_path.to_s, version: version end end if !version_file.file? @logger.debug( "Creating home directory version file: #{CURRENT_SETUP_VERSION}") version_file.open("w") do |f| f.write(CURRENT_SETUP_VERSION) end end # Create the rgloader/loader file so we can use encoded files. loader_file = @home_path.join("rgloader", "loader.rb") if !loader_file.file? source_loader = Vagrant.source_root.join("templates/rgloader.rb") FileUtils.cp(source_loader.to_s, loader_file.to_s) end end # This creates the local data directory and show an error if it # couldn't properly be created. def setup_local_data_path(force=false) if @local_data_path.nil? @logger.warn("No local data path is set. Local data cannot be stored.") return end @logger.info("Local data path: #{@local_data_path}") # If the local data path is a file, then we are probably seeing an # old (V1) "dotfile." In this case, we upgrade it. The upgrade process # will remove the old data file if it is successful. if @local_data_path.file? upgrade_v1_dotfile(@local_data_path) end # If we don't have a root path, we don't setup anything return if !force && root_path.nil? begin @logger.debug("Creating: #{@local_data_path}") FileUtils.mkdir_p(@local_data_path) # Create the rgloader/loader file so we can use encoded files. loader_file = @local_data_path.join("rgloader", "loader.rb") if !loader_file.file? source_loader = Vagrant.source_root.join("templates/rgloader.rb") FileUtils.mkdir_p(@local_data_path.join("rgloader").to_s) FileUtils.cp(source_loader.to_s, loader_file.to_s) end rescue Errno::EACCES raise Errors::LocalDataDirectoryNotAccessible, local_data_path: @local_data_path.to_s end end protected # Attempt to guess the configured provider in use. Will fallback # to the default provider if an explicit provider name is not # provided. This can be pretty error prone, but is used during # initial environment setup to allow loading plugins so it doesn't # need to be perfect # # @return [String] def guess_provider gp = nil ARGV.each_with_index do |val, idx| if val.start_with?("--provider=") gp = val.split("=", 2).last break elsif val == "--provider" gp = ARGV[idx+1] break end end return gp.to_sym if gp begin default_provider rescue Errors::NoDefaultProvider # if a provider cannot be determined just return nil nil end end # Load any configuration provided by guests defined within # the Vagrantfile to pull plugin information they may have # defined. def find_configured_plugins plugins = [] provider = guess_provider vagrantfile.machine_names.each do |mname| ldp = @local_data_path.join("machines/#{mname}/#{provider}") if @local_data_path plugins << vagrantfile.machine_config(mname, provider, boxes, ldp, false)[:config] end result = plugins.reverse.inject(Vagrant::Util::HashWithIndifferentAccess.new) do |memo, val| Vagrant::Util::DeepMerge.deep_merge(memo, val.vagrant.plugins) end Vagrant::Util::DeepMerge.deep_merge(result, vagrantfile.config.vagrant.plugins) end # Check for any local plugins defined within the Vagrantfile. If # found, validate they are available. If they are not available, # request to install them, or raise an exception # # @return [Hash] plugin list for loading def process_configured_plugins return if !Vagrant.plugins_enabled? errors = vagrantfile.config.vagrant.validate(nil) if !Array(errors["vagrant"]).empty? raise Errors::ConfigInvalid, errors: Util::TemplateRenderer.render( "config/validation_failed", errors: {vagrant: errors["vagrant"]} ) end # Check if defined plugins are installed installed = Plugin::Manager.instance.installed_plugins needs_install = [] config_plugins = find_configured_plugins config_plugins.each do |name, info| if !installed[name] needs_install << name end end if !needs_install.empty? ui.warn(I18n.t("vagrant.plugins.local.uninstalled_plugins", plugins: needs_install.sort.join(", "))) if !Vagrant.auto_install_local_plugins? answer = nil until ["y", "n"].include?(answer) answer = ui.ask(I18n.t("vagrant.plugins.local.request_plugin_install") + " [N]: ") answer = answer.strip.downcase answer = "n" if answer.to_s.empty? end if answer == "n" raise Errors::PluginMissingLocalError, plugins: needs_install.sort.join(", ") end end needs_install.each do |name| pconfig = Util::HashWithIndifferentAccess.new(config_plugins[name]) ui.info(I18n.t("vagrant.commands.plugin.installing", name: name)) options = {sources: Vagrant::Bundler::DEFAULT_GEM_SOURCES.dup, env_local: true} options[:sources] = pconfig[:sources] if pconfig[:sources] options[:require] = pconfig[:entry_point] if pconfig[:entry_point] options[:version] = pconfig[:version] if pconfig[:version] spec = Plugin::Manager.instance.install_plugin(name, **options) ui.info(I18n.t("vagrant.commands.plugin.installed", name: spec.name, version: spec.version.to_s)) end ui.info("\n") # Force halt after installation and require command to be run again. This # will properly load any new locally installed plugins which are now available. ui.warn(I18n.t("vagrant.plugins.local.install_rerun_command")) exit(-1) end if Vagrant::Plugin::Manager.instance.local_file Vagrant::Plugin::Manager.instance.local_file.installed_plugins else {} end end # This method copies the private keys into the home directory if they # do not already exist. The `default_private_key_path` references the # original rsa based private key and is retained for compatibility. The # `default_private_keys_directory` contains the list of valid private # keys supported by Vagrant. # # NOTE: The keys are copied because `ssh` requires that the key is chmod # 0600, but if Vagrant is installed as a separate user, then the # effective uid won't be able to read the key. So the key is copied # to the home directory and chmod 0600. def copy_insecure_private_keys # First setup the deprecated single key path if !@default_private_key_path.exist? @logger.info("Copying private key to home directory") source = File.expand_path("keys/vagrant", Vagrant.source_root) destination = @default_private_key_path begin FileUtils.cp(source, destination) rescue Errno::EACCES raise Errors::CopyPrivateKeyFailed, source: source, destination: destination end end if !Util::Platform.windows? # On Windows, permissions don't matter as much, so don't worry # about doing chmod. if Util::FileMode.from_octal(@default_private_key_path.stat.mode) != "600" @logger.info("Changing permissions on private key to 0600") @default_private_key_path.chmod(0600) end end # Now setup the key directory Dir.glob(File.expand_path("keys/vagrant.key.*", Vagrant.source_root)).each do |source| destination = default_private_keys_directory.join(File.basename(source)) default_private_key_paths << destination next if File.exist?(destination) begin FileUtils.cp(source, destination) rescue Errno::EACCES raise Errors::CopyPrivateKeyFailed, source: source, destination: destination end end if !Util::Platform.windows? default_private_key_paths.each do |key_path| if Util::FileMode.from_octal(key_path.stat.mode) != "600" @logger.info("Changing permissions on private key (#{key_path}) to 0600") key_path.chmod(0600) end end end end # Finds the Vagrantfile in the given directory. # # @param [Pathname] path Path to search in. # @return [Pathname] def find_vagrantfile(search_path, filenames=nil) filenames ||= ["Vagrantfile", "vagrantfile"] filenames.each do |vagrantfile| current_path = search_path.join(vagrantfile) return current_path if current_path.file? end 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 if !ENV["VAGRANT_UPGRADE_SILENT_1_5"] @ui.ask(I18n.t("vagrant.upgrading_home_path_v1_5")) end collection = BoxCollection.new( @home_path.join("boxes"), temp_dir_root: tmp_path) collection.upgrade_v1_1_v1_5 end # This upgrades a Vagrant 1.0.x "dotfile" to the new V2 format. # # This is a destructive process. Once the upgrade is complete, the # old dotfile is removed, and the environment becomes incompatible for # Vagrant 1.0 environments. # # @param [Pathname] path The path to the dotfile def upgrade_v1_dotfile(path) @logger.info("Upgrading V1 dotfile to V2 directory structure...") # First, verify the file isn't empty. If it is an empty file, we # just delete it and go on with life. contents = path.read.strip if contents.strip == "" @logger.info("V1 dotfile was empty. Removing and moving on.") path.delete return end # Otherwise, verify there is valid JSON in here since a Vagrant # environment would always ensure valid JSON. This is a sanity check # to make sure we don't nuke a dotfile that is not ours... @logger.debug("Attempting to parse JSON of V1 file") json_data = nil begin json_data = JSON.parse(contents) @logger.debug("JSON parsed successfully. Things are okay.") rescue JSON::ParserError # The file could've been tampered with since Vagrant 1.0.x is # supposed to ensure that the contents are valid JSON. Show an error. raise Errors::DotfileUpgradeJSONError, state_file: path.to_s end # Alright, let's upgrade this guy to the new structure. Start by # backing up the old dotfile. backup_file = path.dirname.join(".vagrant.v1.#{Time.now.to_i}") @logger.info("Renaming old dotfile to: #{backup_file}") path.rename(backup_file) # Now, we create the actual local data directory. This should succeed # this time since we renamed the old conflicting V1. setup_local_data_path(true) if json_data["active"] @logger.debug("Upgrading to V2 style for each active VM") json_data["active"].each do |name, id| @logger.info("Upgrading dotfile: #{name} (#{id})") # Create the machine configuration directory directory = @local_data_path.join("machines/#{name}/virtualbox") FileUtils.mkdir_p(directory) # Write the ID file directory.join("id").open("w+") do |f| f.write(id) end end end # Upgrade complete! Let the user know @ui.info(I18n.t("vagrant.general.upgraded_v1_dotfile", backup_path: backup_file.to_s)) end end end ================================================ FILE: lib/vagrant/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This file contains all of the internal errors in Vagrant's core # commands, actions, etc. module Vagrant # This module contains all of the internal errors in Vagrant's core. # These errors are _expected_ errors and as such don't typically represent # bugs in Vagrant itself. These are meant as a way to detect errors and # display them in a user-friendly way. # # # Defining a new Error # # To define a new error, inherit from {VagrantError}, which lets Vagrant # know that this is an expected error, and also gives you some helpers for # providing exit codes and error messages. An example is shown below, then # it is explained: # # class MyError < Vagrant::Errors::VagrantError # error_key "my_error" # end # # This creates an error with an I18n error key of "my_error." {VagrantError} # uses I18n to look up error messages, in the "vagrant.errors" namespace. So # in the above, the error message would be the translation of "vagrant.errors.my_error" # # If you don't want to use I18n, you can override the {#initialize} method and # set your own error message. # # # Raising an Error # # To raise an error, it is nothing special, just raise it like any normal # exception: # # raise MyError.new # # Eventually this exception will bubble out to the `vagrant` binary which # will show a nice error message. And if it is raised in the middle of a # middleware sequence, then {Action::Warden} will catch it and begin the # recovery process prior to exiting. module Errors # Main superclass of any errors in Vagrant. This provides some # convenience methods for setting the status code and error key. # The status code is used by the `vagrant` executable as the # error code, and the error key is used as a default message from # I18n. class VagrantError < StandardError # This is extra data passed into the message for translation. attr_accessor :extra_data def self.error_key(key=nil, namespace=nil) define_method(:error_key) { key } error_namespace(namespace) if namespace end def self.error_message(message) define_method(:error_message) { message } end def self.error_namespace(namespace) define_method(:error_namespace) { namespace } end def initialize(*args) key = args.shift if args.first.is_a?(Symbol) message = args.shift if args.first.is_a?(Hash) message ||= {} @extra_data = message.dup message[:_key] ||= error_key message[:_namespace] ||= error_namespace message[:_key] = key if key if message[:_key] message = translate_error(message) else message = error_message end super(message) end # The error message for this error. This is used if no error_key # is specified for a translatable error message. def error_message; "No error message"; end # The default error namespace which is used for the error key. # This can be overridden here or by calling the "error_namespace" # class method. def error_namespace; "vagrant.errors"; end # The key for the error message. This should be set using the # {error_key} method but can be overridden here if needed. def error_key; nil; end # This is the exit code that should be used when exiting from # this exception. # # @return [Integer] def status_code; 1; end protected def translate_error(opts) return nil if !opts[:_key] I18n.t("#{opts[:_namespace]}.#{opts[:_key]}", **opts) end end class ActiveMachineWithDifferentProvider < VagrantError error_key(:active_machine_with_different_provider) end class AliasInvalidError < VagrantError error_key(:alias_invalid_error) end class BatchMultiError < VagrantError error_key(:batch_multi_error) end class BoxAddDirectVersion < VagrantError error_key(:box_add_direct_version) end class BoxAddMetadataMultiURL < VagrantError error_key(:box_add_metadata_multi_url) end class BoxAddNameMismatch < VagrantError error_key(:box_add_name_mismatch) end class BoxAddNameRequired < VagrantError error_key(:box_add_name_required) end class BoxAddNoMatchingProvider < VagrantError error_key(:box_add_no_matching_provider) end class BoxAddNoArchitectureSupport < VagrantError error_key(:box_add_no_architecture_support) end class BoxAddNoMatchingArchitecture < VagrantError error_key(:box_add_no_matching_architecture) end class BoxAddNoMatchingProviderVersion < VagrantError error_key(:box_add_no_matching_provider_version) end class BoxAddNoMatchingVersion < VagrantError error_key(:box_add_no_matching_version) end class BoxAddShortNotFound < VagrantError error_key(:box_add_short_not_found) end class BoxAlreadyExists < VagrantError error_key(:box_add_exists) end class BoxChecksumInvalidType < VagrantError error_key(:box_checksum_invalid_type) end class BoxChecksumMismatch < VagrantError error_key(:box_checksum_mismatch) end class BoxConfigChangingBox < VagrantError error_key(:box_config_changing_box) end class BoxFileNotExist < VagrantError error_key(:box_file_not_exist) end class BoxMetadataCorrupted < VagrantError error_key(:box_metadata_corrupted) end class BoxMetadataMissingRequiredFields < VagrantError error_key(:box_metadata_missing_required_fields) end class BoxMetadataDownloadError < VagrantError error_key(:box_metadata_download_error) end class BoxMetadataFileNotFound < VagrantError error_key(:box_metadata_file_not_found) end class BoxMetadataMalformed < VagrantError error_key(:box_metadata_malformed) end class BoxMetadataMalformedVersion < VagrantError error_key(:box_metadata_malformed_version) end class BoxNotFound < VagrantError error_key(:box_not_found) end class BoxNotFoundWithProvider < VagrantError error_key(:box_not_found_with_provider) end class BoxNotFoundWithProviderArchitecture < VagrantError error_key(:box_not_found_with_provider_architecture) end class BoxNotFoundWithProviderAndVersion < VagrantError error_key(:box_not_found_with_provider_and_version) end class BoxProviderDoesntMatch < VagrantError error_key(:box_provider_doesnt_match) end class BoxRemoveNotFound < VagrantError error_key(:box_remove_not_found) end class BoxRemoveArchitectureNotFound < VagrantError error_key(:box_remove_architecture_not_found) end class BoxRemoveProviderNotFound < VagrantError error_key(:box_remove_provider_not_found) end class BoxRemoveVersionNotFound < VagrantError error_key(:box_remove_version_not_found) end class BoxRemoveMultiArchitecture < VagrantError error_key(:box_remove_multi_architecture) end class BoxRemoveMultiProvider < VagrantError error_key(:box_remove_multi_provider) end class BoxRemoveMultiVersion < VagrantError error_key(:box_remove_multi_version) end class BoxServerNotSet < VagrantError error_key(:box_server_not_set) end class BoxUnpackageFailure < VagrantError error_key(:untar_failure, "vagrant.actions.box.unpackage") end class BoxUpdateMultiProvider < VagrantError error_key(:box_update_multi_provider) end class BoxUpdateMultiArchitecture < VagrantError error_key(:box_update_multi_architecture) end class BoxUpdateNoMetadata < VagrantError error_key(:box_update_no_metadata) end class BoxVerificationFailed < VagrantError error_key(:failed, "vagrant.actions.box.verify") end class BoxVersionInvalid < VagrantError error_key(:box_version_invalid) end class BundlerDisabled < VagrantError error_key(:bundler_disabled) end class BundlerError < VagrantError error_key(:bundler_error) end class SourceSpecNotFound < BundlerError error_key(:source_spec_not_found) end class CantReadMACAddresses < VagrantError error_key(:cant_read_mac_addresses) end class CapabilityHostExplicitNotDetected < VagrantError error_key(:capability_host_explicit_not_detected) end class CapabilityHostNotDetected < VagrantError error_key(:capability_host_not_detected) end class CapabilityInvalid < VagrantError error_key(:capability_invalid) end class CapabilityNotFound < VagrantError error_key(:capability_not_found) end class CFEngineBootstrapFailed < VagrantError error_key(:cfengine_bootstrap_failed) end class CFEngineCantAutodetectIP < VagrantError error_key(:cfengine_cant_autodetect_ip) end class CFEngineInstallFailed < VagrantError error_key(:cfengine_install_failed) end class CFEngineNotInstalled < VagrantError error_key(:cfengine_not_installed) end class CLIInvalidUsage < VagrantError error_key(:cli_invalid_usage) end class CLIInvalidOptions < VagrantError 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 CloudInitNotFound < VagrantError error_key(:cloud_init_not_found) end class CloudInitCommandFailed < VagrantError error_key(:cloud_init_command_failed) end class CommandDeprecated < VagrantError error_key(:command_deprecated) end class CommandSuspendAllArgs < VagrantError error_key(:command_suspend_all_arguments) end class CommandUnavailable < VagrantError error_key(:command_unavailable) end class CommandUnavailableWindows < CommandUnavailable error_key(:command_unavailable_windows) end class CommunicatorNotFound < VagrantError error_key(:communicator_not_found) end class ConfigInvalid < VagrantError error_key(:config_invalid) end class ConfigUpgradeErrors < VagrantError error_key(:config_upgrade_errors) end class CopyPrivateKeyFailed < VagrantError error_key(:copy_private_key_failed) end class CorruptMachineIndex < VagrantError error_key(:corrupt_machine_index) end class CreateIsoHostCapNotFound < VagrantError error_key(:create_iso_host_cap_not_found) end class DarwinMountFailed < VagrantError error_key(:darwin_mount_failed) end class DarwinVersionFailed < VagrantError error_key(:darwin_version_failed) end class DestroyRequiresForce < VagrantError error_key(:destroy_requires_force) end class DotfileUpgradeJSONError < VagrantError error_key(:dotfile_upgrade_json_error) end class DownloadAlreadyInProgress < VagrantError error_key(:download_already_in_progress_error) end class DownloaderError < VagrantError error_key(:downloader_error) end class DownloaderInterrupted < DownloaderError error_key(:downloader_interrupted) end class DownloaderChecksumError < VagrantError error_key(:downloader_checksum_error) end class EnvInval < VagrantError error_key(:env_inval) end class EnvironmentNonExistentCWD < VagrantError error_key(:environment_non_existent_cwd) end class EnvironmentLockedError < VagrantError error_key(:environment_locked) end class HomeDirectoryLaterVersion < VagrantError error_key(:home_dir_later_version) end class HomeDirectoryNotAccessible < VagrantError error_key(:home_dir_not_accessible) end class HomeDirectoryUnknownVersion < VagrantError error_key(:home_dir_unknown_version) end class HypervVirtualBoxError < VagrantError error_key(:hyperv_virtualbox_error) end class ForwardPortAdapterNotFound < VagrantError error_key(:forward_port_adapter_not_found) end class ForwardPortAutolistEmpty < VagrantError error_key(:auto_empty, "vagrant.actions.vm.forward_ports") end class ForwardPortHostIPNotFound < VagrantError error_key(:host_ip_not_found, "vagrant.actions.vm.forward_ports") end class ForwardPortCollision < VagrantError error_key(:collision_error, "vagrant.actions.vm.forward_ports") end class GuestCapabilityInvalid < VagrantError error_key(:guest_capability_invalid) end class GuestCapabilityNotFound < VagrantError error_key(:guest_capability_not_found) end class GuestExplicitNotDetected < VagrantError error_key(:guest_explicit_not_detected) end class GuestNotDetected < VagrantError error_key(:guest_not_detected) end class HostExplicitNotDetected < VagrantError error_key(:host_explicit_not_detected) end class ISOBuildFailed < VagrantError error_key(:iso_build_failed) end class LinuxMountFailed < VagrantError error_key(:linux_mount_failed) end class LinuxRDPClientNotFound < VagrantError error_key(:linux_rdp_client_not_found) end class LocalDataDirectoryNotAccessible < VagrantError error_key(:local_data_dir_not_accessible) end class MachineActionLockedError < VagrantError error_key(:machine_action_locked) end class MachineFolderNotAccessible < VagrantError error_key(:machine_folder_not_accessible) end class MachineGuestNotReady < VagrantError error_key(:machine_guest_not_ready) end class MachineLocked < VagrantError error_key(:machine_locked) end class MachineNotFound < VagrantError error_key(:machine_not_found) end class MachineStateInvalid < VagrantError error_key(:machine_state_invalid) end class MultiVMTargetRequired < VagrantError error_key(:multi_vm_target_required) end class NetplanNoAvailableRenderers < VagrantError error_key(:netplan_no_available_renderers) end class NetSSHException < VagrantError error_key(:net_ssh_exception) end class NetworkCollision < VagrantError error_key(:collides, "vagrant.actions.vm.host_only_network") end class NetworkAddressInvalid < VagrantError error_key(:network_address_invalid) end class NetworkDHCPAlreadyAttached < VagrantError error_key(:dhcp_already_attached, "vagrant.actions.vm.network") end class NetworkNotFound < VagrantError error_key(:not_found, "vagrant.actions.vm.host_only_network") end class NetworkTypeNotSupported < VagrantError error_key(:network_type_not_supported) end class NetworkManagerNotInstalled < VagrantError error_key(:network_manager_not_installed) end class NFSBadExports < VagrantError error_key(:nfs_bad_exports) end class NFSDupePerms < VagrantError error_key(:nfs_dupe_permissions) end class NFSExportsFailed < VagrantError error_key(:nfs_exports_failed) end class NFSCantReadExports < VagrantError error_key(:nfs_cant_read_exports) end class NFSMountFailed < VagrantError error_key(:nfs_mount_failed) end class NFSNoGuestIP < VagrantError error_key(:nfs_no_guest_ip) end class NFSNoHostIP < VagrantError error_key(:nfs_no_host_ip) end class NFSNoHostonlyNetwork < VagrantError error_key(:nfs_no_hostonly_network) end class NFSNoValidIds < VagrantError error_key(:nfs_no_valid_ids) end class NFSNotSupported < VagrantError error_key(:nfs_not_supported) end class NFSClientNotInstalledInGuest < VagrantError error_key(:nfs_client_not_installed_in_guest) end class NoDefaultProvider < VagrantError error_key(:no_default_provider) end class NoDefaultSyncedFolderImpl < VagrantError error_key(:no_default_synced_folder_impl) end class NoEnvironmentError < VagrantError error_key(:no_env) end class OscdimgCommandMissingError < VagrantError error_key(:oscdimg_command_missing) end class PackageIncludeMissing < VagrantError error_key(:include_file_missing, "vagrant.actions.general.package") end class PackageIncludeSymlink < VagrantError error_key(:package_include_symlink) end class PackageOutputDirectory < VagrantError error_key(:output_is_directory, "vagrant.actions.general.package") end class PackageOutputExists < VagrantError error_key(:output_exists, "vagrant.actions.general.package") end class PackageRequiresDirectory < VagrantError error_key(:requires_directory, "vagrant.actions.general.package") end class PackageInvalidInfo < VagrantError error_key(:package_invalid_info) end class PowerShellNotFound < VagrantError error_key(:powershell_not_found) end class PowerShellInvalidVersion < VagrantError error_key(:powershell_invalid_version) end class PowerShellError < VagrantError error_key(:powershell_error, "vagrant_ps.errors.powershell_error") end class ProviderCantInstall < VagrantError error_key(:provider_cant_install) end class ProviderChecksumMismatch < VagrantError error_key(:provider_checksum_mismatch) end class ProviderInstallFailed < VagrantError error_key(:provider_install_failed) end class ProviderNotFound < VagrantError error_key(:provider_not_found) end class ProviderNotFoundSuggestion < VagrantError error_key(:provider_not_found_suggestion) end class ProviderNotUsable < VagrantError error_key(:provider_not_usable) end class ProvisionerFlagInvalid < VagrantError error_key(:provisioner_flag_invalid) end class ProvisionerWinRMUnsupported < VagrantError error_key(:provisioner_winrm_unsupported) end class PluginNeedsDeveloperTools < VagrantError error_key(:plugin_needs_developer_tools) end class PluginMissingLibrary < VagrantError error_key(:plugin_missing_library) end class PluginMissingRubyDev < VagrantError error_key(:plugin_missing_ruby_dev) end class PluginGemNotFound < VagrantError error_key(:plugin_gem_not_found) end class PluginInstallLicenseNotFound < VagrantError error_key(:plugin_install_license_not_found) end class PluginInstallFailed < VagrantError error_key(:plugin_install_failed) end class PluginInstallSpace < VagrantError error_key(:plugin_install_space) end class PluginInstallVersionConflict < VagrantError error_key(:plugin_install_version_conflict) end class PluginLoadError < VagrantError error_key(:plugin_load_error) end class PluginNotInstalled < VagrantError error_key(:plugin_not_installed) end class PluginStateFileParseError < VagrantError error_key(:plugin_state_file_not_parsable) end class PluginUninstallSystem < VagrantError error_key(:plugin_uninstall_system) end class PluginInitError < VagrantError error_key(:plugin_init_error) end class PluginSourceError < VagrantError error_key(:plugin_source_error) end class PluginNoLocalError < VagrantError error_key(:plugin_no_local_error) end class PluginMissingLocalError < VagrantError error_key(:plugin_missing_local_error) end class PushesNotDefined < VagrantError error_key(:pushes_not_defined) end class PushStrategyNotDefined < VagrantError error_key(:push_strategy_not_defined) end class PushStrategyNotLoaded < VagrantError error_key(:push_strategy_not_loaded) end class PushStrategyNotProvided < VagrantError error_key(:push_strategy_not_provided) end class RSyncPostCommandError < VagrantError error_key(:rsync_post_command_error) end class RSyncError < VagrantError error_key(:rsync_error) end class RSyncNotFound < VagrantError error_key(:rsync_not_found) end class RSyncNotInstalledInGuest < VagrantError error_key(:rsync_not_installed_in_guest) end class RSyncGuestInstallError < VagrantError error_key(:rsync_guest_install_error) end class SCPPermissionDenied < VagrantError error_key(:scp_permission_denied) end class SCPUnavailable < VagrantError error_key(:scp_unavailable) end class SharedFolderCreateFailed < VagrantError error_key(:shared_folder_create_failed) end class ShellExpandFailed < VagrantError error_key(:shell_expand_failed) end class SnapshotConflictFailed < VagrantError error_key(:snapshot_force) end class SnapshotNotFound < VagrantError error_key(:snapshot_not_found) end class SnapshotNotSupported < VagrantError error_key(:snapshot_not_supported) end class SSHAuthenticationFailed < VagrantError error_key(:ssh_authentication_failed) end class SSHChannelOpenFail < VagrantError error_key(:ssh_channel_open_fail) end class SSHConnectEACCES < VagrantError error_key(:ssh_connect_eacces) end class SSHConnectionRefused < VagrantError error_key(:ssh_connection_refused) end class SSHConnectionAborted < VagrantError error_key(:ssh_connection_aborted) end class SSHConnectionReset < VagrantError error_key(:ssh_connection_reset) end class SSHConnectionTimeout < VagrantError error_key(:ssh_connection_timeout) end class SSHDisconnected < VagrantError error_key(:ssh_disconnected) end class SSHHostDown < VagrantError error_key(:ssh_host_down) end class SSHInvalidShell< VagrantError error_key(:ssh_invalid_shell) end class SSHInsertKeyUnsupported < VagrantError error_key(:ssh_insert_key_unsupported) end class SSHIsPuttyLink < VagrantError error_key(:ssh_is_putty_link) end class SSHKeyBadOwner < VagrantError error_key(:ssh_key_bad_owner) end class SSHKeyBadPermissions < VagrantError error_key(:ssh_key_bad_permissions) end class SSHKeyTypeNotSupported < VagrantError error_key(:ssh_key_type_not_supported) end class SSHKeyTypeNotSupportedByServer < VagrantError error_key(:ssh_key_type_not_supported_by_server) end class SSHNoExitStatus < VagrantError error_key(:ssh_no_exit_status) end class SSHNoRoute < VagrantError error_key(:ssh_no_route) end class SSHNotReady < VagrantError error_key(:ssh_not_ready) end class SSHRunRequiresKeys < VagrantError error_key(:ssh_run_requires_keys) end class SSHUnavailable < VagrantError error_key(:ssh_unavailable) end class SSHUnavailableWindows < VagrantError error_key(:ssh_unavailable_windows) end class SyncedFolderUnusable < VagrantError error_key(:synced_folder_unusable) end class TriggersBadExitCodes < VagrantError error_key(:triggers_bad_exit_codes) end class TriggersGuestNotExist < VagrantError error_key(:triggers_guest_not_exist) end class TriggersGuestNotRunning < VagrantError error_key(:triggers_guest_not_running) end class TriggersNoBlockGiven < VagrantError error_key(:triggers_no_block_given) end class TriggersNoStageGiven < VagrantError error_key(:triggers_no_stage_given) end class UIExpectsTTY < VagrantError error_key(:ui_expects_tty) end class UnimplementedProviderAction < VagrantError error_key(:unimplemented_provider_action) end class UploadInvalidCompressionType < VagrantError error_key(:upload_invalid_compression_type) end class UploadMissingExtractCapability < VagrantError error_key(:upload_missing_extract_capability) end class UploadMissingTempCapability < VagrantError error_key(:upload_missing_temp_capability) end class UploadSourceMissing < VagrantError error_key(:upload_source_missing) end class UploaderError < VagrantError error_key(:uploader_error) end class UploaderInterrupted < UploaderError error_key(:uploader_interrupted) end class VagrantLocked < VagrantError error_key(:vagrant_locked) end class VagrantInterrupt < VagrantError error_key(:interrupted) end class VagrantfileExistsError < VagrantError error_key(:vagrantfile_exists) end class VagrantfileLoadError < VagrantError error_key(:vagrantfile_load_error) end class VagrantfileNameError < VagrantError error_key(:vagrantfile_name_error) end class VagrantfileSyntaxError < VagrantError error_key(:vagrantfile_syntax_error) end class VagrantfileTemplateNotFoundError < VagrantError error_key(:vagrantfile_template_not_found_error) end class VagrantfileWriteError < VagrantError error_key(:vagrantfile_write_error) end class VagrantVersionBad < VagrantError error_key(:vagrant_version_bad) end class VBoxManageError < VagrantError error_key(:vboxmanage_error) end class VBoxManageLaunchError < VagrantError error_key(:vboxmanage_launch_error) end class VBoxManageNotFoundError < VagrantError error_key(:vboxmanage_not_found_error) end class VirtualBoxBrokenVersion040214 < VagrantError error_key(:virtualbox_broken_version_040214) end class VirtualBoxConfigNotFound < VagrantError error_key(:virtualbox_config_not_found) end class VirtualBoxDisksDefinedExceedLimit < VagrantError error_key(:virtualbox_disks_defined_exceed_limit) end class VirtualBoxDisksControllerNotFound < VagrantError error_key(:virtualbox_disks_controller_not_found) end class VirtualBoxDisksNoSupportedControllers < VagrantError error_key(:virtualbox_disks_no_supported_controllers) end class VirtualBoxDisksPrimaryNotFound < VagrantError error_key(:virtualbox_disks_primary_not_found) end class VirtualBoxDisksUnsupportedController < VagrantError error_key(:virtualbox_disks_unsupported_controller) end class VirtualBoxGuestPropertyNotFound < VagrantError error_key(:virtualbox_guest_property_not_found) end class VirtualBoxInvalidVersion < VagrantError error_key(:virtualbox_invalid_version) end class VirtualBoxNoRoomForHighLevelNetwork < VagrantError error_key(:virtualbox_no_room_for_high_level_network) end class VirtualBoxNotDetected < VagrantError error_key(:virtualbox_not_detected) end class VirtualBoxKernelModuleNotLoaded < VagrantError error_key(:virtualbox_kernel_module_not_loaded) end class VirtualBoxInstallIncomplete < VagrantError error_key(:virtualbox_install_incomplete) end class VirtualBoxMachineFolderNotFound < VagrantError error_key(:virtualbox_machine_folder_not_found) end class VirtualBoxNoName < VagrantError error_key(:virtualbox_no_name) end class VirtualBoxMountFailed < VagrantError error_key(:virtualbox_mount_failed) end class VirtualBoxMountNotSupportedBSD < VagrantError error_key(:virtualbox_mount_not_supported_bsd) end class VirtualBoxNameExists < VagrantError error_key(:virtualbox_name_exists) end class VirtualBoxUserMismatch < VagrantError error_key(:virtualbox_user_mismatch) end class VirtualBoxVersionEmpty < VagrantError error_key(:virtualbox_version_empty) end class VirtualBoxInvalidHostSubnet < VagrantError error_key(:virtualbox_invalid_host_subnet) end class VMBaseMacNotSpecified < VagrantError error_key(:no_base_mac, "vagrant.actions.vm.match_mac") end class VMBootBadState < VagrantError error_key(:boot_bad_state) end class VMBootTimeout < VagrantError 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 class VMImportFailure < VagrantError error_key(:failure, "vagrant.actions.vm.import") end class VMInaccessible < VagrantError error_key(:vm_inaccessible) end class VMNameExists < VagrantError error_key(:vm_name_exists) end class VMNoMatchError < VagrantError error_key(:vm_no_match) end class VMNotCreatedError < VagrantError error_key(:vm_creation_required) end class VMNotFoundError < VagrantError error_key(:vm_not_found) end class VMNotRunningError < VagrantError error_key(:vm_not_running) end class VMPowerOffToPackage < VagrantError error_key(:power_off, "vagrant.actions.vm.export") end class WinRMInvalidCommunicator < VagrantError error_key(:winrm_invalid_communicator) end class WSLVagrantVersionMismatch < VagrantError error_key(:wsl_vagrant_version_mismatch) end class WSLVagrantAccessError < VagrantError error_key(:wsl_vagrant_access_error) end class WSLVirtualBoxWindowsAccessError < VagrantError error_key(:wsl_virtualbox_windows_access) end class WSLRootFsNotFoundError < VagrantError error_key(:wsl_rootfs_not_found_error) end end end ================================================ FILE: lib/vagrant/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/capability_host" module Vagrant # This class handles guest-OS specific interactions with a machine. # It is primarily responsible for detecting the proper guest OS # implementation and then delegating capabilities. # # Vagrant has many tasks which require specific guest OS knowledge. # These are implemented using a guest/capability system. Various plugins # register as "guests" which determine the underlying OS of the system. # Then, "guest capabilities" register themselves for a specific OS (one # or more), and these capabilities are called. # # Example capabilities might be "mount_virtualbox_shared_folder" or # "configure_networks". # # This system allows for maximum flexibility and pluginability for doing # guest OS specific operations. class Guest include CapabilityHost def initialize(machine, guests, capabilities) @capabilities = capabilities @guests = guests @machine = machine end # This will detect the proper guest OS for the machine and set up # the class to actually execute capabilities. def detect! guest_name = @machine.config.vm.guest initialize_capabilities!(guest_name, @guests, @capabilities, @machine) rescue Errors::CapabilityHostExplicitNotDetected => e raise Errors::GuestExplicitNotDetected, value: e.extra_data[:value] rescue Errors::CapabilityHostNotDetected raise Errors::GuestNotDetected end # See {CapabilityHost#capability} def capability(*args) super rescue Errors::CapabilityNotFound => e raise Errors::GuestCapabilityNotFound, cap: e.extra_data[:cap], guest: name rescue Errors::CapabilityInvalid => e raise Errors::GuestCapabilityInvalid, cap: e.extra_data[:cap], guest: name end # Returns the specified or detected guest type name. # # @return [Symbol] def name capability_host_chain[0][0] end # This returns whether the guest is ready to work. If this returns # `false`, then {#detect!} should be called in order to detect the # guest OS. # # @return [Boolean] def ready? !!capability_host_chain end end end ================================================ FILE: lib/vagrant/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/capability_host" module Vagrant # This class handles host-OS specific interactions. It is responsible for # detecting the proper host OS implementation and delegating capabilities # to plugins. # # See {Guest} for more information on capabilities. class Host include CapabilityHost def initialize(host, hosts, capabilities, env) initialize_capabilities!(host, hosts, capabilities, env) end end end ================================================ FILE: lib/vagrant/machine.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "./util/ssh" require_relative "./action/builtin/mixin_synced_folders" require "digest/md5" require "thread" require "log4r" module Vagrant # This represents a machine that Vagrant manages. This provides a singular # API for querying the state and making state changes to the machine, which # is backed by any sort of provider (VirtualBox, VMware, etc.). class Machine extend Vagrant::Action::Builtin::MixinSyncedFolders # The box that is backing this machine. # # @return [Box] attr_accessor :box # Configuration for the machine. # # @return [Object] attr_accessor :config # Directory where machine-specific data can be stored. # # @return [Pathname] attr_reader :data_dir # The environment that this machine is a part of. # # @return [Environment] attr_reader :env # ID of the machine. This ID comes from the provider and is not # guaranteed to be of any particular format except that it is # a string. # # @return [String] attr_reader :id # Name of the machine. This is assigned by the Vagrantfile. # # @return [Symbol] attr_reader :name # The provider backing this machine. # # @return [Object] attr_reader :provider # The provider-specific configuration for this machine. # # @return [Object] attr_accessor :provider_config # The name of the provider. # # @return [Symbol] attr_reader :provider_name # The options given to the provider when registering the plugin. # # @return [Hash] attr_reader :provider_options # The triggers with machine specific configuration applied # # @return [Vagrant::Plugin::V2::Trigger] attr_reader :triggers # The UI for outputting in the scope of this machine. # # @return [UI] attr_reader :ui # The Vagrantfile that this machine is attached to. # # @return [Vagrantfile] attr_reader :vagrantfile # Initialize a new machine. # # @param [String] name Name of the virtual machine. # @param [Class] provider The provider backing this machine. This is # currently expected to be a V1 `provider` plugin. # @param [Object] provider_config The provider-specific configuration for # this machine. # @param [Hash] provider_options The provider-specific options from the # plugin definition. # @param [Object] config The configuration for this machine. # @param [Pathname] data_dir The directory where machine-specific data # can be stored. This directory is ensured to exist. # @param [Box] box The box that is backing this virtual machine. # @param [Environment] env The environment that this machine is a # part of. def initialize(name, provider_name, provider_cls, provider_config, provider_options, config, data_dir, box, env, vagrantfile, base=false) @logger = Log4r::Logger.new("vagrant::machine") @logger.info("Initializing machine: #{name}") @logger.info(" - Provider: #{provider_cls}") @logger.info(" - Box: #{box}") @logger.info(" - Data dir: #{data_dir}") @box = box @config = config @data_dir = data_dir @env = env @vagrantfile = vagrantfile @guest = Guest.new( self, Vagrant.plugin("2").manager.guests, Vagrant.plugin("2").manager.guest_capabilities) @name = name @provider_config = provider_config @provider_name = provider_name @provider_options = provider_options @ui = Vagrant::UI::Prefixed.new(@env.ui, @name) @ui_mutex = Mutex.new @state_mutex = Mutex.new @triggers = Vagrant::Plugin::V2::Trigger.new(@env, @config.trigger, self, @ui) # Read the ID, which is usually in local storage @id = nil # XXX: This is temporary. This will be removed very soon. if base @id = name # For base setups, we don't want to insert the key @config.ssh.insert_key = false else reload end # Keep track of where our UUID should be placed @index_uuid_file = nil @index_uuid_file = @data_dir.join("index_uuid") if @data_dir # Initializes the provider last so that it has access to all the # state we setup on this machine. @provider = provider_cls.new(self) @provider._initialize(@provider_name, self) # If we're using WinRM, we eager load the plugin because of # GH-3390 if @config.vm.communicator == :winrm @logger.debug("Eager loading WinRM communicator to avoid GH-3390") communicate end # If the ID is the special not created ID, then set our ID to # nil so that we destroy all our data. if state.id == MachineState::NOT_CREATED_ID self.id = nil end # Output a bunch of information about this machine in # machine-readable format in case someone is listening. @ui.machine("metadata", "provider", provider_name) end # This calls an action on the provider. The provider may or may not # actually implement the action. # # @param [Symbol] name Name of the action to run. # @param [Hash] extra_env This data will be passed into the action runner # as extra data set on the environment hash for the middleware # runner. def action(name, opts=nil) @logger.info("Calling action: #{name} on provider #{@provider}") opts ||= {} # Determine whether we lock or not lock = true lock = opts.delete(:lock) if opts.key?(:lock) # Extra env keys are the remaining opts extra_env = opts.dup check_cwd # Warns the UI if the machine was last used on a different dir # Create a deterministic ID for this machine vf = nil vf = @env.vagrantfile_name[0] if @env.vagrantfile_name id = Digest::MD5.hexdigest( "#{@env.root_path}#{vf}#{@env.local_data_path}#{@name}") # We only lock if we're not executing an SSH action. In the future # we will want to do more fine-grained unlocking in actions themselves # but for a 1.6.2 release this will work. locker = Proc.new { |*args, &block| block.call } locker = @env.method(:lock) if lock && !name.to_s.start_with?("ssh") # Lock this machine for the duration of this action return_env = locker.call("machine-action-#{id}") do # Get the callable from the provider. callable = @provider.action(name) # If this action doesn't exist on the provider, then an exception # must be raised. if callable.nil? raise Errors::UnimplementedProviderAction, action: name, provider: @provider.to_s end # Call the action ui.machine("action", name.to_s, "start") action_result = action_raw(name, callable, extra_env) ui.machine("action", name.to_s, "end") action_result end # preserve returning environment after machine action runs return return_env rescue Errors::EnvironmentLockedError raise Errors::MachineActionLockedError, action: name, name: @name end # This calls a raw callable in the proper context of the machine using # the middleware stack. # # @param [Symbol] name Name of the action # @param [Proc] callable # @param [Hash] extra_env Extra env for the action env. # @return [Hash] The resulting env def action_raw(name, callable, extra_env={}) if !extra_env.is_a?(Hash) extra_env = {} end # Run the action with the action runner on the environment env = {ui: @ui}.merge(extra_env).merge( raw_action_name: name, action_name: "machine_action_#{name}".to_sym, machine: self, machine_action: name ) @env.action_runner.run(callable, env) end # Returns a communication object for executing commands on the remote # machine. Note that the _exact_ semantics of this are up to the # communication provider itself. Despite this, the semantics are expected # to be consistent across operating systems. For example, all linux-based # systems should have similar communication (usually a shell). All # Windows systems should have similar communication as well. Therefore, # prior to communicating with the machine, users of this method are # expected to check the guest OS to determine their behavior. # # This method will _always_ return some valid communication object. # The `ready?` API can be used on the object to check if communication # is actually ready. # # @return [Object] def communicate if !@communicator requested = @config.vm.communicator requested ||= :ssh klass = Vagrant.plugin("2").manager.communicators[requested] raise Errors::CommunicatorNotFound, comm: requested.to_s if !klass @communicator = klass.new(self) end @communicator end # Returns a guest implementation for this machine. The guest implementation # knows how to do guest-OS specific tasks, such as configuring networks, # mounting folders, etc. # # @return [Guest] def guest raise Errors::MachineGuestNotReady if !communicate.ready? @guest.detect! if !@guest.ready? @guest end # This sets the unique ID associated with this machine. This will # persist this ID so that in the future Vagrant will be able to find # this machine again. The unique ID must be absolutely unique to the # virtual machine, and can be used by providers for finding the # actual machine associated with this instance. # # **WARNING:** Only providers should ever use this method. # # @param [String] value The ID. def id=(value) @logger.info("New machine ID: #{value.inspect}") id_file = nil if @data_dir # The file that will store the id if we have one. This allows the # ID to persist across Vagrant runs. Also, store the UUID for the # machine index. id_file = @data_dir.join("id") end if value if id_file # Write the "id" file with the id given. id_file.open("w+") do |f| f.write(value) end end if uid_file # Write the user id that created this machine uid_file.open("w+") do |f| f.write(Process.uid.to_s) end end # If we don't have a UUID, then create one if index_uuid.nil? # Create the index entry and save it entry = MachineIndex::Entry.new entry.local_data_path = @env.local_data_path entry.name = @name.to_s entry.provider = @provider_name.to_s entry.architecture = @architecture entry.state = "preparing" entry.vagrantfile_path = @env.root_path entry.vagrantfile_name = @env.vagrantfile_name if @box entry.extra_data["box"] = { "name" => @box.name, "provider" => @box.provider.to_s, "architecture" => @box.architecture, "version" => @box.version.to_s, } end entry = @env.machine_index.set(entry) @env.machine_index.release(entry) # Store our UUID so we can access it later if @index_uuid_file @index_uuid_file.open("w+") do |f| f.write(entry.id) end end end else @logger.debug("machine ID has been unset, deregistering machine and removing data directory") # Delete the file, since the machine is now destroyed id_file.delete if id_file && id_file.file? uid_file.delete if uid_file && uid_file.file? # If we have a UUID associated with the index, remove it uuid = index_uuid if uuid entry = @env.machine_index.get(uuid) @env.machine_index.delete(entry) if entry end if @data_dir # Delete the entire data directory contents since all state # associated with the VM is now gone. @data_dir.children.each do |child| begin child.rmtree rescue Errno::EACCES @logger.info("EACCESS deleting file: #{child}") end end end end # Store the ID locally @id = value.nil? ? nil : value.to_s # Notify the provider that the ID changed in case it needs to do # any accounting from it. @provider.machine_id_changed end # Returns the UUID associated with this machine in the machine # index. We only have a UUID if an ID has been set. # # @return [String] UUID or nil if we don't have one yet. def index_uuid return nil if !@index_uuid_file return @index_uuid_file.read.chomp if @index_uuid_file.file? return nil end # This returns a clean inspect value so that printing the value via # a pretty print (`p`) results in a readable value. # # @return [String] def inspect "#<#{self.class}: #{@name} (#{@provider.class})>" end # This reloads the ID of the underlying machine. def reload old_id = @id @id = nil if @data_dir # Read the id file from the data directory if it exists as the # ID for the pre-existing physical representation of this machine. id_file = @data_dir.join("id") id_content = id_file.read.strip if id_file.file? if !id_content.to_s.empty? @id = id_content end end if @id != old_id && @provider # It changed, notify the provider @provider.machine_id_changed end @id end # This returns the SSH info for accessing this machine. This SSH info # is queried from the underlying provider. This method returns `nil` if # the machine is not ready for SSH communication. # # The structure of the resulting hash is guaranteed to contain the # following structure, although it may return other keys as well # not documented here: # # { # host: "1.2.3.4", # port: "22", # username: "mitchellh", # private_key_path: "/path/to/my/key" # } # # Note that Vagrant makes no guarantee that this info works or is # correct. This is simply the data that the provider gives us or that # is configured via a Vagrantfile. It is still possible after this # point when attempting to connect via SSH to get authentication # errors. # # @return [Hash] SSH information. def ssh_info # First, ask the provider for their information. If the provider # returns nil, then the machine is simply not ready for SSH, and # we return nil as well. info = @provider.ssh_info return nil if info.nil? # Delete out the nil entries. info.dup.each do |key, value| info.delete(key) if value.nil? end # We set the defaults info[:host] ||= @config.ssh.default.host info[:port] ||= @config.ssh.default.port info[:private_key_path] ||= @config.ssh.default.private_key_path info[:keys_only] ||= @config.ssh.default.keys_only info[:verify_host_key] ||= @config.ssh.default.verify_host_key info[:username] ||= @config.ssh.default.username info[:remote_user] ||= @config.ssh.default.remote_user info[:compression] ||= @config.ssh.default.compression info[:dsa_authentication] ||= @config.ssh.default.dsa_authentication info[:extra_args] ||= @config.ssh.default.extra_args info[:config] ||= @config.ssh.default.config # We set overrides if they are set. These take precedence over # provider-returned data. info[:host] = @config.ssh.host if @config.ssh.host info[:port] = @config.ssh.port if @config.ssh.port info[:keys_only] = @config.ssh.keys_only info[:verify_host_key] = @config.ssh.verify_host_key info[:compression] = @config.ssh.compression info[:dsa_authentication] = @config.ssh.dsa_authentication info[:username] = @config.ssh.username if @config.ssh.username info[:password] = @config.ssh.password if @config.ssh.password info[:remote_user] = @config.ssh.remote_user if @config.ssh.remote_user info[:extra_args] = @config.ssh.extra_args if @config.ssh.extra_args info[:config] = @config.ssh.config if @config.ssh.config # We also set some fields that are purely controlled by Vagrant info[:forward_agent] = @config.ssh.forward_agent info[:forward_x11] = @config.ssh.forward_x11 info[:forward_env] = @config.ssh.forward_env info[:connect_timeout] = @config.ssh.connect_timeout info[:connect_retries] = @config.ssh.connect_retries info[:connect_retry_delay] = @config.ssh.connect_retry_delay info[:ssh_command] = @config.ssh.ssh_command if @config.ssh.ssh_command # Add in provided proxy command config info[:proxy_command] = @config.ssh.proxy_command if @config.ssh.proxy_command # Set the private key path. If a specific private key is given in # the Vagrantfile we set that. Otherwise, we use the default (insecure) # private key, but only if the provider didn't give us one. if !info[:private_key_path] && !info[:password] if @config.ssh.private_key_path info[:private_key_path] = @config.ssh.private_key_path else info[:private_key_path] = @env.default_private_key_paths end end # If we have a private key in our data dir, then use that if @data_dir && !@config.ssh.private_key_path data_private_key = @data_dir.join("private_key") if data_private_key.file? info[:private_key_path] = [data_private_key.to_s] end end # Setup the keys info[:private_key_path] ||= [] info[:private_key_path] = Array(info[:private_key_path]) # Expand the private key path relative to the root path info[:private_key_path].map! do |path| File.expand_path(path, @env.root_path) end # Check that the private key permissions are valid info[:private_key_path].each do |path| key_path = Pathname.new(path) if key_path.exist? Vagrant::Util::SSH.check_key_permissions(key_path) end end # Return the final compiled SSH info data info end # Returns the state of this machine. The state is queried from the # backing provider, so it can be any arbitrary symbol. # # @return [MachineState] def state result = @provider.state raise Errors::MachineStateInvalid if !result.is_a?(MachineState) # Update our state cache if we have a UUID and an entry in the # master index. uuid = index_uuid if uuid # active_machines provides access to query this info on each machine # from a different thread, ensure multiple machines do not access # the locked entry simultaneously as this triggers a locked machine # exception. @state_mutex.synchronize do entry = @env.machine_index.get(uuid) if entry entry.state = result.short_description @env.machine_index.set(entry) @env.machine_index.release(entry) end end end result end # Returns the state of this machine. The state is queried from the # backing provider, so it can be any arbitrary symbol. # # @param [Symbol] state of machine # @return [Entry] entry of recovered machine def recover_machine(state) entry = @env.machine_index.get(index_uuid) if entry @env.machine_index.release(entry) return entry end entry = MachineIndex::Entry.new(id=index_uuid, {}) entry.local_data_path = @env.local_data_path entry.name = @name.to_s entry.provider = @provider_name.to_s entry.state = state entry.vagrantfile_path = @env.root_path entry.vagrantfile_name = @env.vagrantfile_name if @box entry.extra_data["box"] = { "name" => @box.name, "provider" => @box.provider.to_s, "architecture" => @box.architecture, "version" => @box.version.to_s, } end @state_mutex.synchronize do entry = @env.machine_index.recover(entry) @env.machine_index.release(entry) end return entry end # Returns the user ID that created this machine. This is specific to # the host machine that this was created on. # # @return [String] def uid path = uid_file return nil if !path return nil if !path.file? return uid_file.read.chomp end # Temporarily changes the machine UI. This is useful if you want # to execute an {#action} with a different UI. def with_ui(ui) @ui_mutex.synchronize do begin old_ui = @ui @ui = ui yield ensure @ui = old_ui end end end # This returns the set of shared folders that should be done for # this machine. It returns the folders in a hash keyed by the # implementation class for the synced folders. # # @return [Hash>] def synced_folders self.class.synced_folders(self) end protected # Returns the path to the file that stores the UID. def uid_file return nil if !@data_dir @data_dir.join("creator_uid") end # Checks the current directory for a given machine # and displays a warning if that machine has moved # from its previous location on disk. If the machine # has moved, it prints a warning to the user. def check_cwd desired_encoding = @env.root_path.to_s.encoding vagrant_cwd_filepath = @data_dir.join('vagrant_cwd') vagrant_cwd = if File.exist?(vagrant_cwd_filepath) File.read(vagrant_cwd_filepath, external_encoding: desired_encoding ).chomp end if !File.identical?(vagrant_cwd.to_s, @env.root_path.to_s) if vagrant_cwd ui.warn(I18n.t( 'vagrant.moved_cwd', old_wd: "#{vagrant_cwd}", current_wd: "#{@env.root_path.to_s}")) end File.write(vagrant_cwd_filepath, @env.root_path.to_s, external_encoding: desired_encoding ) end end end end ================================================ FILE: lib/vagrant/machine_index.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" require "pathname" require "securerandom" require "thread" require "vagrant/util/silence_warnings" module Vagrant # MachineIndex is able to manage the index of created Vagrant environments # in a central location. # # The MachineIndex stores a mapping of UUIDs to basic information about # a machine. The UUIDs are stored with the Vagrant environment and are # looked up in the machine index. # # The MachineIndex stores information such as the name of a machine, # the directory it was last seen at, its last known state, etc. Using # this information, we can load the entire {Machine} object for a machine, # or we can just display metadata if needed. # # The internal format of the data file is currently JSON in the following # structure: # # { # "version": 1, # "machines": { # "uuid": { # "name": "foo", # "provider": "vmware_fusion", # "architecture": "amd64", # "data_path": "/path/to/data/dir", # "vagrantfile_path": "/path/to/Vagrantfile", # "state": "running", # "updated_at": "2014-03-02 11:11:44 +0100" # } # } # } # class MachineIndex include Enumerable # Initializes a MachineIndex at the given file location. # # @param [Pathname] data_dir Path to the directory where data for the # index can be stored. This folder should exist and must be writable. def initialize(data_dir) @data_dir = data_dir @index_file = data_dir.join("index") @lock = Monitor.new @machines = {} @machine_locks = {} with_index_lock do unlocked_reload end end # Deletes a machine by UUID. # # The machine being deleted with this UUID must either be locked # by this index or must be unlocked. # # @param [Entry] entry The entry to delete. # @return [Boolean] true if delete is successful def delete(entry) return true if !entry.id @lock.synchronize do with_index_lock do return true if !@machines[entry.id] # If we don't have the lock, then we need to acquire it. if !@machine_locks[entry.id] raise "Unlocked delete on machine: #{entry.id}" end # Reload so we have the latest data, then delete and save unlocked_reload @machines.delete(entry.id) unlocked_save # Release access on this machine unlocked_release(entry.id) end end true end # Iterate over every machine in the index. The yielded {Entry} objects # will NOT be locked, so you'll have to call {#get} manually to acquire # the lock on them. def each(reload=false) if reload @lock.synchronize do with_index_lock do unlocked_reload end end end @machines.each do |uuid, data| yield Entry.new(uuid, data.merge("id" => uuid)) end end # Accesses a machine by UUID and returns a {MachineIndex::Entry} # # The entry returned is locked and can't be read again or updated by # this process or any other. To unlock the machine, call {#release} # with the entry. # # You can only {#set} an entry (update) when the lock is held. # # @param [String] uuid UUID for the machine to access. # @return [MachineIndex::Entry] def get(uuid) entry = nil @lock.synchronize do with_index_lock do # Reload the data unlocked_reload data = find_by_prefix(uuid) return nil if !data uuid = data["id"] entry = Entry.new(uuid, data) # Lock this machine lock_file = lock_machine(uuid) if !lock_file raise Errors::MachineLocked, name: entry.name, provider: entry.provider end @machine_locks[uuid] = lock_file end end entry end # Tests if the index has the given UUID. # # @param [String] uuid # @return [Boolean] def include?(uuid) @lock.synchronize do with_index_lock do unlocked_reload return !!find_by_prefix(uuid) end end end # Releases an entry, unlocking it. # # This is an idempotent operation. It is safe to call this even if you're # unsure if an entry is locked or not. # # After calling this, the previous entry should no longer be used. # # @param [Entry] entry def release(entry) @lock.synchronize do unlocked_release(entry.id) end end # Creates/updates an entry object and returns the resulting entry. # # If the entry was new (no UUID), then the UUID will be set on the # resulting entry and can be used. Additionally, the a lock will # be created for the resulting entry, so you must {#release} it # if you want others to be able to access it. # # If the entry isn't new (has a UUID). then this process must hold # that entry's lock or else this set will fail. # # @param [Entry] entry # @return [Entry] def set(entry) # Get the struct and update the updated_at attribute struct = entry.to_json_struct # Set an ID if there isn't one already set id = entry.id @lock.synchronize do with_index_lock do # Reload so we have the latest machine data. This allows other # processes to update their own machines without conflicting # with our own. unlocked_reload # If we don't have a machine ID, try to look one up if !id self.each do |other| if entry.name == other.name && entry.provider == other.provider && entry.vagrantfile_path.to_s == other.vagrantfile_path.to_s id = other.id break end end # If we still don't have an ID, generate a random one id = SecureRandom.uuid.gsub("-", "") if !id # Get a lock on this machine lock_file = lock_machine(id) if !lock_file raise "Failed to lock new machine: #{entry.name}" end @machine_locks[id] = lock_file end if !@machine_locks[id] raise "Unlocked write on machine: #{id}" end # Set our machine and save @machines[id] = struct unlocked_save end end Entry.new(id, struct) end # Reinsert a machine into the global index if it has # a valid existing uuid but does not currently exist # in the index. # # @param [Entry] entry # @return [Entry] def recover(entry) @lock.synchronize do with_index_lock do # Reload the data unlocked_reload # Don't recover if entry already exists in the global return entry if find_by_prefix(entry.id) lock_file = lock_machine(entry.id) if !lock_file raise Errors::MachineLocked, name: entry.name, provider: entry.provider end @machine_locks[entry.id] = lock_file end end return set(entry) end protected # Finds a machine where the UUID is prefixed by the given string. # # @return [Hash] def find_by_prefix(prefix) return if !prefix || prefix == "" @machines.each do |uuid, data| return data.merge("id" => uuid) if uuid.start_with?(prefix) end nil end # Locks a machine exclusively to us, returning the file handle # that holds the lock. # # If the lock cannot be acquired, then nil is returned. # # This should be called within an index lock. # # @return [File] def lock_machine(uuid) lock_path = @data_dir.join("#{uuid}.lock") lock_file = lock_path.open("w+") if lock_file.flock(File::LOCK_EX | File::LOCK_NB) === false lock_file.close lock_file = nil end lock_file end # Releases a local lock on a machine. This does not acquire any locks # so make sure to lock around it. # # @param [String] id def unlocked_release(id) lock_file = @machine_locks[id] if lock_file lock_file.close begin File.delete(lock_file.path) rescue Errno::EACCES # Another process is probably opened it, no problem. end @machine_locks.delete(id) end end # This will reload the data without locking the index. It is assumed # the caller with lock the index outside of this call. # # @param [File] f def unlocked_reload return if !@index_file.file? data = nil begin data = JSON.load(@index_file.read) rescue JSON::ParserError raise Errors::CorruptMachineIndex, path: @index_file.to_s end if data if !data["version"] || data["version"].to_i != 1 raise Errors::CorruptMachineIndex, path: @index_file.to_s end @machines = data["machines"] || {} end end # Saves the index. def unlocked_save @index_file.open("w") do |f| f.write(JSON.dump({ "version" => 1, "machines" => @machines, })) end end # This will hold a lock to the index so it can be read or updated. def with_index_lock lock_path = "#{@index_file}.lock" File.open(lock_path, "w+") do |f| f.flock(File::LOCK_EX) yield end end # An entry in the MachineIndex. class Entry # The unique ID for this entry. This is _not_ the ID for the # machine itself (which is provider-specific and in the data directory). # # @return [String] attr_reader :id # The path for the "local data" directory for the environment. # # @return [Pathname] attr_accessor :local_data_path # The name of the machine. # # @return [String] attr_accessor :name # The name of the provider. # # @return [String] attr_accessor :provider # The name of the architecture. # # @return [String] attr_accessor :architecture # The last known state of this machine. # # @return [String] attr_accessor :state # The last known state of this machine. # # @return [MachineState] attr_accessor :full_state # The valid Vagrantfile filenames for this environment. # # @return [Array] attr_accessor :vagrantfile_name # The path to the Vagrantfile that manages this machine. # # @return [Pathname] attr_accessor :vagrantfile_path # The last time this entry was updated. # # @return [DateTime] attr_reader :updated_at # Extra data to store with the index entry. This can be anything # and is treated like a general global state bag. # # @return [Hash] attr_accessor :extra_data # Initializes an entry. # # The parameter given should be nil if this is being created # publicly. def initialize(id=nil, raw=nil) @logger = Log4r::Logger.new("vagrant::machine_index::entry") @extra_data = {} @id = id # Do nothing if we aren't given a raw value. Otherwise, parse it. return if !raw @local_data_path = raw["local_data_path"] @name = raw["name"] @provider = raw["provider"] @architecture = raw["architecture"] @state = raw["state"] @full_state = raw["full_state"] @vagrantfile_name = raw["vagrantfile_name"] @vagrantfile_path = raw["vagrantfile_path"] # TODO(mitchellh): parse into a proper datetime @updated_at = raw["updated_at"] @extra_data = raw["extra_data"] || {} # Be careful with the paths @local_data_path = nil if @local_data_path == "" @vagrantfile_path = nil if @vagrantfile_path == "" # Convert to proper types @local_data_path = Pathname.new(@local_data_path) if @local_data_path @vagrantfile_path = Pathname.new(@vagrantfile_path) if @vagrantfile_path end # Returns boolean true if this entry appears to be valid. # The criteria for being valid: # # * Vagrantfile directory exists # * Vagrant environment contains a machine with this # name and provider. # # This method is _slow_. It should be used with care. # # @param [Pathname] home_path The home path for the Vagrant # environment. # @return [Boolean] def valid?(home_path) return false if !vagrantfile_path return false if !vagrantfile_path.directory? # Create an environment so we can determine the active # machines... found = false env = vagrant_env(home_path) env.active_machines.each do |name, provider| if name.to_s == self.name.to_s && provider.to_s == self.provider.to_s found = true break end end # If an active machine of the same name/provider was not # found, it is already false. return false if !found # Get the machine machine = nil begin machine = env.machine(self.name.to_sym, self.provider.to_sym) rescue Errors::MachineNotFound return false end # Refresh the machine state return false if machine.state.id == MachineState::NOT_CREATED_ID true end # Creates a {Vagrant::Environment} for this entry. # # @return [Vagrant::Environment] def vagrant_env(home_path, opts={}) Vagrant::Util::SilenceWarnings.silence! do Environment.new({ cwd: @vagrantfile_path, home_path: home_path, local_data_path: @local_data_path, vagrantfile_name: @vagrantfile_name, }.merge(opts)) end end # Converts to the structure used by the JSON def to_json_struct { "local_data_path" => @local_data_path.to_s, "name" => @name, "provider" => @provider, "architecture" => @architecture, "state" => @state, "vagrantfile_name" => @vagrantfile_name, "vagrantfile_path" => @vagrantfile_path.to_s, "updated_at" => @updated_at, "extra_data" => @extra_data, } end end end end ================================================ FILE: lib/vagrant/machine_state.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant # This represents the state of a given machine. This is a very basic # class that simply stores a short and long description of the state # of a machine. # # The state also stores a state "id" which can be used as a unique # identifier for a state. This should be a symbol. This allows internal # code to compare state such as ":not_created" instead of using # string comparison. # # The short description should be a single word description of the # state of the machine such as "running" or "not created". # # The long description can span multiple lines describing what the # state actually means. class MachineState # This is a special ID that can be set for the state ID that # tells Vagrant that the machine is not created. If this is the # case, then Vagrant will set the ID to nil which will automatically # clean out the machine data directory. NOT_CREATED_ID = :not_created # Unique ID for this state. # # @return [Symbol] attr_reader :id # Short description for this state. # # @return [String] attr_reader :short_description # Long description for this state. # # @return [String] attr_reader :long_description # Creates a new instance to represent the state of a machine. # # @param [Symbol] id Unique identifier for this state. # @param [String] short Short (preferably one-word) description of # the state. # @param [String] long Long description (can span multiple lines) # of the state. def initialize(id, short, long) @id = id @short_description = short @long_description = long end end end ================================================ FILE: lib/vagrant/patches/builder/mkmf.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This custom mkmf.rb file is used on Windows platforms # to handle common path related build failures where # a space is included in the path. The default installation # location being in Program Files results in most many # extensions failing to build. These patches will attempt # to find unquoted paths in flags and quote them prior to # usage. # Start with locating the real mkmf.rb file and # loading it mkmf_paths = $LOAD_PATH.find_all { |x| !x.start_with?(__dir__) && File.exist?(File.join(x, "mkmf.rb")) }.uniq # At this point the path collection should only consist # of a single entry. If there's more than one, load all # of them but include a warning message that more than # one was encountered. If none are found, then something # bad is going on so just bail. if mkmf_paths.size > 1 $stderr.puts "WARNING: Multiple mkmf.rb files located: #{mkmf_paths.inspect}" elsif mkmf_paths.empty? raise "Failed to locate mkmf.rb file" end mkmf_paths.each do |mpath| require File.join(mpath, "mkmf.rb") end # Attempt to detect and quote Windos paths found within # the given string of flags # # @param [String] flags Compiler/linker flags # @return [String] flags with paths quoted def flag_cleaner(flags) parts = flags.split(" -") parts.map! do |p| if p !~ %r{[A-Za-z]:(/|\\)} next p elsif p =~ %r{"[A-Za-z]:(/|\\).+"$} next p end p.gsub(%r{([A-Za-z]:(/|\\).+)$}, '"\1"') end parts.join(" -") end # Check values defined for CFLAGS, CPPFLAGS, LDFLAGS, # and INCFLAGS for unquoted Windows paths and quote # them. def clean_flags! $CFLAGS = flag_cleaner($CFLAGS) $CPPFLAGS = flag_cleaner($CPPFLAGS) $LDFLAGS = flag_cleaner($LDFLAGS) $INCFLAGS = flag_cleaner($INCFLAGS) end # Since mkmf loads the MakeMakefile module directly into the # current scope, apply patches directly in the scope def vagrant_create_makefile(*args) clean_flags! ruby_create_makefile(*args) end alias :ruby_create_makefile :create_makefile alias :create_makefile :vagrant_create_makefile def vagrant_append_cflags(*args) result = ruby_append_cflags(*args) clean_flags! result end alias :ruby_append_cflags :append_cflags alias :append_cflags :vagrant_append_cflags def vagrant_append_cppflags(*args) result = ruby_append_cppflags(*args) clean_flags! result end alias :ruby_append_cppflags :append_cppflags alias :append_cppflags :vagrant_append_cppflags def vagrant_append_ldflags(*args) result = ruby_append_ldflags(*args) clean_flags! result end alias :ruby_append_ldflags :append_ldflags alias :append_ldflags :vagrant_append_ldflags def vagrant_cc_config(*args) clean_flags! ruby_cc_config(*args) end alias :ruby_cc_config :cc_config alias :cc_config :vagrant_cc_config def vagrant_link_config(*args) clean_flags! ruby_link_config(*args) end alias :ruby_link_config :link_config alias :link_config :vagrant_link_config # Finally, always append the flags that Vagrant has # defined via the environment append_cflags(ENV["CFLAGS"]) if ENV["CFLAGS"] append_cppflags(ENV["CPPFLAGS"]) if ENV["CPPFLAGS"] append_ldflags(ENV["LDFLAGS"]) if ENV["LDFLAGS"] ================================================ FILE: lib/vagrant/patches/fake_ftp.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fake_ftp" module FakeFtp class File def initialize(name = nil, data = nil, type = nil, last_modified_time = Time.now) @created = Time.now @name = name @data = data @bytes = data_is_bytes(data) ? data : data.bytes.length @data = data_is_bytes(data) ? nil : data @type = type @last_modified_time = last_modified_time.utc end def data_is_bytes(d) d.nil? || d.is_a?(Integer) end def data=(data) @bytes = data_is_bytes(data) ? data : data.bytes.length @data = data_is_bytes(data) ? nil : data end end end ================================================ FILE: lib/vagrant/patches/log4r.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This adds trace level support to log4r. Since log4r # loggers use the trace method for checking if trace # information should be included in the output, we # make some modifications to allow the trace check to # still work while also supporting trace as a valid level require "log4r/loggerfactory" if !Log4r::Logger::LoggerFactory.respond_to?(:fake_define_methods) class Log4r::Logger::LoggerFactory class << self def fake_set_log(logger, lname) real_set_log(logger, lname) if lname == "TRACE" logger.instance_eval do alias :trace_as_level :trace def trace(*args) return @trace if args.empty? trace_as_level(*args) end end end end def fake_undefine_methods(logger) real_undefine_methods(logger) logger.instance_eval do def trace(*_) @trace end end end alias_method :real_undefine_methods, :undefine_methods alias_method :undefine_methods, :fake_undefine_methods alias_method :real_set_log, :set_log alias_method :set_log, :fake_set_log end end class Log4r::Logger # The factory allows using a previously created logger # instance if it exists. Doing this prevents knocking # out configuration that may have already been applied # to the logger instance (like log level) def self.factory(name, *args) l = Log4r::Logger::Repository[name] return l unless l.nil? Log4r::Logger.new(name, *args) end end end ================================================ FILE: lib/vagrant/patches/net-ssh.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "net/ssh" require "net/ssh/buffer" # Set the version requirement for when net-ssh should be patched NET_SSH_PATCH_REQUIREMENT = Gem::Requirement.new(">= 7.0.0", "<= 7.3") # This patch provides support for properly loading ECDSA private keys if NET_SSH_PATCH_REQUIREMENT.satisfied_by?(Gem::Version.new(Net::SSH::Version::STRING)) Net::SSH::Buffer.class_eval do def vagrant_read_private_keyblob(type) case type when /^ecdsa\-sha2\-(\w*)$/ curve_name_in_type = $1 curve_name_in_key = read_string unless curve_name_in_type == curve_name_in_key raise Net::SSH::Exception, "curve name mismatched (`#{curve_name_in_key}' with `#{curve_name_in_type}')" end public_key_oct = read_string priv_key_bignum = read_bignum begin curvename = OpenSSL::PKey::EC::CurveNameAlias[curve_name_in_key] group = OpenSSL::PKey::EC::Group.new(curvename) point = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(public_key_oct, 2)) priv_bn = OpenSSL::BN.new(priv_key_bignum, 2) asn1 = OpenSSL::ASN1::Sequence( [ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(0)), OpenSSL::ASN1::Sequence.new( [ OpenSSL::ASN1::ObjectId("id-ecPublicKey"), OpenSSL::ASN1::ObjectId(curvename) ] ), OpenSSL::ASN1::OctetString.new( OpenSSL::ASN1::Sequence.new( [ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(1)), OpenSSL::ASN1::OctetString.new(priv_bn.to_s(2)), OpenSSL::ASN1::ASN1Data.new( [ OpenSSL::ASN1::BitString.new(point.to_octet_string(:uncompressed)), ], 1, :CONTEXT_SPECIFIC, ) ] ).to_der ) ] ) key = OpenSSL::PKey::EC.new(asn1.to_der) return key rescue OpenSSL::PKey::ECError raise NotImplementedError, "unsupported key type `#{type}'" end else netssh_read_private_keyblob(type) end end alias_method :netssh_read_private_keyblob, :read_private_keyblob alias_method :read_private_keyblob, :vagrant_read_private_keyblob end OpenSSL::PKey::EC::Point.class_eval do include Net::SSH::Authentication::PubKeyFingerprint def to_pem "#{ssh_type} #{self.to_bn.to_s(2)}" end end end ================================================ FILE: lib/vagrant/patches/rubygems.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This allows for effective monkey patching of the MakeMakefile # module when building gem extensions. When gem extensions are # built, the extconf.rb file is executed as a separate process. # To support monkey patching the MakeMakefile module, the ruby # executable path is adjusted to add a custom load path allowing # a customized mkmf.rb file to load the proper mkmf.rb file, and # then applying the proper patches. if Gem.win_platform? Gem.class_eval do class << self def vagrant_ruby cmd = ruby_ruby "#{cmd} -I\"#{Vagrant.source_root.join("lib/vagrant/patches/builder")}\"" end alias_method :ruby_ruby, :ruby alias_method :ruby, :vagrant_ruby end end end ================================================ FILE: lib/vagrant/patches/timeout_error.rb ================================================ # Adds an IO::TimeoutError for versions of ruby where it isn't defined (< 3.2). if !defined?(IO::TimeoutError) class IO::TimeoutError < StandardError end end ================================================ FILE: lib/vagrant/plugin/manager.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "set" require_relative "../bundler" require_relative "../shared_helpers" require_relative "state_file" module Vagrant module Plugin # The Manager helps with installing, listing, and initializing plugins. class Manager # Returns the path to the [StateFile] for user plugins. # # @return [Pathname] def self.user_plugins_file Vagrant.user_data_path.join("plugins.json") end # Returns the path to the [StateFile] for system plugins. def self.system_plugins_file dir = Vagrant.installer_embedded_dir return nil if !dir Pathname.new(dir).join("plugins.json") end def self.instance @instance ||= self.new(user_plugins_file) end attr_reader :user_file attr_reader :system_file attr_reader :local_file # @param [Pathname] user_file def initialize(user_file) @logger = Log4r::Logger.new("vagrant::plugin::manager") @user_file = StateFile.new(user_file) system_path = self.class.system_plugins_file @system_file = nil @system_file = StateFile.new(system_path) if system_path && system_path.file? @local_file = nil @globalized = @localized = false end # Enable global plugins # # @return [Hash] list of plugins def globalize! @globalized = true @logger.debug("Enabling globalized plugins") plugins = installed_plugins bundler_init(plugins, global: user_file.path) plugins end # Enable environment local plugins # # @param [Environment] env Vagrant environment # @return [Hash, nil] list of plugins def localize!(env) @localized = true if env.local_data_path @logger.debug("Enabling localized plugins") @local_file = StateFile.new(env.local_data_path.join("plugins.json")) Vagrant::Bundler.instance.environment_path = env.local_data_path plugins = local_file.installed_plugins bundler_init(plugins, local: local_file.path) plugins end end # @return [Boolean] local and global plugins are loaded def ready? @globalized && @localized end # Initialize bundler with given plugins # # @param [Hash] plugins List of plugins # @return [nil] def bundler_init(plugins, **opts) if !Vagrant.plugins_init? @logger.warn("Plugin initialization is disabled") return nil end @logger.info("Plugins:") plugins.each do |plugin_name, plugin_info| installed_version = plugin_info["installed_gem_version"] version_constraint = plugin_info["gem_version"] installed_version = 'undefined' if installed_version.to_s.empty? version_constraint = '> 0' if version_constraint.to_s.empty? @logger.info( " - #{plugin_name} = [installed: " \ "#{installed_version} constraint: " \ "#{version_constraint}]" ) end begin Vagrant::Bundler.instance.init!(plugins, **opts) rescue StandardError, ScriptError => err @logger.error("Plugin initialization error - #{err.class}: #{err}") err.backtrace.each do |backtrace_line| @logger.debug(backtrace_line) end raise Vagrant::Errors::PluginInitError, message: err.to_s end end # Installs another plugin into our gem directory. # # @param [String] name Name of the plugin (gem) # @return [Gem::Specification] def install_plugin(name, **opts) if opts[:env_local] && @local_file.nil? raise Errors::PluginNoLocalError end if name =~ /\.gem$/ # If this is a gem file, then we install that gem locally. local_spec = Vagrant::Bundler.instance.install_local(name, opts) name = local_spec.name opts[:version] = local_spec.version.to_s end plugins = installed_plugins plugins[name] = { "require" => opts[:require], "gem_version" => opts[:version], "sources" => opts[:sources], } if local_spec.nil? result = nil install_lambda = lambda do Vagrant::Bundler.instance.install(plugins, opts[:env_local]).each do |spec| next if spec.name != name next if result && result.version >= spec.version result = spec end end if opts[:verbose] Vagrant::Bundler.instance.verbose(&install_lambda) else install_lambda.call end else result = local_spec end if result # Add the plugin to the state file plugin_file = opts[:env_local] ? @local_file : @user_file plugin_file.add_plugin( result.name, version: opts[:version], require: opts[:require], sources: opts[:sources], env_local: !!opts[:env_local], installed_gem_version: result.version.to_s ) else r = Gem::Dependency.new(name, opts[:version]) result = Gem::Specification.find { |s| s.satisfies_requirement?(r) && s.activated? } raise Errors::PluginInstallFailed, name: name if result.nil? @logger.warn("Plugin install returned no result as no new plugins were installed.") end # After install clean plugin gems to remove any cruft. This is useful # for removing outdated dependencies or other versions of an installed # plugin if the plugin is upgraded/downgraded Vagrant::Bundler.instance.clean(installed_plugins, local: !!opts[:local]) result rescue Gem::GemNotFoundException raise Errors::PluginGemNotFound, name: name rescue Gem::Exception => err @logger.warn("Failed to install plugin: #{err}") @logger.debug("#{err.class}: #{err}\n#{err.backtrace.join("\n")}") # Try and determine a cause for the failure case err.message when /install development tools first/ raise Errors::PluginNeedsDeveloperTools when /library not found in default locations/ lib = err.message.match(/(\w+) library not found in default locations/) if lib.nil? raise Errors::BundlerError, message: err.message end raise Errors::PluginMissingLibrary, library: lib.captures.first, name: name when /find header files for ruby/ raise Errors::PluginMissingRubyDev else raise Errors::BundlerError, message: err.message end end # Uninstalls the plugin with the given name. # # @param [String] name def uninstall_plugin(name, **opts) if @system_file if !@user_file.has_plugin?(name) && @system_file.has_plugin?(name) raise Errors::PluginUninstallSystem, name: name end end if opts[:env_local] && @local_file.nil? raise Errors::PluginNoLocalError end plugin_file = opts[:env_local] ? @local_file : @user_file if !plugin_file.has_plugin?(name) raise Errors::PluginNotInstalled, name: name end plugin_file.remove_plugin(name) # Clean the environment, removing any old plugins Vagrant::Bundler.instance.clean(installed_plugins) rescue Gem::Exception => e raise Errors::BundlerError, message: e.to_s end # Updates all or a specific set of plugins. def update_plugins(specific, **opts) if opts[:env_local] && @local_file.nil? raise Errors::PluginNoLocalError end plugin_file = opts[:env_local] ? @local_file : @user_file result = Vagrant::Bundler.instance.update(plugin_file.installed_plugins, specific) plugin_file.installed_plugins.each do |name, info| matching_spec = result.detect{|s| s.name == name} info = Hash[ info.map do |key, value| [key.to_sym, value] end ] if matching_spec plugin_file.add_plugin(name, **info.merge( version: "> 0", installed_gem_version: matching_spec.version.to_s )) end end Vagrant::Bundler.instance.clean(installed_plugins) result rescue Gem::Exception => e raise Errors::BundlerError, message: e.to_s end # This returns the list of plugins that should be enabled. # # @return [Hash] def installed_plugins system = {} if @system_file @system_file.installed_plugins.each do |k, v| system[k] = v.merge("system" => true) end end plugin_list = Util::DeepMerge.deep_merge(system, @user_file.installed_plugins) if @local_file plugin_list = Util::DeepMerge.deep_merge(plugin_list, @local_file.installed_plugins) end # Sort plugins by name Hash[ plugin_list.map{|plugin_name, plugin_info| [plugin_name, plugin_info] }.sort_by(&:first) ] end # This returns the list of plugins that are installed as # Gem::Specifications. # # @return [Array] def installed_specs installed_plugin_info = installed_plugins installed = Set.new(installed_plugin_info.keys) installed_versions = Hash[ installed_plugin_info.map{|plugin_name, plugin_info| gem_version = plugin_info["gem_version"].to_s gem_version = "> 0" if gem_version.empty? [plugin_name, Gem::Requirement.new(gem_version)] } ] # Go through the plugins installed in this environment and # get the latest version of each. installed_map = {} Gem::Specification.find_all.each do |spec| # Ignore specs that aren't in our installed list next if !installed.include?(spec.name) next if installed_versions[spec.name] && !installed_versions[spec.name].satisfied_by?(spec.version) # If we already have a newer version in our list of installed, # then ignore it next if installed_map.key?(spec.name) && installed_map[spec.name].version >= spec.version installed_map[spec.name] = spec end installed_map.values end # Loads the requested plugins into the Vagrant runtime # # @param [Hash] plugins List of plugins to load # @return [nil] def load_plugins(plugins) if !Vagrant.plugins_enabled? @logger.warn("Plugin loading is disabled") return end if plugins.nil? @logger.debug("No plugins provided for loading") return end begin @logger.info("Loading plugins...") plugins.each do |plugin_name, plugin_info| if plugin_info["require"].to_s.empty? begin @logger.info("Loading plugin `#{plugin_name}` with default require: `#{plugin_name}`") require plugin_name rescue LoadError => err if plugin_name.include?("-") plugin_slash = plugin_name.gsub("-", "/") @logger.error("Failed to load plugin `#{plugin_name}` with default require. - #{err.class}: #{err}") @logger.info("Loading plugin `#{plugin_name}` with slash require: `#{plugin_slash}`") require plugin_slash else raise end end else @logger.debug("Loading plugin `#{plugin_name}` with custom require: `#{plugin_info["require"]}`") require plugin_info["require"] end @logger.debug("Successfully loaded plugin `#{plugin_name}`.") end if defined?(::Bundler) @logger.debug("Bundler detected in use. Loading `:plugins` group.") ::Bundler.require(:plugins) end rescue ScriptError, StandardError => err @logger.error("Plugin loading error: #{err.class} - #{err}") err.backtrace.each do |backtrace_line| @logger.debug(backtrace_line) end raise Vagrant::Errors::PluginLoadError, message: err.to_s end nil end # Check if the requested plugin is installed # # @param [String] name Name of plugin # @param [String] version Specific version of the plugin # @return [Boolean] def plugin_installed?(name, version=nil) # Make the requirement object version = Gem::Requirement.new([version.to_s]) if version # If plugins are loaded, check for match in loaded specs if ready? return installed_specs.any? do |s| match = s.name == name next match if !version next match && version.satisfied_by?(s.version) end end # Plugins are not loaded yet so check installed plugin data plugin_info = installed_plugins[name] return false if !plugin_info return !!plugin_info if version.nil? || plugin_info["installed_gem_version"].nil? installed_version = Gem::Version.new(plugin_info["installed_gem_version"]) version.satisfied_by?(installed_version) end end end end ================================================ FILE: lib/vagrant/plugin/state_file.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" require "fileutils" require "tempfile" module Vagrant module Plugin # This is a helper to deal with the plugin state file that Vagrant # uses to track what plugins are installed and activated and such. class StateFile # @return [Pathname] path to file attr_reader :path def initialize(path) @path = path @data = {} if @path.exist? begin @data = JSON.parse(@path.read) rescue JSON::ParserError => e raise Vagrant::Errors::PluginStateFileParseError, path: path, message: e.message end upgrade_v0! if !@data["version"] end @data["version"] ||= "1" @data["installed"] ||= {} end # Add a plugin that is installed to the state file. # # @param [String] name The name of the plugin def add_plugin(name, **opts) @data["installed"][name] = { "ruby_version" => RUBY_VERSION, "vagrant_version" => Vagrant::VERSION, "gem_version" => opts[:version] || "", "require" => opts[:require] || "", "sources" => opts[:sources] || [], "installed_gem_version" => opts[:installed_gem_version], "env_local" => !!opts[:env_local] } save! end # Adds a RubyGems index source to look up gems. # # @param [String] url URL of the source. def add_source(url) @data["sources"] ||= [] @data["sources"] << url if !@data["sources"].include?(url) save! end # This returns a hash of installed plugins according to the state # file. Note that this may _not_ directly match over to actually # installed gems. # # @return [Hash] def installed_plugins @data["installed"] end # Returns true/false if the plugin is present in this state file. # # @return [Boolean] def has_plugin?(name) @data["installed"].key?(name) end # Remove a plugin that is installed from the state file. # # @param [String] name The name of the plugin. def remove_plugin(name) @data["installed"].delete(name) save! end # Remove a source for RubyGems. # # @param [String] url URL of the source def remove_source(url) @data["sources"] ||= [] @data["sources"].delete(url) save! end # Returns the list of RubyGems sources that will be searched for # plugins. # # @return [Array] def sources @data["sources"] || [] end # This saves the state back into the state file. def save! Tempfile.open(@path.basename.to_s, @path.dirname.to_s) do |f| f.binmode f.write(JSON.dump(@data)) f.fsync f.chmod(0644) f.close FileUtils.mv(f.path, @path) end end protected # This upgrades the internal data representation from V0 (the initial # version) to V1. def upgrade_v0! @data["version"] = "1" new_installed = {} (@data["installed"] || []).each do |plugin| new_installed[plugin] = { "ruby_version" => "0", "vagrant_version" => "0", } end @data["installed"] = new_installed save! end end end end ================================================ FILE: lib/vagrant/plugin/v1/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' require "vagrant/util/safe_puts" module Vagrant module Plugin module V1 # This is the base class for a CLI command. class Command include Util::SafePuts def initialize(argv, env) @argv = argv @env = env @logger = Log4r::Logger.new("vagrant::command::#{self.class.to_s.downcase}") end # This is what is called on the class to actually execute it. Any # subclasses should implement this method and do any option parsing # and validation here. def execute end protected # Parses the options given an OptionParser instance. # # This is a convenience method that properly handles duping the # originally argv array so that it is not destroyed. # # This method will also automatically detect "-h" and "--help" # and print help. And if any invalid options are detected, the help # will be printed, as well. # # If this method returns `nil`, then you should assume that help # was printed and parsing failed. def parse_options(opts=nil) # Creating a shallow copy of the arguments so the OptionParser # doesn't destroy the originals. argv = @argv.dup # Default opts to a blank optionparser if none is given opts ||= OptionParser.new # Add the help option, which must be on every command. opts.on_tail("-h", "--help", "Print this help") do safe_puts(opts.help) return nil end opts.parse!(argv) return argv rescue OptionParser::InvalidOption raise Errors::CLIInvalidOptions, help: opts.help.chomp end # Yields a VM for each target VM for the command. # # This is a convenience method for easily implementing methods that # take a target VM (in the case of multi-VM) or every VM if no # specific VM name is specified. # # @param [String] name The name of the VM. Nil if every VM. # @param [Boolean] single_target If true, then an exception will be # raised if more than one target is found. def with_target_vms(names=nil, options=nil) # Using VMs requires a Vagrant environment to be properly setup raise Errors::NoEnvironmentError if !@env.root_path # Setup the options hash options ||= {} # Require that names be an array names ||= [] names = [names] if !names.is_a?(Array) # First determine the proper array of VMs. vms = [] if names.length > 0 names.each do |name| if pattern = name[/^\/(.+?)\/$/, 1] # This is a regular expression name, so we convert to a regular # expression and allow that sort of matching. regex = Regexp.new(pattern) @env.vms.each do |name, vm| vms << vm if name =~ regex end raise Errors::VMNoMatchError if vms.empty? else # String name, just look for a specific VM vms << @env.vms[name.to_sym] raise Errors::VMNotFoundError, name: name if !vms[0] end end else vms = @env.vms_ordered end # Make sure we're only working with one VM if single target if options[:single_target] && vms.length != 1 vm = @env.primary_vm raise Errors::MultiVMTargetRequired if !vm vms = [vm] end # If we asked for reversed ordering, then reverse it vms.reverse! if options[:reverse] # Go through each VM and yield it! vms.each do |old_vm| # We get a new VM from the environment here to avoid potentially # stale VMs (if there was a config reload on the environment # or something). vm = @env.vms[old_vm.name] yield vm end end # This method will split the argv given into three parts: the # flags to this command, the subcommand, and the flags to the # subcommand. For example: # # -v status -h -v # # The above would yield 3 parts: # # ["-v"] # "status" # ["-h", "-v"] # # These parts are useful because the first is a list of arguments # given to the current command, the second is a subcommand, and the # third are the commands given to the subcommand. # # @return [Array] The three parts. def split_main_and_subcommand(argv) # Initialize return variables main_args = nil sub_command = nil sub_args = [] # We split the arguments into two: One set containing any # flags before a word, and then the rest. The rest are what # get actually sent on to the subcommand. argv.each_index do |i| if !argv[i].start_with?("-") # We found the beginning of the sub command. Split the # args up. main_args = argv[0, i] sub_command = argv[i] sub_args = argv[i + 1, argv.length - i + 1] # Break so we don't find the next non flag and shift our # main args. break end end # Handle the case that argv was empty or didn't contain any subcommand main_args = argv.dup if main_args.nil? return [main_args, sub_command, sub_args] end end end end end ================================================ FILE: lib/vagrant/plugin/v1/communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V1 # Base class for a communicator in Vagrant. A communicator is # responsible for communicating with a machine in some way. There # are various stages of Vagrant that require things such as uploading # files to the machine, executing shell commands, etc. Implementors # of this class are expected to provide this functionality in some # way. # # Note that a communicator must provide **all** of the methods # in this base class. There is currently no way for one communicator # to provide say a more efficient way of uploading a file, but not # provide shell execution. This sort of thing will come in a future # version. class Communicator # This returns true/false depending on if the given machine # can be communicated with using this communicator. If this returns # `true`, then this class will be used as the primary communication # method for the machine. # # @return [Boolean] def self.match?(machine) false end # Initializes the communicator with the machine that we will be # communicating with. This base method does nothing (it doesn't # even store the machine in an instance variable for you), so you're # expected to override this and do something with the machine if # you care about it. # # @param [Machine] machine The machine this instance is expected to # communicate with. def initialize(machine) end # Checks if the target machine is ready for communication. If this # returns true, then all the other methods for communicating with # the machine are expected to be functional. # # @return [Boolean] def ready? false end # Download a file from the remote machine to the local machine. # # @param [String] from Path of the file on the remote machine. # @param [String] to Path of where to save the file locally. def download(from, to) end # Upload a file to the remote machine. # # @param [String] from Path of the file locally to upload. # @param [String] to Path of where to save the file on the remote # machine. def upload(from, to) end # Execute a command on the remote machine. The exact semantics # of this method are up to the implementor, but in general the # users of this class will expect this to be a shell. # # This method gives you no way to write data back to the remote # machine, so only execute commands that don't expect input. # # @param [String] command Command to execute. # @yield [type, data] Realtime output of the command being executed. # @yieldparam [String] type Type of the output. This can be # `:stdout`, `:stderr`, etc. The exact types are up to the # implementor. # @yieldparam [String] data Data for the given output. # @return [Integer] Exit code of the command. def execute(command, opts=nil) end # Executes a command on the remote machine with administrative # privileges. See {#execute} for documentation, as the API is the # same. # # @see #execute def sudo(command, opts=nil) end # Executes a command and returns true if the command succeeded, # and false otherwise. By default, this executes as a normal user, # and it is up to the communicator implementation if they expose an # option for running tests as an administrator. # # @see #execute def test(command, opts=nil) end end end end end ================================================ FILE: lib/vagrant/plugin/v1/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V1 # This is the base class for a configuration key defined for # V1. Any configuration key plugins for V1 should inherit from this # class. class Config # This constant represents an unset value. This is useful so it is # possible to know the difference between a configuration value that # was never set, and a value that is nil (explicitly). Best practice # is to initialize all variables to this value, then the {#merge} # method below will "just work" in many cases. UNSET_VALUE = Object.new # This is called as a last-minute hook that allows the configuration # object to finalize itself before it will be put into use. This is # a useful place to do some defaults in the case the user didn't # configure something or so on. # # An example of where this sort of thing is used or has been used: # the "vm" configuration key uses this to make sure that at least # one sub-VM has been defined: the default VM. # # The configuration object is expected to mutate itself. def finalize! # Default implementation is to do nothing. end # Merge another configuration object into this one. This assumes that # the other object is the same class as this one. This should not # mutate this object, but instead should return a new, merged object. # # The default implementation will simply iterate over the instance # variables and merge them together, with this object overriding # any conflicting instance variables of the older object. Instance # variables starting with "__" (double underscores) will be ignored. # This lets you set some sort of instance-specific state on your # configuration keys without them being merged together later. # # @param [Object] other The other configuration object to merge from, # this must be the same type of object as this one. # @return [Object] The merged object. def merge(other) result = self.class.new # Set all of our instance variables on the new class [self, other].each do |obj| obj.instance_variables.each do |key| # Ignore keys that start with a double underscore. This allows # configuration classes to still hold around internal state # that isn't propagated. if !key.to_s.start_with?("@__") result.instance_variable_set(key, obj.instance_variable_get(key)) end end end result end # Allows setting options from a hash. By default this simply calls # the `#{key}=` method on the config class with the value, which is # the expected behavior most of the time. # # This is expected to mutate itself. # # @param [Hash] options A hash of options to set on this configuration # key. def set_options(options) options.each do |key, value| send("#{key}=", value) end end # Converts this configuration object to JSON. def to_json(*a) instance_variables_hash.to_json(*a) end # Returns the instance variables as a hash of key-value pairs. def instance_variables_hash instance_variables.inject({}) do |acc, iv| acc[iv.to_s[1..-1]] = instance_variable_get(iv) acc end end # This is called to upgrade this V1 config to V2. The parameter given # is the full V2 configuration object, so you can do anything to it # that you want. # # No return value is expected, modifications should be made directly # to the new V2 object. # # @param [V2::Root] new def upgrade(new) end # Called after the configuration is finalized and loaded to validate # this object. # # @param [Environment] env Vagrant::Environment object of the # environment that this configuration has been loaded into. This # gives you convenient access to things like the root path # and so on. # @param [ErrorRecorder] errors def validate(env, errors) end end end end end ================================================ FILE: lib/vagrant/plugin/v1/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This file contains all the errors that the V1 plugin interface # may throw. module Vagrant module Plugin module V1 # Exceptions that can be thrown within the plugin interface all # inherit from this parent exception. class Error < StandardError; end # This is thrown when a command name given is invalid. class InvalidCommandName < Error; end end end end ================================================ FILE: lib/vagrant/plugin/v1/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V1 # The base class for a guest. A guest represents an installed system # within a machine that Vagrant manages. There are some portions of # Vagrant which are OS-specific such as mounting shared folders and # halting the machine, and this abstraction allows the implementation # for these to be separate from the core of Vagrant. class Guest class BaseError < Errors::VagrantError error_namespace("vagrant.guest.base") end include Vagrant::Util # The VM which this system is tied to. attr_reader :vm # Initializes the system. Any subclasses MUST make sure this # method is called on the parent. Therefore, if a subclass overrides # `initialize`, then you must call `super`. def initialize(vm) @vm = vm end # This method is automatically called when the system is available (when # Vagrant can successfully SSH into the machine) to give the system a chance # to determine the distro and return a distro-specific system. # # If this method returns nil, then this instance is assumed to be # the most specific guest implementation. def distro_dispatch end # Halt the machine. This method should gracefully shut down the # operating system. This method will cause `vagrant halt` and associated # commands to _block_, meaning that if the machine doesn't halt # in a reasonable amount of time, this method should just return. # # If when this method returns, the machine's state isn't "powered_off," # Vagrant will proceed to forcefully shut the machine down. def halt raise BaseError, _key: :unsupported_halt end # Mounts a shared folder. # # This method should create, mount, and properly set permissions # on the shared folder. This method should also properly # adhere to any configuration values such as `shared_folder_uid` # on `config.vm`. # # @param [String] name The name of the shared folder. # @param [String] guestpath The path on the machine which the user # wants the folder mounted. # @param [Hash] options Additional options for the shared folder # which can be honored. def mount_shared_folder(name, guestpath, options) raise BaseError, _key: :unsupported_shared_folder end # Mounts a shared folder via NFS. This assumes that the exports # via the host are already done. def mount_nfs(ip, folders) raise BaseError, _key: :unsupported_nfs end # Configures the given list of networks on the virtual machine. # # The networks parameter will be an array of hashes where the hashes # represent the configuration of a network interface. The structure # of the hash will be roughly the following: # # { # type: :static, # ip: "192.168.33.10", # netmask: "255.255.255.0", # interface: 1 # } # def configure_networks(networks) raise BaseError, _key: :unsupported_configure_networks end # Called to change the hostname of the virtual machine. def change_host_name(name) raise BaseError, _key: :unsupported_host_name end end end end end ================================================ FILE: lib/vagrant/plugin/v1/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V1 # Base class for a host in Vagrant. A host class contains functionality # that is specific to a specific OS that is running Vagrant. This # abstraction is done because there is some host-specific logic that # Vagrant must do in some cases. class Host # This returns true/false depending on if the current running system # matches the host class. # # @return [Boolean] def self.match? nil end # The precedence of the host when checking for matches. This is to # allow certain host such as generic OS's ("Linux", "BSD", etc.) # to be specified last. # # The hosts with the higher numbers will be checked first. # # If you're implementing a basic host, you can probably ignore this. def self.precedence 5 end # Initializes a new host class. # # The only required parameter is a UI object so that the host # objects have some way to communicate with the outside world. # # @param [UI] ui UI for the hosts to output to. def initialize(ui) @ui = ui end # Returns true of false denoting whether or not this host supports # NFS shared folder setup. This method ideally should verify that # NFS is installed. # # @return [Boolean] def nfs? false end # Exports the given hash of folders via NFS. # # @param [String] id A unique ID that is guaranteed to be unique to # match these sets of folders. # @param [String] ip IP of the guest machine. # @param [Hash] folders Shared folders to sync. def nfs_export(id, ip, folders) end # Prunes any NFS exports made by Vagrant which aren't in the set # of valid ids given. # # @param [Array] valid_ids Valid IDs that should not be # pruned. def nfs_prune(valid_ids) end end end end end ================================================ FILE: lib/vagrant/plugin/v1/manager.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Plugin module V1 # This class maintains a list of all the registered plugins as well # as provides methods that allow querying all registered components of # those plugins as a single unit. class Manager attr_reader :registered def initialize @logger = Log4r::Logger.new("vagrant::plugin::v1::manager") @registered = [] end # This returns all the registered communicators. # # @return [Hash] def communicators result = {} @registered.each do |plugin| result.merge!(plugin.communicator.to_hash) end result end # This returns all the registered configuration classes. # # @return [Hash] def config result = {} @registered.each do |plugin| plugin.config.each do |key, klass| result[key] = klass end end result end # This returns all the registered configuration classes that were # marked as "upgrade safe." # # @return [Hash] def config_upgrade_safe result = {} @registered.each do |plugin| configs = plugin.data[:config_upgrade_safe] if configs configs.each do |key| result[key] = plugin.config.get(key) end end end result end # This returns all the registered guests. # # @return [Hash] def guests result = {} @registered.each do |plugin| result.merge!(plugin.guest.to_hash) end result end # This returns all registered host classes. # # @return [Hash] def hosts hosts = {} @registered.each do |plugin| hosts.merge!(plugin.host.to_hash) end hosts end # This returns all registered providers. # # @return [Hash] def providers providers = {} @registered.each do |plugin| providers.merge!(plugin.provider.to_hash) end providers end # This registers a plugin. This should _NEVER_ be called by the public # and should only be called from within Vagrant. Vagrant will # automatically register V1 plugins when a name is set on the # plugin. def register(plugin) if !@registered.include?(plugin) @logger.info("Registered plugin: #{plugin.name}") @registered << plugin end end # This clears out all the registered plugins. This is only used by # unit tests and should not be called directly. def reset! @registered.clear end # This unregisters a plugin so that its components will no longer # be used. Note that this should only be used for testing purposes. def unregister(plugin) if @registered.include?(plugin) @logger.info("Unregistered: #{plugin.name}") @registered.delete(plugin) end end end end end end ================================================ FILE: lib/vagrant/plugin/v1/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "set" require "log4r" module Vagrant module Plugin module V1 # This is the superclass for all V1 plugins. class Plugin # Special marker that can be used for action hooks that matches # all action sequences. ALL_ACTIONS = :__all_actions__ # The logger for this class. LOGGER = Log4r::Logger.new("vagrant::plugin::v1::plugin") # Set the root class up to be ourself, so that we can reference this # from within methods which are probably in subclasses. ROOT_CLASS = self # This returns the manager for all V1 plugins. # # @return [V1::Manager] def self.manager @manager ||= Manager.new end # Set the name of the plugin. The moment that this is called, the # plugin will be registered and available. Before this is called, a # plugin does not exist. The name must be unique among all installed # plugins. # # @param [String] name Name of the plugin. # @return [String] The name of the plugin. def self.name(name=UNSET_VALUE) # Get or set the value first, so we have a name for logging when # we register. result = get_or_set(:name, name) # The plugin should be registered if we're setting a real name on it Plugin.manager.register(self) if name != UNSET_VALUE # Return the result result end # Sets a human-friendly description of the plugin. # # @param [String] value Description of the plugin. # @return [String] Description of the plugin. def self.description(value=UNSET_VALUE) get_or_set(:description, value) end # Registers a callback to be called when a specific action sequence # is run. This allows plugin authors to hook into things like VM # bootup, VM provisioning, etc. # # @param [Symbol] name Name of the action. # @return [Array] List of the hooks for the given action. def self.action_hook(name, &block) # Get the list of hooks for the given hook name data[:action_hooks] ||= {} hooks = data[:action_hooks][name.to_sym] ||= [] # Return the list if we don't have a block return hooks if !block_given? # Otherwise add the block to the list of hooks for this action. hooks << block end # Defines additional command line commands available by key. The key # becomes the subcommand, so if you register a command "foo" then # "vagrant foo" becomes available. # # @param [String] name Subcommand key. def self.command(name=UNSET_VALUE, &block) data[:command] ||= Registry.new if name != UNSET_VALUE # Validate the name of the command if name.to_s !~ /^[-a-z0-9]+$/i raise InvalidCommandName, "Commands can only contain letters, numbers, and hyphens" end # Register a new command class only if a name was given. data[:command].register(name.to_sym, &block) end # Return the registry data[:command] end # Defines additional communicators to be available. Communicators # should be returned by a block passed to this method. This is done # to ensure that the class is lazy loaded, so if your class inherits # from or uses any Vagrant internals specific to Vagrant 1.0, then # the plugin can still be defined without breaking anything in future # versions of Vagrant. # # @param [String] name Communicator name. def self.communicator(name=UNSET_VALUE, &block) data[:communicator] ||= Registry.new # Register a new communicator class only if a name was given. data[:communicator].register(name.to_sym, &block) if name != UNSET_VALUE # Return the registry data[:communicator] end # Defines additional configuration keys to be available in the # Vagrantfile. The configuration class should be returned by a # block passed to this method. This is done to ensure that the class # is lazy loaded, so if your class inherits from any classes that # are specific to Vagrant 1.0, then the plugin can still be defined # without breaking anything in future versions of Vagrant. # # @param [String] name Configuration key. # @param [Boolean] upgrade_safe If this is true, then this configuration # key is safe to load during an upgrade, meaning that it depends # on NO Vagrant internal classes. Do _not_ set this to true unless # you really know what you're doing, since you can cause Vagrant # to crash (although Vagrant will output a user-friendly error # message if this were to happen). def self.config(name=UNSET_VALUE, upgrade_safe=false, &block) data[:config] ||= Registry.new # Register a new config class only if a name was given. if name != UNSET_VALUE data[:config].register(name.to_sym, &block) # If we were told this is an upgrade safe configuration class # then we add it to the set. if upgrade_safe data[:config_upgrade_safe] ||= Set.new data[:config_upgrade_safe].add(name.to_sym) end end # Return the registry data[:config] end # Defines an additionally available guest implementation with # the given key. # # @param [String] name Name of the guest. def self.guest(name=UNSET_VALUE, &block) data[:guests] ||= Registry.new # Register a new guest class only if a name was given data[:guests].register(name.to_sym, &block) if name != UNSET_VALUE # Return the registry data[:guests] end # Defines an additionally available host implementation with # the given key. # # @param [String] name Name of the host. def self.host(name=UNSET_VALUE, &block) data[:hosts] ||= Registry.new # Register a new host class only if a name was given data[:hosts].register(name.to_sym, &block) if name != UNSET_VALUE # Return the registry data[:hosts] end # Registers additional providers to be available. # # @param [Symbol] name Name of the provider. def self.provider(name=UNSET_VALUE, &block) data[:providers] ||= Registry.new # Register a new provider class only if a name was given data[:providers].register(name.to_sym, &block) if name != UNSET_VALUE # Return the registry data[:providers] end # Registers additional provisioners to be available. # # @param [String] name Name of the provisioner. def self.provisioner(name=UNSET_VALUE, &block) data[:provisioners] ||= Registry.new # Register a new provisioner class only if a name was given data[:provisioners].register(name.to_sym, &block) if name != UNSET_VALUE # Return the registry data[:provisioners] end # Returns the internal data associated with this plugin. This # should NOT be called by the general public. # # @return [Hash] def self.data @data ||= {} end protected # Sentinel value denoting that a value has not been set. UNSET_VALUE = Object.new # Helper method that will set a value if a value is given, or otherwise # return the already set value. # # @param [Symbol] key Key for the data # @param [Object] value Value to store. # @return [Object] Stored value. def self.get_or_set(key, value=UNSET_VALUE) # If no value is to be set, then return the value we have already set return data[key] if value.eql?(UNSET_VALUE) # Otherwise set the value data[key] = value end end end end end ================================================ FILE: lib/vagrant/plugin/v1/provider.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V1 # This is the base class for a provider for the V1 API. A provider # is responsible for creating compute resources to match the needs # of a Vagrant-configured system. class Provider # Initialize the provider to represent the given machine. # # @param [Vagrant::Machine] machine The machine that this provider # is responsible for. def initialize(machine) end # This should return an action callable for the given name. # # @param [Symbol] name Name of the action. # @return [Object] A callable action sequence object, whether it # is a proc, object, etc. def action(name) nil end # This method is called if the underlying machine ID changes. Providers # can use this method to load in new data for the actual backing # machine or to realize that the machine is now gone (the ID can # become `nil`). No parameters are given, since the underlying machine # is simply the machine instance given to this object. And no # return value is necessary. def machine_id_changed end # This should return a hash of information that explains how to # SSH into the machine. If the machine is not at a point where # SSH is even possible, then `nil` should be returned. # # The general structure of this returned hash should be the # following: # # { # host: "1.2.3.4", # port: "22", # username: "mitchellh", # private_key_path: "/path/to/my/key" # } # # **Note:** Vagrant only supports private key based authentication, # mainly for the reason that there is no easy way to exec into an # `ssh` prompt with a password, whereas we can pass a private key # via commandline. # # @return [Hash] SSH information. For the structure of this hash # read the accompanying documentation for this method. def ssh_info nil end # This should return the state of the machine within this provider. # The state can be any symbol. # # @return [Symbol] def state nil end end end end end ================================================ FILE: lib/vagrant/plugin/v1/provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V1 # This is the base class for a provisioner for the V1 API. A provisioner # is primarily responsible for installing software on a Vagrant guest. class Provisioner # The environment which provisioner is running in. This is the # action environment, not a Vagrant::Environment. attr_reader :env # The configuration for this provisioner. This will be an instance of # the `Config` class which is part of the provisioner. attr_reader :config def initialize(env, config) @env = env @config = config end # This method is expected to return a class that is used for # configuring the provisioner. This return value is expected to be # a subclass of {Config}. # # @return [Config] def self.config_class end # This is the method called to "prepare" the provisioner. This is called # before any actions are run by the action runner (see {Vagrant::Actions::Runner}). # This can be used to setup shared folders, forward ports, etc. Whatever is # necessary on a "meta" level. # # No return value is expected. def prepare end # This is the method called to provision the system. This method # is expected to do whatever necessary to provision the system (create files, # SSH, etc.) def provision! end # This is the method called to when the system is being destroyed # and allows the provisioners to engage in any cleanup tasks necessary. def cleanup end end end end end ================================================ FILE: lib/vagrant/plugin/v1.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/plugin/v1/errors" module Vagrant module Plugin module V1 autoload :Command, "vagrant/plugin/v1/command" autoload :Communicator, "vagrant/plugin/v1/communicator" autoload :Config, "vagrant/plugin/v1/config" autoload :Guest, "vagrant/plugin/v1/guest" autoload :Host, "vagrant/plugin/v1/host" autoload :Manager, "vagrant/plugin/v1/manager" autoload :Plugin, "vagrant/plugin/v1/plugin" autoload :Provider, "vagrant/plugin/v1/provider" autoload :Provisioner, "vagrant/plugin/v1/provisioner" # Errors autoload :Error, "vagrant/plugin/v1/error" autoload :InvalidCommandName, "vagrant/plugin/v1/error" end end end ================================================ FILE: lib/vagrant/plugin/v2/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' require "vagrant/util/safe_puts" module Vagrant module Plugin module V2 # This is the base class for a CLI command. class Command include Util::SafePuts # This should return a brief (60 characters or less) synopsis of what # this command does. It will be used in the output of the help. # # @return [String] def self.synopsis "" end def initialize(argv, env) @argv = argv @env = env @logger = Log4r::Logger.new("vagrant::command::#{self.class.to_s.downcase}") end # This is what is called on the class to actually execute it. Any # subclasses should implement this method and do any option parsing # and validation here. def execute end protected # Parses the options given an OptionParser instance. # # This is a convenience method that properly handles duping the # originally argv array so that it is not destroyed. # # This method will also automatically detect "-h" and "--help" # and print help. And if any invalid options are detected, the help # will be printed, as well. # # If this method returns `nil`, then you should assume that help # was printed and parsing failed. def parse_options(opts=nil) # make sure optparse doesn't use POSIXLY_CORRECT parsing ENV["POSIXLY_CORRECT"] = nil # Creating a shallow copy of the arguments so the OptionParser # doesn't destroy the originals. argv = @argv.dup # Default opts to a blank optionparser if none is given opts ||= Vagrant::OptionParser.new # Add the help option, which must be on every command. opts.on_tail("-h", "--help", "Print this help") do safe_puts(opts.help) return nil end opts.parse!(argv) return argv rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::AmbiguousOption raise Errors::CLIInvalidOptions, help: opts.help.chomp end # Yields a VM for each target VM for the command. # # This is a convenience method for easily implementing methods that # take a target VM (in the case of multi-VM) or every VM if no # specific VM name is specified. # # @param [String] name The name of the VM. Nil if every VM. # @param [Hash] options Additional tweakable settings. # @option options [Symbol] :provider The provider to back the # machines with. All machines will be backed with this # provider. If none is given, a sensible default is chosen. # @option options [Boolean] :reverse If true, the resulting order # of machines is reversed. # @option options [Boolean] :single_target If true, then an # exception will be raised if more than one target is found. def with_target_vms(names=nil, options=nil) @logger.debug("Getting target VMs for command. Arguments:") @logger.debug(" -- names: #{names.inspect}") @logger.debug(" -- options: #{options.inspect}") # Setup the options hash options ||= {} # Require that names be an array names ||= [] names = [names] if !names.is_a?(Array) # Determine if we require a local Vagrant environment. There are # two cases that we require a local environment: # # * We're asking for ANY/EVERY VM (no names given). # # * We're asking for specific VMs, at least once of which # is NOT in the local machine index. # requires_local_env = false requires_local_env = true if names.empty? requires_local_env ||= names.any? { |n| !@env.machine_index.include?(n) } raise Errors::NoEnvironmentError if requires_local_env && !@env.root_path @logger.info("getting active machines") # Cache the active machines outside the loop active_machines = @env.active_machines # This is a helper that gets a single machine with the proper # provider. The "proper provider" in this case depends on what was # given: # # * If a provider was explicitly specified, then use that provider. # But if an active machine exists with a DIFFERENT provider, # then throw an error (for now), since we don't yet support # bringing up machines with different providers. # # * If no provider was specified, then use the active machine's # provider if it exists, otherwise use the default provider. # get_machine = lambda do |name| # Check for an active machine with the same name provider_to_use = options[:provider] provider_to_use = provider_to_use.to_sym if provider_to_use # If we have this machine in our index, load that. entry = @env.machine_index.get(name.to_s) if entry @env.machine_index.release(entry) # Create an environment for this location and yield the # machine in that environment. We silence warnings here because # Vagrantfiles often have constants, so people would otherwise # constantly (heh) get "already initialized constant" warnings. begin env = entry.vagrant_env( @env.home_path, ui_class: @env.ui_class) rescue Vagrant::Errors::EnvironmentNonExistentCWD # This means that this environment working directory # no longer exists, so delete this entry. entry = @env.machine_index.get(name.to_s) @env.machine_index.delete(entry) if entry raise end next env.machine(entry.name.to_sym, entry.provider.to_sym) end active_machines.each do |active_name, active_provider| if name == active_name # We found an active machine with the same name if provider_to_use && provider_to_use != active_provider # We found an active machine with a provider that doesn't # match the requested provider. Show an error. raise Errors::ActiveMachineWithDifferentProvider, name: active_name.to_s, active_provider: active_provider.to_s, requested_provider: provider_to_use.to_s else # Use this provider and exit out of the loop. One of the # invariants [for now] is that there shouldn't be machines # with multiple providers. @logger.info("Active machine found with name #{active_name}. " + "Using provider: #{active_provider}") provider_to_use = active_provider break end end end # Use the default provider if nothing else provider_to_use ||= @env.default_provider(machine: name) # Get the right machine with the right provider @env.machine(name, provider_to_use) end # First determine the proper array of VMs. machines = [] if names.length > 0 names.each do |name| if pattern = name[/^\/(.+?)\/$/, 1] @logger.debug("Finding machines that match regex: #{pattern}") # This is a regular expression name, so we convert to a regular # expression and allow that sort of matching. regex = Regexp.new(pattern) @env.machine_names.each do |machine_name| if machine_name =~ regex machines << get_machine.call(machine_name) end end raise Errors::VMNoMatchError if machines.empty? else # String name, just look for a specific VM @logger.debug("Finding machine that match name: #{name}") machines << get_machine.call(name.to_sym) raise Errors::VMNotFoundError, name: name if !machines[0] end end else # No name was given, so we return every VM in the order # configured. @logger.debug("Loading all machines...") machines = @env.machine_names.map do |machine_name| get_machine.call(machine_name) end end @logger.debug("have machine list to process") # Make sure we're only working with one VM if single target if options[:single_target] && machines.length != 1 @logger.debug("Using primary machine since single target") primary_name = @env.primary_machine_name raise Errors::MultiVMTargetRequired if !primary_name machines = [get_machine.call(primary_name)] end # If we asked for reversed ordering, then reverse it machines.reverse! if options[:reverse] # Go through each VM and yield it! color_order = [:default] color_index = 0 machines.each do |machine| if (machine.state && machine.state.id != :not_created && !machine.index_uuid.nil? && !@env.machine_index.include?(machine.index_uuid)) machine.recover_machine(machine.state.id) end # Set the machine color machine.ui.opts[:color] = color_order[color_index % color_order.length] color_index += 1 @logger.info("With machine: #{machine.name} (#{machine.provider.inspect})") yield machine # Call the state method so that we update our index state. Don't # worry about exceptions here, since we just care about updating # the cache. begin # Called for side effects machine.state rescue Errors::VagrantError end end end # This method will split the argv given into three parts: the # flags to this command, the subcommand, and the flags to the # subcommand. For example: # # -v status -h -v # # The above would yield 3 parts: # # ["-v"] # "status" # ["-h", "-v"] # # These parts are useful because the first is a list of arguments # given to the current command, the second is a subcommand, and the # third are the commands given to the subcommand. # # @return [Array] The three parts. def split_main_and_subcommand(argv) # Initialize return variables main_args = nil sub_command = nil sub_args = [] # We split the arguments into two: One set containing any # flags before a word, and then the rest. The rest are what # get actually sent on to the subcommand. argv.each_index do |i| if !argv[i].start_with?("-") # We found the beginning of the sub command. Split the # args up. main_args = argv[0, i] sub_command = argv[i] sub_args = argv[i + 1, argv.length - i + 1] # Break so we don't find the next non flag and shift our # main args. break end end # Handle the case that argv was empty or didn't contain any subcommand main_args = argv.dup if main_args.nil? return [main_args, sub_command, sub_args] end end end end end ================================================ FILE: lib/vagrant/plugin/v2/communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "timeout" module Vagrant module Plugin module V2 # Base class for a communicator in Vagrant. A communicator is # responsible for communicating with a machine in some way. There # are various stages of Vagrant that require things such as uploading # files to the machine, executing shell commands, etc. Implementors # of this class are expected to provide this functionality in some # way. # # Note that a communicator must provide **all** of the methods # in this base class. There is currently no way for one communicator # to provide say a more efficient way of uploading a file, but not # provide shell execution. This sort of thing will come in a future # version. class Communicator # This returns true/false depending on if the given machine # can be communicated with using this communicator. If this returns # `true`, then this class will be used as the primary communication # method for the machine. # # @return [Boolean] def self.match?(machine) true end # Initializes the communicator with the machine that we will be # communicating with. This base method does nothing (it doesn't # even store the machine in an instance variable for you), so you're # expected to override this and do something with the machine if # you care about it. # # @param [Machine] machine The machine this instance is expected to # communicate with. def initialize(machine) end # Checks if the target machine is ready for communication. If this # returns true, then all the other methods for communicating with # the machine are expected to be functional. # # @return [Boolean] def ready? false end # wait_for_ready waits until the communicator is ready, blocking # until then. It will wait up to the given duration or raise an # exception if something goes wrong. # # @param [Integer] duration Timeout in seconds. # @return [Boolean] Will return true on successful connection # or false on timeout. def wait_for_ready(duration) # By default, we implement a naive solution. begin Timeout.timeout(duration) do while true return true if ready? sleep 0.5 end end rescue Timeout::Error # We timed out, we failed. end return false end # Download a file from the remote machine to the local machine. # # @param [String] from Path of the file on the remote machine. # @param [String] to Path of where to save the file locally. def download(from, to) end # Upload a file to the remote machine. # # @param [String] from Path of the file locally to upload. # @param [String] to Path of where to save the file on the remote # machine. def upload(from, to) end # Execute a command on the remote machine. The exact semantics # of this method are up to the implementor, but in general the # users of this class will expect this to be a shell. # # This method gives you no way to write data back to the remote # machine, so only execute commands that don't expect input. # # @param [String] command Command to execute. # @yield [type, data] Realtime output of the command being executed. # @yieldparam [String] type Type of the output. This can be # `:stdout`, `:stderr`, etc. The exact types are up to the # implementor. # @yieldparam [String] data Data for the given output. # @return [Integer] Exit code of the command. def execute(command, opts=nil) end # Executes a command on the remote machine with administrative # privileges. See {#execute} for documentation, as the API is the # same. # # @see #execute def sudo(command, opts=nil) end # Executes a command and returns true if the command succeeded, # and false otherwise. By default, this executes as a normal user, # and it is up to the communicator implementation if they expose an # option for running tests as an administrator. # # @see #execute def test(command, opts=nil) end # Reset the communicator. For communicators which establish # a persistent connection to the remote machine, this connection # should be terminated and re-established. The communicator # instance should be in a "fresh" state after calling this method. def reset! end end end end end ================================================ FILE: lib/vagrant/plugin/v2/components.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V2 # This is the container class for the components of a single plugin. # This allows us to separate the plugin class which defines the # components, and the actual container of those components. This # removes a bit of state overhead from the plugin class itself. class Components # This contains all the action hooks. # # @return [Hash] attr_reader :action_hooks # This contains all the command plugins by name, and returns # the command class and options. The command class is wrapped # in a Proc so that it can be lazy loaded. # # @return [Registry>] attr_reader :commands # This contains all the configuration plugins by scope. # # @return [Hash] attr_reader :configs # This contains all the guests and their parents. # # @return [Registry>] attr_reader :guests # This contains all the registered guest capabilities. # # @return [Hash] attr_reader :guest_capabilities # This contains all the hosts and their parents. # # @return [Registry>] attr_reader :hosts # This contains all the registered host capabilities. # # @return [Hash] attr_reader :host_capabilities # This contains all the provider plugins by name, and returns # the provider class and options. # # @return [Hash] attr_reader :providers # This contains all the registered provider capabilities. # # @return [Hash] attr_reader :provider_capabilities # This contains all the push implementations by name. # # @return [Registry>] attr_reader :pushes # This contains all the synced folder implementations by name. # # @return [Registry>] attr_reader :synced_folders # This contains all the registered synced folder capabilities. # # @return [Hash] attr_reader :synced_folder_capabilities def initialize # The action hooks hash defaults to [] @action_hooks = Hash.new { |h, k| h[k] = [] } @commands = Registry.new @configs = Hash.new { |h, k| h[k] = Registry.new } @guests = Registry.new @guest_capabilities = Hash.new { |h, k| h[k] = Registry.new } @hosts = Registry.new @host_capabilities = Hash.new { |h, k| h[k] = Registry.new } @providers = Registry.new @provider_capabilities = Hash.new { |h, k| h[k] = Registry.new } @pushes = Registry.new @synced_folders = Registry.new @synced_folder_capabilities = Hash.new { |h, k| h[k] = Registry.new } end end end end end ================================================ FILE: lib/vagrant/plugin/v2/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "set" module Vagrant module Plugin module V2 # This is the base class for a configuration key defined for # V2. Any configuration key plugins for V2 should inherit from this # class. class Config # This constant represents an unset value. This is useful so it is # possible to know the difference between a configuration value that # was never set, and a value that is nil (explicitly). Best practice # is to initialize all variables to this value, then the {#merge} # method below will "just work" in many cases. UNSET_VALUE = :__UNSET__VALUE__ # This is called as a last-minute hook that allows the configuration # object to finalize itself before it will be put into use. This is # a useful place to do some defaults in the case the user didn't # configure something or so on. # # An example of where this sort of thing is used or has been used: # the "vm" configuration key uses this to make sure that at least # one sub-VM has been defined: the default VM. # # The configuration object is expected to mutate itself. def finalize! # Default implementation is to do nothing. end # Merge another configuration object into this one. This assumes that # the other object is the same class as this one. This should not # mutate this object, but instead should return a new, merged object. # # The default implementation will simply iterate over the instance # variables and merge them together, with this object overriding # any conflicting instance variables of the older object. Instance # variables starting with "__" (double underscores) will be ignored. # This lets you set some sort of instance-specific state on your # configuration keys without them being merged together later. # # @param [Object] other The other configuration object to merge from, # this must be the same type of object as this one. # @return [Object] The merged object. def merge(other) result = self.class.new # Set all of our instance variables on the new class [self, other].each do |obj| obj.instance_variables.each do |key| # Ignore keys that start with a double underscore. This allows # configuration classes to still hold around internal state # that isn't propagated. if !key.to_s.start_with?("@__") # Don't set the value if it is the unset value, either. value = obj.instance_variable_get(key) result.instance_variable_set(key, value) if value != UNSET_VALUE end end end # Persist through the set of invalid methods this_invalid = @__invalid_methods || Set.new other_invalid = other.instance_variable_get(:"@__invalid_methods") || Set.new result.instance_variable_set(:"@__invalid_methods", this_invalid + other_invalid) result end # Capture all bad configuration calls and save them for an error # message later during validation. def method_missing(name, *args, &block) return super if @__finalized # There are a few scenarios where ruby will attempt to implicity # coerce a given object into a certain type. Configs can end up # in some of these scenarios when they're being shipped around in # callbacks with splats. If method_missing allows these methods to be # called but continues to return Config back, Ruby will raise a # TypeError. Doing the normal thing of raising NoMethodError allows # Config to behave normally as its being passed through splats. # # For a bit more detail and some keywords for further searching, see: # https://ruby-doc.org/core-2.7.2/doc/implicit_conversion_rdoc.html if [:to_hash, :to_ary].include?(name) return super end name = name.to_s name = name[0...-1] if name.end_with?("=") @__invalid_methods ||= Set.new @__invalid_methods.add(name) # Return the dummy object so that anything else works ::Vagrant::Config::V2::DummyConfig.new end # Allows setting options from a hash. By default this simply calls # the `#{key}=` method on the config class with the value, which is # the expected behavior most of the time. # # This is expected to mutate itself. # # @param [Hash] options A hash of options to set on this configuration # key. def set_options(options) options.each do |key, value| send("#{key}=", value) end end # Converts this configuration object to JSON. def to_json(*a) instance_variables_hash.to_json(*a) end # A default to_s implementation. def to_s self.class.to_s end # Returns the instance variables as a hash of key-value pairs. def instance_variables_hash instance_variables.inject({}) do |acc, iv| acc[iv.to_s[1..-1]] = instance_variable_get(iv) acc end end # Called after the configuration is finalized and loaded to validate # this object. # # @param [Machine] machine Access to the machine that is being # validated. # @return [Hash] def validate(machine) return { self.to_s => _detected_errors } end # This returns any automatically detected errors. # # @return [Array] def _detected_errors return [] if !@__invalid_methods || @__invalid_methods.empty? return [I18n.t("vagrant.config.common.bad_field", fields: @__invalid_methods.to_a.sort.join(", "))] end # An internal finalize call that no subclass should override. def _finalize! @__finalized = true end def clean_up_config_object(config) # Remote variables that are internal config.delete_if{|k,_| k.start_with?("_") } config end end end end end ================================================ FILE: lib/vagrant/plugin/v2/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This file contains all the errors that the V2 plugin interface # may throw. module Vagrant module Plugin module V2 # Exceptions that can be thrown within the plugin interface all # inherit from this parent exception. class Error < StandardError; end # This is thrown when a command name given is invalid. class InvalidCommandName < Error; end end end end ================================================ FILE: lib/vagrant/plugin/v2/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V2 # A base class for a guest OS. A guest OS is responsible for detecting # that the guest operating system running within the machine. The guest # can then be extended with various "guest capabilities" which are their # own form of plugin. # # The guest class itself is only responsible for detecting itself, # and may provide helpers for the capabilities. class Guest # This method is called when the machine is booted and has communication # capabilities in order to detect whether this guest operating system # is running within the machine. # # @return [Boolean] def detect?(machine) false end end end end end ================================================ FILE: lib/vagrant/plugin/v2/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V2 # Base class for a host in Vagrant. A host class contains functionality # that is specific to a specific OS that is running Vagrant. This # abstraction is done because there is some host-specific logic that # Vagrant must do in some cases. class Host # This returns true/false depending on if the current running system # matches the host class. # # @return [Boolean] def detect?(env) false end end end end end ================================================ FILE: lib/vagrant/plugin/v2/manager.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Plugin module V2 # This class maintains a list of all the registered plugins as well # as provides methods that allow querying all registered components of # those plugins as a single unit. class Manager attr_reader :registered def initialize @logger = Log4r::Logger.new("vagrant::plugin::v2::manager") @registered = [] end # This returns all the action hooks. # # @return [Array] def action_hooks(hook_name) result = [] @registered.each do |plugin| result += plugin.components.action_hooks[Plugin::ALL_ACTIONS] result += plugin.components.action_hooks[hook_name] end result end # Find all hooks that are applicable for the given key. This # lookup does not include hooks which are defined for ALL_ACTIONS. # Key lookups will match on either string or symbol values. The # provided keys is broken down into multiple parts for lookups, # which allows defining hooks with an entire namespaced name, # or a short suffx. For example: # # Assume we are given an action class # key = Vagrant::Action::Builtin::SyncedFolders # # The list of keys that will be checked for hooks: # ["Vagrant::Action::Builtin::SyncedFolders", "vagrant_action_builtin_synced_folders", # "Action::Builtin::SyncedFolders", "action_builtin_synced_folders", # "Builtin::SyncedFolders", "builtin_synced_folders", # "SyncedFolders", "synced_folders"] # # @param key [Class, String] key Key for hook lookups # @return [Array] def find_action_hooks(key) result = [] generate_hook_keys(key).each do |k| @registered.each do |plugin| result += plugin.components.action_hooks[k] result += plugin.components.action_hooks[k.to_sym] end end result end # Generate all valid lookup keys for given key # # @param [Class, String] key Base key for generation # @return [Array] all valid keys def generate_hook_keys(key) if key.is_a?(Class) key = key.name.to_s else key = key.to_s end parts = key.split("::") [].tap do |keys| until parts.empty? x = parts.join("::") keys << x y = x.gsub(/([a-z])([A-Z])/, '\1_\2').gsub('::', '_').downcase keys << y if x != y parts.shift end end end # This returns all the registered commands. # # @return [Registry>] def commands Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.commands) end end end # This returns all the registered communicators. # # @return [Hash] def communicators Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.communicator) end end end # This returns all the registered configuration classes. # # @return [Hash] def config Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.configs[:top]) end end end # This returns all the registered guests. # # @return [Hash] def guests Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.guests) end end end # This returns all the registered guest capabilities. # # @return [Hash] def guest_capabilities results = Hash.new { |h, k| h[k] = Registry.new } @registered.each do |plugin| plugin.components.guest_capabilities.each do |guest, caps| results[guest].merge!(caps) end end results end # This returns all the registered guests. # # @return [Hash] def hosts Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.hosts) end end end # This returns all the registered host capabilities. # # @return [Hash] def host_capabilities results = Hash.new { |h, k| h[k] = Registry.new } @registered.each do |plugin| plugin.components.host_capabilities.each do |host, caps| results[host].merge!(caps) end end results end # This returns all registered providers. # # @return [Hash] def providers Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.providers) end end end # This returns all the registered provider capabilities. # # @return [Hash] def provider_capabilities results = Hash.new { |h, k| h[k] = Registry.new } @registered.each do |plugin| plugin.components.provider_capabilities.each do |provider, caps| results[provider].merge!(caps) end end results end # This returns all the config classes for the various providers. # # @return [Hash] def provider_configs Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.configs[:provider]) end end end # This returns all the config classes for the various provisioners. # # @return [Registry] def provisioner_configs Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.configs[:provisioner]) end end end # This returns all registered provisioners. # # @return [Hash] def provisioners Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.provisioner) end end end # This returns all registered pushes. # # @return [Registry] def pushes Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.pushes) end end end # This returns all the config classes for the various pushes. # # @return [Registry] def push_configs Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.configs[:push]) end end end # This returns all synced folder implementations. # # @return [Registry] def synced_folders Registry.new.tap do |result| @registered.each do |plugin| result.merge!(plugin.components.synced_folders) end end end # This returns all the registered synced folder capabilities. # # @return [Hash] def synced_folder_capabilities results = Hash.new { |h, k| h[k] = Registry.new } @registered.each do |plugin| plugin.components.synced_folder_capabilities.each do |synced_folder, caps| results[synced_folder].merge!(caps) end end results end # This registers a plugin. This should _NEVER_ be called by the public # and should only be called from within Vagrant. Vagrant will # automatically register V2 plugins when a name is set on the # plugin. def register(plugin) if !@registered.include?(plugin) @logger.info("Registered plugin: #{plugin.name}") @registered << plugin end end # This clears out all the registered plugins. This is only used by # unit tests and should not be called directly. def reset! @registered.clear end # This unregisters a plugin so that its components will no longer # be used. Note that this should only be used for testing purposes. def unregister(plugin) if @registered.include?(plugin) @logger.info("Unregistered: #{plugin.name}") @registered.delete(plugin) end end end end end end ================================================ FILE: lib/vagrant/plugin/v2/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "set" require "log4r" require "vagrant/plugin/v2/components" module Vagrant module Plugin module V2 # This is the superclass for all V2 plugins. class Plugin # Special marker that can be used for action hooks that matches # all action sequences. ALL_ACTIONS = :__all_actions__ # The logger for this class. LOGGER = Log4r::Logger.new("vagrant::plugin::v2::plugin") # Set the root class up to be ourself, so that we can reference this # from within methods which are probably in subclasses. ROOT_CLASS = self # This returns the manager for all V2 plugins. # # @return [V2::Manager] def self.manager @manager ||= local_manager end def self.local_manager @_manager ||= Manager.new end # Returns the {Components} for this plugin. # # @return [Components] def self.components @components ||= Components.new end # Set the name of the plugin. The moment that this is called, the # plugin will be registered and available. Before this is called, a # plugin does not exist. The name must be unique among all installed # plugins. # # @param [String] name Name of the plugin. # @return [String] The name of the plugin. def self.name(name=UNSET_VALUE) # Get or set the value first, so we have a name for logging when # we register. result = get_or_set(:name, name) # The plugin should be registered if we're setting a real name on it Plugin.manager.register(self) if name != UNSET_VALUE # Return the result result end # Sets a human-friendly description of the plugin. # # @param [String] value Description of the plugin. # @return [String] Description of the plugin. def self.description(value=UNSET_VALUE) get_or_set(:description, value) end # Registers a callback to be called when a specific action sequence # is run. This allows plugin authors to hook into things like VM # bootup, VM provisioning, etc. # # @param [String] name Name of the action. # @param [Symbol] hook_name The location to hook. If this isn't # set, every middleware action is hooked. # @return [Array] List of the hooks for the given action. def self.action_hook(name, hook_name=nil, &block) # The name is currently not used but we want it for the future. hook_name = hook_name.to_s if hook_name hook_name ||= ALL_ACTIONS components.action_hooks[hook_name.to_sym] << block end # Defines additional command line commands available by key. The key # becomes the subcommand, so if you register a command "foo" then # "vagrant foo" becomes available. # # @param [String] name Subcommand key. def self.command(name, **opts, &block) # Validate the name of the command if name.to_s !~ /^[-a-z0-9]+$/i raise InvalidCommandName, "Commands can only contain letters, numbers, and hyphens" end # By default, the command is primary opts[:primary] = true if !opts.key?(:primary) # Register the command components.commands.register(name.to_sym) do [block, opts] end nil end # Defines additional communicators to be available. Communicators # should be returned by a block passed to this method. This is done # to ensure that the class is lazy loaded, so if your class inherits # from or uses any Vagrant internals specific to Vagrant 1.0, then # the plugin can still be defined without breaking anything in future # versions of Vagrant. # # @param [String] name Communicator name. def self.communicator(name=UNSET_VALUE, &block) data[:communicator] ||= Registry.new # Register a new communicator class only if a name was given. data[:communicator].register(name.to_sym, &block) if name != UNSET_VALUE # Return the registry data[:communicator] end # Defines additional configuration keys to be available in the # Vagrantfile. The configuration class should be returned by a # block passed to this method. This is done to ensure that the class # is lazy loaded, so if your class inherits from any classes that # are specific to Vagrant 1.0, then the plugin can still be defined # without breaking anything in future versions of Vagrant. # # @param [String] name Configuration key. def self.config(name, scope=nil, &block) scope ||= :top components.configs[scope].register(name.to_sym, &block) nil end # Defines an additionally available guest implementation with # the given key. # # @param [String] name Name of the guest. # @param [String] parent Name of the parent guest (if any) def self.guest(name, parent=nil, &block) components.guests.register(name.to_sym) do parent = parent.to_sym if parent [block.call, parent] end nil end # Defines a capability for the given guest. The block should return # a class/module that has a method with the capability name, ready # to be executed. This means that if it is an instance method, # the block should return an instance of the class. # # @param [String] guest The name of the guest # @param [String] cap The name of the capability def self.guest_capability(guest, cap, &block) components.guest_capabilities[guest.to_sym].register(cap.to_sym, &block) nil end # Defines an additionally available host implementation with # the given key. # # @param [String] name Name of the host. # @param [String] parent Name of the parent host (if any) def self.host(name, parent=nil, &block) components.hosts.register(name.to_sym) do parent = parent.to_sym if parent [block.call, parent] end nil end # Defines a capability for the given host. The block should return # a class/module that has a method with the capability name, ready # to be executed. This means that if it is an instance method, # the block should return an instance of the class. # # @param [String] host The name of the host # @param [String] cap The name of the capability def self.host_capability(host, cap, &block) components.host_capabilities[host.to_sym].register(cap.to_sym, &block) nil end # Registers additional providers to be available. # # @param [Symbol] name Name of the provider. def self.provider(name=UNSET_VALUE, options=nil, &block) options ||= {} options[:priority] ||= 5 components.providers.register(name.to_sym) do [block.call, options] end nil end # Defines a capability for the given provider. The block should return # a class/module that has a method with the capability name, ready # to be executed. This means that if it is an instance method, # the block should return an instance of the class. # # @param [String] provider The name of the provider # @param [String] cap The name of the capability def self.provider_capability(provider, cap, &block) components.provider_capabilities[provider.to_sym].register(cap.to_sym, &block) nil end # Registers additional provisioners to be available. # # @param [String] name Name of the provisioner. def self.provisioner(name=UNSET_VALUE, &block) data[:provisioners] ||= Registry.new # Register a new provisioner class only if a name was given data[:provisioners].register(name.to_sym, &block) if name != UNSET_VALUE # Return the registry data[:provisioners] end # Registers additional pushes to be available. # # @param [String] name Name of the push. # @param [Hash] options List of options for the push. def self.push(name, options=nil, &block) components.pushes.register(name.to_sym) do [block.call, options] end nil end # Registers additional synced folder implementations. # # @param [String] name Name of the implementation. # @param [Integer] priority The priority of the implementation, # higher (big) numbers are tried before lower (small) numbers. def self.synced_folder(name, priority=10, &block) components.synced_folders.register(name.to_sym) do [block.call, priority] end nil end # Defines a capability for the given synced folder. The block should return # a class/module that has a method with the capability name, ready # to be executed. This means that if it is an instance method, # the block should return an instance of the class. # # @param [String] synced_folder The name of the synced folder # @param [String] cap The name of the capability def self.synced_folder_capability(synced_folder, cap, &block) components.synced_folder_capabilities[synced_folder.to_sym].register(cap.to_sym, &block) nil end # Returns the internal data associated with this plugin. This # should NOT be called by the general public. # # @return [Hash] def self.data @data ||= {} end protected # Sentinel value denoting that a value has not been set. UNSET_VALUE = :__UNSET__VALUE__ # Helper method that will set a value if a value is given, or otherwise # return the already set value. # # @param [Symbol] key Key for the data # @param [Object] value Value to store. # @return [Object] Stored value. def self.get_or_set(key, value=UNSET_VALUE) # If no value is to be set, then return the value we have already set return data[key] if value.eql?(UNSET_VALUE) # Otherwise set the value data[key] = value end end end end end ================================================ FILE: lib/vagrant/plugin/v2/provider.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/capability_host" module Vagrant module Plugin module V2 # This is the base class for a provider for the V2 API. A provider # is responsible for creating compute resources to match the needs # of a Vagrant-configured system. class Provider include CapabilityHost # This is called early, before a machine is instantiated, to check # if this provider is usable. This should return true or false. # # If raise_error is true, then instead of returning false, this # should raise an error with a helpful message about why this # provider cannot be used. # # @param [Boolean] raise_error If true, raise exception if not usable. # @return [Boolean] def self.usable?(raise_error=false) # Return true by default for backwards compat since this was # introduced long after providers were being written. 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 # is responsible for. def initialize(machine) end # This should return an action callable for the given name. # # @param [Symbol] name Name of the action. # @return [Object] A callable action sequence object, whether it # is a proc, object, etc. def action(name) nil end # This method is called if the underlying machine ID changes. Providers # can use this method to load in new data for the actual backing # machine or to realize that the machine is now gone (the ID can # become `nil`). No parameters are given, since the underlying machine # is simply the machine instance given to this object. And no # return value is necessary. def machine_id_changed end # This should return a hash of information that explains how to # SSH into the machine. If the machine is not at a point where # SSH is even possible, then `nil` should be returned. # # The general structure of this returned hash should be the # following: # # { # host: "1.2.3.4", # port: "22", # username: "mitchellh", # private_key_path: "/path/to/my/key" # } # # **Note:** Vagrant only supports private key based authentication, # mainly for the reason that there is no easy way to exec into an # `ssh` prompt with a password, whereas we can pass a private key # via commandline. # # @return [Hash] SSH information. For the structure of this hash # read the accompanying documentation for this method. def ssh_info nil end # This should return the state of the machine within this provider. # The state must be an instance of {MachineState}. Please read the # documentation of that class for more information. # # @return [MachineState] def state nil end # This is an internal initialize function that should never be # overridden. It is used to initialize some common internal state # that is used in a provider. def _initialize(name, machine) initialize_capabilities!( name.to_sym, { name.to_sym => [Class.new, nil] }, Vagrant.plugin("2").manager.provider_capabilities, machine, ) end end end end end ================================================ FILE: lib/vagrant/plugin/v2/provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V2 # This is the base class for a provisioner for the V2 API. A provisioner # is primarily responsible for installing software on a Vagrant guest. class Provisioner attr_reader :machine attr_reader :config # Initializes the provisioner with the machine that it will be # provisioning along with the provisioner configuration (if there # is any). # # The provisioner should _not_ do anything at this point except # initialize internal state. # # @param [Machine] machine The machine that this will be provisioning. # @param [Object] config Provisioner configuration, if one was set. def initialize(machine, config) @machine = machine @config = config end # Called with the root configuration of the machine so the provisioner # can add some configuration on top of the machine. # # During this step, and this step only, the provisioner should modify # the root machine configuration to add any additional features it # may need. Examples include sharing folders, networking, and so on. # This step is guaranteed to be called before any of those steps are # done so the provisioner may do that. # # No return value is expected. def configure(root_config) end # This is the method called when the actual provisioning should be # done. The communicator is guaranteed to be ready at this point, # and any shared folders or networks are already setup. # # No return value is expected. def provision end # This is the method called when destroying a machine that allows # for any state related to the machine created by the provisioner # to be cleaned up. def cleanup end end end end end ================================================ FILE: lib/vagrant/plugin/v2/push.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V2 class Push attr_reader :env attr_reader :config # Initializes the pusher with the given environment the push # configuration. # # @param [Environment] env # @param [Object] config Push configuration def initialize(env, config) @env = env @config = config end # This is the method called when the actual pushing should be # done. # # No return value is expected. def push end end end end end ================================================ FILE: lib/vagrant/plugin/v2/synced_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin module V2 # This is the base class for a synced folder implementation. class SyncedFolder class Collection < Hash # @return [Array] names of synced folder types def types keys end # Fetch the synced plugin folder of the given type # # @param [Symbol] t Synced folder type # @return [Vagrant::Plugin::V2::SyncedFolder] def type(t) f = detect { |k, _| k.to_sym == t.to_sym }.last raise KeyError, "Unknown synced folder type" if !f f.values.first[:plugin] end # Converts to a regular Hash and removes # plugin instances so the result is ready # for serialization # # @return [Hash] def to_h c = lambda do |h| h.keys.each do |k| if h[k].is_a?(Hash) h[k] = c.call(h[k].to_h.clone) end end h end h = c.call(super) h.values.each do |f| f.values.each do |g| g.delete(:plugin) end end h end end include CapabilityHost # This is called early when the synced folder is set to determine # if this implementation can be used for this machine. This should # return true or false. # # @param [Machine] machine # @param [Boolean] raise_error If true, should raise an exception # if it isn't usable. # @return [Boolean] def usable?(machine, raise_error=false) end # DEPRECATED: This will be removed. # # @deprecated def prepare(machine, folders, opts) end # This is called after the machine is booted and after networks # are setup. # # This might be called with new folders while the machine is running. # If so, then this should add only those folders without removing # any existing ones. # # No return value. def enable(machine, folders, opts) end # This is called to remove the synced folders from a running # machine. # # This is not guaranteed to be called, but this should be implemented # by every synced folder implementation. # # @param [Machine] machine The machine to modify. # @param [Hash] folders The folders to remove. This will not contain # any folders that should remain. # @param [Hash] opts Any options for the synced folders. def disable(machine, folders, opts) end # This is called after destroying the machine during a # `vagrant destroy` and also prior to syncing folders during # a `vagrant up`. # # No return value. # # @param [Machine] machine # @param [Hash] opts def cleanup(machine, opts) end def _initialize(machine, synced_folder_type) plugins = Vagrant.plugin("2").manager.synced_folders capabilities = Vagrant.plugin("2").manager.synced_folder_capabilities initialize_capabilities!(synced_folder_type, plugins, capabilities, machine) self end end end end end ================================================ FILE: lib/vagrant/plugin/v2/trigger.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'fileutils' require 'log4r' require 'shellwords' require Vagrant.source_root.join("plugins/provisioners/shell/provisioner") require "vagrant/util/subprocess" require "vagrant/util/platform" require "vagrant/util/powershell" module Vagrant module Plugin module V2 class Trigger # @return [Kernel_V2::Config::Trigger] attr_reader :config # This class is responsible for setting up basic triggers that were # defined inside a Vagrantfile. # # @param [Vagrant::Environment] env Vagrant environment # @param [Kernel_V2::TriggerConfig] config Trigger configuration # @param [Vagrant::Machine] machine Active Machine # @param [Vagrant::UI] ui Class for printing messages to user def initialize(env, config, machine, ui) @env = env @config = config @machine = machine @ui = ui @logger = Log4r::Logger.new("vagrant::trigger::#{self.class.to_s.downcase}") end # Fires all triggers, if any are defined for the named type and guest. Returns early # and logs a warning if the community plugin `vagrant-triggers` is installed # # @param [Symbol] name Name of `type` thing to fire trigger on # @param [Symbol] stage :before or :after # @param [String] guest The guest that invoked firing the triggers # @param [Symbol] type Type of trigger to fire (:action, :hook, :command) def fire(name, stage, guest, type, all: false) if community_plugin_detected? @logger.warn("Community plugin `vagrant-triggers detected, so core triggers will not fire") return end return @logger.warn("Name given is nil, no triggers will fire") if !name return @logger.warn("Name given cannot be symbolized, no triggers will fire") if !name.respond_to?(:to_sym) name = name.to_sym # get all triggers matching action triggers = find(name, stage, guest, type, all: all) if !triggers.empty? @logger.info("Firing trigger for #{type} #{name} on guest #{guest}") @ui.info(I18n.t("vagrant.trigger.start", type: type, stage: stage, name: name)) execute(triggers) end end # Find all triggers defined for the named type and guest. # # @param [Symbol] name Name of `type` thing to fire trigger on # @param [Symbol] stage :before or :after # @param [String] guest The guest that invoked firing the triggers # @param [Symbol] type Type of trigger to fire # @return [Array] def find(name, stage, guest, type, all: false) triggers = nil name = nameify(name) if stage == :before triggers = config.before_triggers.select do |t| (all && t.command.respond_to?(:to_sym) && t.command.to_sym == :all && !t.ignore.include?(name.to_sym)) || (type == :hook && matched_hook?(t.command, name)) || nameify(t.command) == name end elsif stage == :after triggers = config.after_triggers.select do |t| (all && t.command.respond_to?(:to_sym) && t.command.to_sym == :all && !t.ignore.include?(name.to_sym)) || (type == :hook && matched_hook?(t.command, name)) || nameify(t.command) == name end else raise Errors::TriggersNoStageGiven, name: name, stage: stage, type: type, guest_name: guest end filter_triggers(triggers, guest, type) end protected # Convert object into name # # @param [Object, Class] object Object to name # @return [String] def nameify(object) if object.is_a?(Class) object.name.to_s else object.to_s end end #------------------------------------------------------------------- # Internal methods, don't call these. #------------------------------------------------------------------- # Generate all valid lookup keys for given action key # # @param [Class, String] key Base key for generation # @return [Array] all valid keys def matched_hook?(key, subject) subject = nameify(subject) Vagrant.plugin("2").manager.generate_hook_keys(key).any? do |k| k == subject end end # Looks up if the community plugin `vagrant-triggers` is installed # and also caches the result # # @return [Boolean] def community_plugin_detected? if !defined?(@_triggers_enabled) plugins = Vagrant::Plugin::Manager.instance.installed_plugins @_triggers_enabled = plugins.keys.include?("vagrant-triggers") end @_triggers_enabled end # Filters triggers to be fired based on configured restraints # # @param [Array] triggers An array of triggers to be filtered # @param [String] guest_name The name of the current guest # @param [Symbol] type The type of trigger (:command or :type) # @return [Array] The filtered array of triggers def filter_triggers(triggers, guest_name, type) # look for only_on trigger constraint and if it doesn't match guest # name, throw it away also be sure to preserve order filter = triggers.dup filter.each do |trigger| index = nil match = false if trigger.only_on trigger.only_on.each do |o| if o.match(guest_name.to_s) # trigger matches on current guest, so we're fine to use it match = true break end end # no matches found, so don't use trigger for guest index = triggers.index(trigger) unless match == true end if trigger.type != type index = triggers.index(trigger) end if index @logger.debug("Trigger #{trigger.id} will be ignored for #{guest_name}") triggers.delete_at(index) end end return triggers end # Execute all triggers in the given array # # @param [Array] triggers An array of triggers to be fired def execute(triggers) # ensure on_error is respected by exiting or continuing triggers.each do |trigger| @logger.debug("Running trigger #{trigger.id}...") if trigger.name @ui.info(I18n.t("vagrant.trigger.fire_with_name", name: trigger.name)) else @ui.info(I18n.t("vagrant.trigger.fire")) end if trigger.info info(trigger.info) end if trigger.warn warn(trigger.warn) end if trigger.abort trigger_abort(trigger.abort) end if trigger.run run(trigger.run, trigger.on_error, trigger.exit_codes) end if trigger.run_remote run_remote(trigger.run_remote, trigger.on_error, trigger.exit_codes) end if trigger.ruby_block execute_ruby(trigger.ruby_block) end end end # Prints the given message at info level for a trigger # # @param [String] message The string to be printed def info(message) @ui.info(message) end # Prints the given message at warn level for a trigger # # @param [String] message The string to be printed def warn(message) @ui.warn(message) end # Runs a script on a guest # # @param [Provisioners::Shell::Config] config A Shell provisioner config def run(config, on_error, exit_codes) if config.inline if Vagrant::Util::Platform.windows? cmd = config.inline else cmd = Shellwords.split(config.inline) end @ui.detail(I18n.t("vagrant.trigger.run.inline", command: config.inline)) else cmd = File.expand_path(config.path, @env.root_path).shellescape args = Array(config.args) cmd << " #{args.join(' ')}" if !args.empty? cmd = Shellwords.split(cmd) @ui.detail(I18n.t("vagrant.trigger.run.script", path: config.path)) end # Pick an execution method to run the script or inline string with # Default to Subprocess::Execute exec_method = Vagrant::Util::Subprocess.method(:execute) if Vagrant::Util::Platform.windows? if config.inline exec_method = Vagrant::Util::PowerShell.method(:execute_inline) else exec_method = Vagrant::Util::PowerShell.method(:execute) end end begin result = exec_method.call(*cmd, :notify => [:stdout, :stderr]) do |type,data| options = {} case type when :stdout options[:color] = :green if !config.keep_color when :stderr options[:color] = :red if !config.keep_color end @ui.detail(data, **options) end if !exit_codes.include?(result.exit_code) raise Errors::TriggersBadExitCodes, code: result.exit_code end rescue => e @ui.error(I18n.t("vagrant.errors.triggers_run_fail")) @ui.error(e.message) if on_error == :halt @logger.debug("Trigger run encountered an error. Halting on error...") raise e else @logger.debug("Trigger run encountered an error. Continuing on anyway...") @ui.warn(I18n.t("vagrant.trigger.on_error_continue")) end end end # Runs a script on the guest # # @param [ShellProvisioner/Config] config A Shell provisioner config def run_remote(config, on_error, exit_codes) if !@machine # machine doesn't even exist. if on_error == :halt raise Errors::TriggersGuestNotExist else @ui.warn(I18n.t("vagrant.errors.triggers_guest_not_exist")) @ui.warn(I18n.t("vagrant.trigger.on_error_continue")) return end elsif @machine.state.id != :running if on_error == :halt raise Errors::TriggersGuestNotRunning, machine_name: @machine.name, state: @machine.state.id else @machine.ui.error(I18n.t("vagrant.errors.triggers_guest_not_running", machine_name: @machine.name, state: @machine.state.id)) @machine.ui.warn(I18n.t("vagrant.trigger.on_error_continue")) return end end prov = VagrantPlugins::Shell::Provisioner.new(@machine, config) begin prov.provision rescue => e @machine.ui.error(I18n.t("vagrant.errors.triggers_run_fail")) if on_error == :halt @logger.debug("Trigger run encountered an error. Halting on error...") raise e else @logger.debug("Trigger run encountered an error. Continuing on anyway...") @machine.ui.error(e.message) end end end # Exits Vagrant immediately # # @param [Integer] code Code to exit Vagrant on def trigger_abort(exit_code) if Thread.current[:batch_parallel_action] @ui.warn(I18n.t("vagrant.trigger.abort_threaded")) @logger.debug("Trigger abort within parallel batch action. " \ "Setting exit code and terminating.") Thread.current[:exit_code] = exit_code Thread.current.terminate else @ui.warn(I18n.t("vagrant.trigger.abort")) @logger.debug("Trigger abort within non-parallel action, exiting directly") Process.exit!(exit_code) end end # Calls the given ruby block for execution # # @param [Proc] ruby_block def execute_ruby(ruby_block) ruby_block.call(@env, @machine) end end end end end ================================================ FILE: lib/vagrant/plugin/v2.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" # We don't autoload components because if we're loading anything in the # V2 namespace anyways, then we're going to need the Components class. require "vagrant/plugin/v2/components" require "vagrant/plugin/v2/errors" module Vagrant module Plugin module V2 autoload :Command, "vagrant/plugin/v2/command" autoload :Communicator, "vagrant/plugin/v2/communicator" autoload :Components, "vagrant/plugin/v2/components" autoload :Config, "vagrant/plugin/v2/config" autoload :Guest, "vagrant/plugin/v2/guest" autoload :Host, "vagrant/plugin/v2/host" autoload :Manager, "vagrant/plugin/v2/manager" autoload :Plugin, "vagrant/plugin/v2/plugin" autoload :Provider, "vagrant/plugin/v2/provider" autoload :Push, "vagrant/plugin/v2/push" autoload :Provisioner, "vagrant/plugin/v2/provisioner" autoload :SyncedFolder, "vagrant/plugin/v2/synced_folder" autoload :Trigger, "vagrant/plugin/v2/trigger" # Errors autoload :Error, "vagrant/plugin/v2/error" autoload :InvalidCommandName, "vagrant/plugin/v2/error" end end end ================================================ FILE: lib/vagrant/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Plugin autoload :V1, "vagrant/plugin/v1" autoload :V2, "vagrant/plugin/v2" autoload :Manager, "vagrant/plugin/manager" autoload :StateFile, "vagrant/plugin/state_file" end end ================================================ FILE: lib/vagrant/registry.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant # Register components in a single location that can be queried. # # This allows certain components (such as guest systems, configuration # pieces, etc.) to be registered and queried, lazily. class Registry def initialize @items = {} @results_cache = {} end # Register a key with a lazy-loaded value. # # If a key with the given name already exists, it is overwritten. def register(key, &block) raise ArgumentError, "block required" if !block_given? @items[key] = block end # Get a value by the given key. # # This will evaluate the block given to `register` and return the # resulting value. def get(key) return nil if !@items.key?(key) return @results_cache[key] if @results_cache.key?(key) @results_cache[key] = @items[key].call end alias :[] :get # Checks if the given key is registered with the registry. # # @return [Boolean] def key?(key) @items.key?(key) end alias_method :has_key?, :key? # Returns an array populated with the keys of this object. # # @return [Array] def keys @items.keys end # Iterate over the keyspace. def each(&block) @items.each do |key, _| yield key, get(key) end end # Iterate over the keyspace and return result # # @return [Array] def map(&block) @items.map do |key, _| yield key, get(key) end end # Return the number of elements in this registry. # # @return [Integer] def length @items.keys.length end alias_method :size, :length # Checks if this registry has any items. # # @return [Boolean] def empty? @items.keys.empty? end # Merge one registry with another and return a completely new # registry. Note that the result cache is completely busted, so # any gets on the new registry will result in a cache miss. def merge(other) self.class.new.tap do |result| result.merge!(self) result.merge!(other) end end # Like #{merge} but merges into self. def merge!(other) @items.merge!(other.__internal_state[:items]) self end # Converts this registry to a hash def to_hash result = {} self.each do |key, value| result[key] = value end result end def __internal_state { items: @items, results_cache: @results_cache } end end end ================================================ FILE: lib/vagrant/shared_helpers.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tempfile" require "thread" module Vagrant @@global_lock = Mutex.new # This is the default endpoint of the Vagrant Cloud in # use. API calls will be made to this for various functions # of Vagrant that may require remote access. # # @return [String] DEFAULT_SERVER_URL = "https://vagrantcloud.com".freeze # Max number of seconds to wait for joining an active thread. # # @return [Integer] # @note This is not the maximum time for a thread to complete. THREAD_MAX_JOIN_TIMEOUT = 60 # List of required external tools that are expected to be # present when running outside of the installers REQUIRED_EXTERNAL_TOOLS = ["bsdtar", "curl", "ssh"].map(&:freeze).freeze # This holds a global lock for the duration of the block. This should # be invoked around anything that is modifying process state (such as # environmental variables). def self.global_lock @@global_lock.synchronize do return yield end end # This returns a true/false showing whether we're running from the # environment setup by the Vagrant installers. # # @return [Boolean] def self.in_installer? !!ENV["VAGRANT_INSTALLER_ENV"] end # This returns a true/false if we are running within a bundler environment # # @return [Boolean] def self.in_bundler? !!ENV["BUNDLE_GEMFILE"] && !defined?(::Bundler).nil? end # Returns the path to the embedded directory of the Vagrant installer, # if there is one (if we're running in an installer). # # @return [String] def self.installer_embedded_dir return nil if !Vagrant.in_installer? ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"] end # Should the plugin system be initialized # # @return [Boolean] def self.plugins_init? !ENV['VAGRANT_DISABLE_PLUGIN_INIT'] end # This returns whether or not 3rd party plugins should and can be loaded. # # @return [Boolean] def self.plugins_enabled? !ENV["VAGRANT_NO_PLUGINS"] end # Whether or not super quiet mode is enabled. This is ill-advised. # # @return [Boolean] def self.very_quiet? !!ENV["VAGRANT_I_KNOW_WHAT_IM_DOING_PLEASE_BE_QUIET"] end # The current log level for Vagrant # # @return [String] def self.log_level ENV.fetch("VAGRANT_LOG", "fatal").downcase end # Returns the URL prefix to the server. # # @return [String] def self.server_url(config_server_url=nil) result = ENV["VAGRANT_SERVER_URL"] result = config_server_url if result == "" or result == nil result || DEFAULT_SERVER_URL end # The source root is the path to the root directory of the Vagrant source. # # @return [Pathname] def self.source_root @source_root ||= Pathname.new(File.expand_path('../../../', __FILE__)) end # This returns the path to the ~/.vagrant.d folder where Vagrant's # per-user state is stored. # # @return [Pathname] def self.user_data_path # Use user specified env var if available path = ENV["VAGRANT_HOME"] # On Windows, we default to the USERPROFILE directory if it # is available. This is more compatible with Cygwin and sharing # the home directory across shells. if !path && ENV["USERPROFILE"] path = "#{ENV["USERPROFILE"]}/.vagrant.d" end # Fallback to the default path ||= "~/.vagrant.d" Pathname.new(path).expand_path end # This returns true/false if the running version of Vagrant is # a pre-release version (development) # # @return [Boolean] def self.prerelease? Gem::Version.new(Vagrant::VERSION).prerelease? end # This returns true/false if the Vagrant should allow prerelease # versions when resolving plugin dependency constraints # # @return [Boolean] def self.allow_prerelease_dependencies? !!ENV["VAGRANT_ALLOW_PRERELEASE"] end # This allows control over dependency resolution when installing # plugins into vagrant. When true, dependency libraries that Vagrant # core relies upon will be hard constraints. # # @return [Boolean] def self.strict_dependency_enforcement if ENV["VAGRANT_DISABLE_STRICT_DEPENDENCY_ENFORCEMENT"] false else true end end # Automatically install locally defined plugins instead of # waiting for user confirmation. # # @return [Boolean] def self.auto_install_local_plugins? if ENV["VAGRANT_INSTALL_LOCAL_PLUGINS"] true else false end end # Use Ruby Resolv in place of libc # # @return [boolean] enabled or not def self.enable_resolv_replace if ENV["VAGRANT_ENABLE_RESOLV_REPLACE"] if !ENV["VAGRANT_DISABLE_RESOLV_REPLACE"] begin require "resolv-replace" true rescue false end else false end end end # Set the global logger # # @param log Logger # @return [Logger] def self.global_logger=(log) @_global_logger = log end # Get the global logger instance # # @return [Logger] def self.global_logger if @_global_logger.nil? require "log4r" @_global_logger = Log4r::Logger.new("vagrant::global") end @_global_logger end # Add a new block of default CLI options which # should be automatically added to all commands # # @param [Proc] block Proc instance containing OptParser configuration # @return [nil] def self.add_default_cli_options(block) if !block.is_a?(Proc) raise TypeError, "Expecting type `Proc` but received `#{block.class}`" end if block.arity != 1 && block.arity != -1 raise ArgumentError, "Proc must accept OptionParser argument" end @_default_cli_options = [] if !@_default_cli_options @_default_cli_options << block nil end # Array of default CLI options to automatically # add to commands. # # @return [Array] Default optparse options def self.default_cli_options @_default_cli_options = [] if !@_default_cli_options @_default_cli_options.dup end def self.detect_missing_tools REQUIRED_EXTERNAL_TOOLS.find_all do |tool| !Vagrant::Util::Which.which(tool) end end end ================================================ FILE: lib/vagrant/ui.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "delegate" require "io/console" require "thread" require "log4r" require "vagrant/util/platform" require "vagrant/util/safe_puts" module Vagrant module UI # Vagrant UIs handle communication with the outside world (typically # through a shell). They must respond to the following methods: # # * `info` # * `warn` # * `error` # * `success` class Interface # Opts can be used to set some options. These options are implementation # specific. See the implementation for more docs. attr_accessor :opts # @return [IO] UI input. Defaults to `$stdin`. attr_accessor :stdin # @return [IO] UI output. Defaults to `$stdout`. attr_accessor :stdout # @return [IO] UI error output. Defaults to `$stderr`. attr_accessor :stderr def initialize @logger = Log4r::Logger.new("vagrant::ui::interface") @opts = {} @stdin = $stdin @stdout = $stdout @stderr = $stderr end def initialize_copy(original) super @opts = original.opts.dup end [:ask, :detail, :warn, :error, :info, :output, :success].each do |method| define_method(method) do |message, **opts| # Log normal console messages begin @logger.info { "#{method}: #{message}" } rescue ThreadError # We're being called in a trap-context. Wrap in a thread. Thread.new do @logger.info { "#{method}: #{message}" } end.join(THREAD_MAX_JOIN_TIMEOUT) end end end [:clear_line, :report_progress].each do |method| # By default do nothing, these aren't logged define_method(method) { |*args| } end # @return [false] def color? return false end # For machine-readable output. # # @param [String] type The type of the data # @param [Array] data The data associated with the type def machine(type, *data) @logger.info("Machine: #{type} #{data.inspect}") end # Yields self (UI) # Provides a way for selectively displaying or not displaying # updating content like download progress. def rewriting yield self end def to_proto raise NotImplementedError, "Vagrant::UI::Interface#to_proto" end end # This is a UI implementation that does nothing. class Silent < Interface def ask(*args, **opts) super # Silent can't do this, obviously. raise Errors::UIExpectsTTY end end class MachineReadable < Interface include Util::SafePuts def initialize super @lock = Mutex.new end def ask(*args, **opts) super # Machine-readable can't ask for input 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, **opts) end end def machine(type, *data) opts = {} opts = data.pop if data.last.kind_of?(Hash) target = opts[:target] || "" # Prepare the data by replacing characters that aren't outputted data.each_index do |i| data[i] = data[i].to_s.dup data[i].gsub!(",", "%!(VAGRANT_COMMA)") data[i].gsub!("\n", "\\n") data[i].gsub!("\r", "\\r") end # Avoid locks in a trap context introduced from Ruby 2.0 Thread.new do @lock.synchronize do safe_puts("#{Time.now.utc.to_i},#{target},#{type},#{data.join(",")}") end end.join(THREAD_MAX_JOIN_TIMEOUT) end end # This is a UI implementation that outputs the text as is. It # doesn't add any color. class Basic < Interface include Util::SafePuts def initialize super @lock = Mutex.new end # Use some light meta-programming to create the various methods to # output text to the UI. These all delegate the real functionality # to `say`. [:detail, :info, :warn, :error, :output, :success].each do |method| class_eval <<-CODE def #{method}(message, **opts) super(message) say(#{method.inspect}, message, **opts) end CODE end def ask(message, **opts) super(message) # We can't ask questions when the output isn't a TTY. raise Errors::UIExpectsTTY if !@stdin.tty? && !Vagrant::Util::Platform.windows? # Setup the options so that the new line is suppressed opts ||= {} opts[:echo] = true if !opts.key?(:echo) opts[:new_line] = false if !opts.key?(:new_line) opts[:prefix] = false if !opts.key?(:prefix) # Output the data say(:info, message, opts) input = nil if opts[:echo] || !@stdin.respond_to?(:noecho) input = @stdin.gets else begin input = @stdin.noecho(&:gets) # Output a newline because without echo, the newline isn't # echoed either. say(:info, "\n", opts) rescue Errno::EBADF # This means that stdin doesn't support echoless input. say(:info, "\n#{I18n.t("vagrant.stdin_cant_hide_input")}\n ", opts) # Ask again, with echo enabled input = ask(message, **opts.merge(echo: true)) end end # Get the results and chomp off the newline. We do a logical OR # here because `gets` can return a nil, for example in the case # that ctrl-D is pressed on the input. (input || "").chomp end # This is used to output progress reports to the UI. # Send this method progress/total and it will output it # to the UI. Send `clear_line` to clear the line to show # a continuous progress meter. def report_progress(progress, total, show_parts=true) if total && total > 0 percent = (progress.to_f / total.to_f) * 100 line = "Progress: #{percent.to_i}%" line << " (#{progress} / #{total})" if show_parts else line = "Progress: #{progress}" end info(line, new_line: false) end def clear_line # See: https://en.wikipedia.org/wiki/ANSI_escape_code reset = "\r\033[K" info(reset, new_line: false) end # This method handles actually outputting a message of a given type # to the console. def say(type, message, opts={}) defaults = { new_line: true, prefix: true } opts = defaults.merge(@opts).merge(opts) # Don't output if we're hiding details return if type == :detail && opts[:hide_detail] # Determine whether we're expecting to output our # own new line or not. printer = opts[:new_line] ? :puts : :print # Determine the proper IO channel to send this message # to based on the type of the message channel = type == :error || opts[:channel] == :error ? @stderr : @stdout # Output! We wrap this in a lock so that it safely outputs only # one line at a time. We wrap this in a thread because as of Ruby 2.0 # we can't acquire locks in a trap context (ctrl-c), so we have to # do this. Thread.new do @lock.synchronize do safe_puts(format_message(type, message, **opts), io: channel, printer: printer) end end.join(THREAD_MAX_JOIN_TIMEOUT) end def format_message(type, message, **opts) Util::CredentialScrubber.desensitize(message) end end class NonInteractive < Basic def initialize super end def rewriting # no-op end def report_progress(progress, total, show_parts=true) # no-op end def clear_line @logger.warn("Using `clear line` in a non interactive ui") say(:info, "\n", opts) end def ask(*args, **opts) # Non interactive can't ask for input raise Errors::UIExpectsTTY end end # Prefixed wraps an existing UI and adds a prefix to it. class Prefixed < Interface # The prefix for `output` messages. OUTPUT_PREFIX = "==> " def initialize(ui, prefix) super() @prefix = prefix @ui = ui end def to_proto @ui.to_proto end def client @ui.client end def initialize_copy(original) super @ui = original.instance_variable_get(:@ui).dup end # Use some light meta-programming to create the various methods to # output text to the UI. These all delegate the real functionality # to `say`. [:ask, :detail, :info, :warn, :error, :output, :success].each do |method| class_eval <<-CODE def #{method}(message, **opts) super(message) if !@ui.opts.key?(:bold) && !opts.key?(:bold) opts[:bold] = #{method.inspect} != :detail && \ #{method.inspect} != :ask end if !opts.key?(:target) opts[:target] = @prefix end @ui.#{method}(format_message(#{method.inspect}, message, **opts), **opts) end CODE end [:clear_line, :report_progress].each do |method| # By default do nothing, these aren't formatted define_method(method) do |*args| @ui.send(method, *args) end end # For machine-readable output, set the prefix in the # options hash and continue it on. def machine(type, *data) opts = {} opts = data.pop if data.last.is_a?(Hash) opts[:target] = @prefix data << opts @ui.machine(type, *data) end # Return the parent's opts. # # @return [Hash] def opts @ui.opts end def format_message(type, message, **opts) opts = self.opts.merge(opts) prefix = "" if !opts.key?(:prefix) || opts[:prefix] prefix = OUTPUT_PREFIX prefix = " " * OUTPUT_PREFIX.length if \ type == :detail || type == :ask || opts[:prefix_spaces] end message = Util::CredentialScrubber.desensitize(message) # Fast-path if there is no prefix return message if prefix.empty? target = @prefix target = opts[:target] if opts.key?(:target) target = "#{target}:" if target != "" lines = [message] if message != "" lines = [].tap do |l| message.scan(/(.*?)(\n|$)/).each do |m| l << m.first if m.first != "" || (m.first == "" && m.last == "\n") end end lines << "" if message.end_with?("\n") end # Otherwise, make sure to prefix every line properly lines.map do |line| "#{prefix}#{target} #{line}" end.join("\n") end def rewriting @ui.rewriting do |ui| yield ui end end end # This is a UI implementation that outputs color for various types # of messages. This should only be used with a TTY that supports color, # but is up to the user of the class to verify this is the case. class Colored < Basic # Terminal colors COLORS = { red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37, } # @return [true] def color? return true end # This is called by `say` to format the message for output. def format_message(type, message, **opts) # Get the format of the message before adding color. message = super opts = @opts.merge(opts) # Special case some colors for certain message types opts[:color] = :red if type == :error opts[:color] = :green if type == :success opts[:color] = :yellow if type == :warn # If it is a detail, it is not bold. Every other message type # is bolded. bold = !!opts[:bold] colorseq = "#{bold ? 1 : 0 }" if opts[:color] && opts[:color] != :default color = COLORS[opts[:color]] colorseq += ";#{color}" end # Color the message and make sure to reset the color at the end "\033[#{colorseq}m#{message}\033[0m" end end end end ================================================ FILE: lib/vagrant/util/ansi_escape_code_remover.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util module ANSIEscapeCodeRemover # Removes ANSI escape code sequences from the text and returns # it. # # This removes all the ANSI escape codes listed here along with # the escape codes for VT100 terminals: # # http://ascii-table.com/ansi-escape-sequences.php def remove_ansi_escape_codes(text) # An array of regular expressions which match various kinds # of escape sequences. I can't think of a better single regular # expression or any faster way to do this. matchers = [/\e\[\d*[ABCD]/, # Matches things like \e[4D /\e\[(\d*;)?\d*[HF]/, # Matches \e[1;2H or \e[H /\e\[(s|u|2J|K)/, # Matches \e[s, \e[2J, etc. /\e\[=\d*[hl]/, # Matches \e[=24h /\e\[\?[1-9][hl]/, # Matches \e[?2h /\e\[20[hl]/, # Matches \e[20l] /\e[DME78H]/, # Matches \eD, \eH, etc. /\e\[[0-3]?[JK]/, # Matches \e[0J, \e[K, etc. ] # Take each matcher and replace it with emptiness. matchers.each do |matcher| text.gsub!(matcher, "") end text end end end end ================================================ FILE: lib/vagrant/util/busy.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # Utility class which allows blocks of code to be marked as "busy" # with a specified interrupt handler. During busy areas of code, it # is often undesirable for SIGINTs to immediately kill the application. # This class is a helper to cleanly register callbacks to handle this # situation. class Busy @@registered = [] @@mutex = Mutex.new class << self # Mark a given block of code as a "busy" block of code, which will # register a SIGINT handler for the duration of the block. When a # SIGINT occurs, the `sig_callback` proc will be called. It is up # to the callback to behave properly and exit the application. def busy(sig_callback) register(sig_callback) return yield ensure unregister(sig_callback) end # Registers a SIGINT handler. This typically is called from {busy}. # Callbacks are only registered once, so calling this multiple times # with the same callback has no consequence. def register(sig_callback) @@mutex.synchronize do registered << sig_callback registered.uniq! # Register the handler if this is our first callback. Signal.trap("INT") { fire_callbacks } if registered.length == 1 end end # Unregisters a SIGINT handler. def unregister(sig_callback) @@mutex.synchronize do registered.delete(sig_callback) # Remove the signal trap if no more registered callbacks exist Signal.trap("INT", "DEFAULT") if registered.empty? end end # Fires all the registered callbacks. def fire_callbacks registered.reverse.each { |r| r.call } end # Helper method to get access to the class variable. This is mostly # exposed for tests. This shouldn't be mucked with directly, since it's # structure may change at any time. def registered; @@registered; end end end end end ================================================ FILE: lib/vagrant/util/caps.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "fileutils" require "pathname" require "vagrant/util/directory" require "vagrant/util/subprocess" module Vagrant module Util module Caps module BuildISO # Builds an iso given a compatible iso_command # # @param [List] command to build iso # @param [Pathname] input directory for iso build # @param [Pathname] output file for iso build def build_iso(iso_command, source_directory, file_destination) FileUtils.mkdir_p(file_destination.dirname) if !file_destination.exist? || Vagrant::Util::Directory.directory_changed?(source_directory, file_destination.mtime) result = Vagrant::Util::Subprocess.execute(*iso_command) if result.exit_code != 0 raise Vagrant::Errors::ISOBuildFailed, cmd: iso_command.join(" "), stdout: result.stdout, stderr: result.stderr end end end protected def ensure_output_iso(file_destination) if file_destination.nil? tmpfile = Tempfile.new(["vagrant", ".iso"]) file_destination = Pathname.new(tmpfile.path) tmpfile.close tmpfile.unlink else file_destination = Pathname.new(file_destination.to_s) # If the file destination path is a folder, target the output to a randomly named # file in that dir if file_destination.extname != ".iso" file_destination = file_destination.join("#{SecureRandom.hex(3)}_vagrant.iso") end end file_destination end end end end end ================================================ FILE: lib/vagrant/util/checkpoint_client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "singleton" module Vagrant module Util class CheckpointClient include Singleton # Maximum number of seconds to wait for check to complete CHECKPOINT_TIMEOUT = 10 # @return [Log4r::Logger] attr_reader :logger # @return [Boolean] attr_reader :enabled # @return [Hash] attr_reader :files # @return [Vagrant::Environment] attr_reader :env def initialize @logger = Log4r::Logger.new("vagrant::checkpoint_client") @enabled = false end # Setup will attempt to load the checkpoint library and define # required paths # # @param [Vagrant::Environment] env # @return [self] def setup(env) begin require "checkpoint" @enabled = true rescue LoadError @logger.warn("checkpoint library not found. disabling.") end if ENV["VAGRANT_CHECKPOINT_DISABLE"] @logger.debug("checkpoint disabled via explicit user request") @enabled = false end @files = { signature: env.data_dir.join("checkpoint_signature"), cache: env.data_dir.join("checkpoint_cache") } @checkpoint_thread = nil @env = env self end # Check has completed def complete? !@checkpoint_thread.nil? && !@checkpoint_thread.alive? end # Result of check # # @return [Hash, nil] def result if !enabled || @checkpoint_thread.nil? nil elsif !defined?(@result) @checkpoint_thread.join(CHECKPOINT_TIMEOUT) @result = @checkpoint_thread[:result] else @result end end # Run check # # @return [self] def check if enabled && @checkpoint_thread.nil? logger.debug("starting plugin check") @checkpoint_thread = Thread.new do Thread.current.abort_on_exception = false if Thread.current.respond_to?(:report_on_exception=) Thread.current.report_on_exception = false end begin Thread.current[:result] = Checkpoint.check( product: "vagrant", version: VERSION, signature_file: files[:signature], cache_file: files[:cache] ) if !Thread.current[:result].is_a?(Hash) Thread.current[:result] = nil end logger.debug("plugin check complete") rescue => e logger.debug("plugin check failure - #{e}") end end end self end # Display any alerts or version update information # # @return [boolean] true if displayed, false if not def display if !defined?(@displayed) if !complete? @logger.debug("waiting for checkpoint to complete...") end # Don't display if information is cached if result && !result["cached"] version_check alerts_check else @logger.debug("no information received from checkpoint") end @displayed = true else false end end def alerts_check if result["alerts"] && !result["alerts"].empty? result["alerts"].group_by{|a| a["level"]}.each_pair do |_, alerts| alerts.each do |alert| date = nil begin date = Time.at(alert["date"]) rescue date = Time.now end output = I18n.t("vagrant.alert", message: alert["message"], date: date, url: alert["url"] ) case alert["level"] when "info" alert_ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant") alert_ui.info(output) when "warn" alert_ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant-warning") alert_ui.warn(output) when "critical" alert_ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant-alert") alert_ui.error(output) end end env.ui.info("") end else @logger.debug("no alert notifications to display") end end def version_check latest_version = Gem::Version.new(result["current_version"]) installed_version = Gem::Version.new(VERSION) ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant") if latest_version > installed_version @logger.info("new version of Vagrant available - #{latest_version}") ui.info(I18n.t("vagrant.version_upgrade_available", latest_version: latest_version, installed_version: installed_version), channel: :error) env.ui.info("", channel: :error) else @logger.debug("vagrant is currently up to date") end end # @private # Reset the cached values for platform. This is not considered a public # API and should only be used for testing. def reset! logger = @logger instance_variables.each(&method(:remove_instance_variable)) @logger = logger @enabled = false end end end end ================================================ FILE: lib/vagrant/util/command_deprecation.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # Automatically add deprecation notices to commands module CommandDeprecation # @return [String] generated name of command def deprecation_command_name name_parts = self.class.name.split("::") [ name_parts[1].sub('Command', ''), name_parts[3] ].compact.map(&:downcase).join(" ") end def self.included(klass) klass.class_eval do class << self if method_defined?(:synopsis) alias_method :non_deprecated_synopsis, :synopsis def synopsis if !non_deprecated_synopsis.to_s.empty? "#{non_deprecated_synopsis} [DEPRECATED]" else non_deprecated_synopsis end end end end alias_method :non_deprecated_execute, :execute def execute(*args, &block) @env[:ui].warn(I18n.t("vagrant.commands.deprecated", name: deprecation_command_name ) + "\n") non_deprecated_execute(*args, &block) end end end # Mark command deprecation complete and fully disable # the command's functionality module Complete def self.included(klass) klass.include(CommandDeprecation) klass.class_eval do def execute(*_) raise Vagrant::Errors::CommandDeprecated, name: deprecation_command_name end end end end end end end ================================================ FILE: lib/vagrant/util/counter.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'thread' module Vagrant module Util # Atomic counter implementation. This is useful for incrementing # a counter which is guaranteed to only be used once in its class. module Counter def get_and_update_counter(name=nil) name ||= :global mutex.synchronize do @__counter ||= Hash.new(1) result = @__counter[name] @__counter[name] += 1 result end end def mutex @__counter_mutex ||= Mutex.new end end end end ================================================ FILE: lib/vagrant/util/credential_scrubber.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # Utility class to remove credential information from strings class CredentialScrubber # String used to replace credential information REPLACEMENT_TEXT = "*****".freeze # Attempt to remove detected credentials from string # # @param [String] string # @return [String] def self.scrub(string) string = url_scrubber(string) end # Detect URLs and remove any embedded credentials # # @param [String] string # @return [String] def self.url_scrubber(string) string.gsub(%r{(ftp|https?)://[^\s]+@[^\s]+}) do |address| uri = URI.parse(address) uri.user = uri.password = REPLACEMENT_TEXT uri.to_s end end # Remove sensitive information from string # # @param [String] string # @return [String] def self.desensitize(string) string = string.to_s.dup sensitive_strings.each do |remove| string.gsub!(/(\W|^)#{Regexp.escape(remove)}(\W|$)/, "\\1#{REPLACEMENT_TEXT}\\2") end string end # Register a sensitive string to be scrubbed def self.sensitive(string) string = string.to_s.dup if string.length > 0 sensitive_strings.push(string).uniq! end nil end # Deregister a sensitive string and allow output def self.unsensitive(string) sensitive_strings.delete(string) nil end # @return [Array] def self.sensitive_strings if !defined?(@_sensitive_strings) @_sensitive_strings = [] end @_sensitive_strings end # @private # Reset the cached values for scrubber. This is not considered a public # API and should only be used for testing. def self.reset! instance_variables.each(&method(:remove_instance_variable)) end end end end ================================================ FILE: lib/vagrant/util/curl_helper.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util class CurlHelper # Hosts that do not require notification on redirect SILENCED_HOSTS = [ "vagrantcloud-files-production.s3-accelerate.amazonaws.com".freeze, "vagrantcloud.com".freeze, "vagrantup.com".freeze, "cloud.hashicorp.com".freeze, ].freeze def self.capture_output_proc(logger, ui, source=nil) progress_data = "" progress_regexp = /^\r\s*(\d.+?)\r/m # Setup the proc that'll receive the real-time data from # the downloader. data_proc = Proc.new do |type, data| # Type will always be "stderr" because that is the only # type of data we're subscribed for notifications. # Accumulate progress_data progress_data << data redirect_notify = false while true # If the download has been redirected and we are no longer downloading # from the original host, notify the user that the target host has # changed from the source. if progress_data.include?("Location") location = progress_data.scan(/(^|[^\w-])Location: (.+?)$/m).flatten.compact.last.to_s.strip if !location.empty? location_uri = URI.parse(location) if !location_uri.host.nil? && !redirect_notify logger.info("download redirected to #{location}") source_uri = URI.parse(source) source_host = source_uri.host.to_s.split(".", 2).last location_host = location_uri.host.to_s.split(".", 2).last if location_host != source_host && !SILENCED_HOSTS.include?(location_host) && !SILENCED_HOSTS.include?(location_uri.host.to_s) ui.rewriting do |_ui| _ui.clear_line _ui.detail "Download redirected to host: #{location_uri.host}" end end redirect_notify = true end end progress_data.replace("") break end # If we have a full amount of column data (two "\r") then # we report new progress reports. Otherwise, just keep # accumulating. match = nil check_match = true while check_match check_match = progress_regexp.match(progress_data) if check_match data = check_match[1].to_s stop = progress_data.index(data) + data.length progress_data.slice!(0, stop) match = check_match end end break if !match # Ignore the first \r and split by whitespace to grab the columns columns = data.strip.split(/\s+/) # COLUMN DATA: # # 0 - % total # 1 - Total size # 2 - % received # 3 - Received size # 4 - % transferred # 5 - Transferred size # 6 - Average download speed # 7 - Average upload speed # 9 - Total time # 9 - Time spent # 10 - Time left # 11 - Current speed output = "Progress: #{columns[0]}% (Rate: #{columns[11]}/s, Estimated time remaining: #{columns[10]})" ui.rewriting do |ui| ui.clear_line ui.detail(output, new_line: false) end end end return data_proc end end end end ================================================ FILE: lib/vagrant/util/deep_merge.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util module DeepMerge # This was lifted straight from Rails 4.0.2 as a basic Hash # deep merge. def self.deep_merge(myself, other_hash, &block) myself = myself.dup other_hash.each_pair do |k,v| tv = myself[k] if tv.is_a?(Hash) && v.is_a?(Hash) myself[k] = deep_merge(tv, v, &block) else myself[k] = block && tv ? block.call(k, tv, v) : v end end myself end end end end ================================================ FILE: lib/vagrant/util/directory.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'pathname' module Vagrant module Util class Directory # Check if directory has any new updates # # @param [Pathname, String] Path to directory # @param [Time] time to compare to eg. has any file in dir_path # changed since this time # @return [Boolean] def self.directory_changed?(dir_path, threshold_time) Dir.glob(Pathname.new(dir_path).join("**", "*")).any? do |path| Pathname.new(path).mtime > threshold_time end end end end end ================================================ FILE: lib/vagrant/util/downloader.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "cgi" require "uri" require "log4r" require "digest" require "digest/md5" require "digest/sha1" require "vagrant/util/busy" require "vagrant/util/platform" require "vagrant/util/subprocess" require "vagrant/util/curl_helper" require "vagrant/util/file_checksum" module Vagrant module Util # This class downloads files using various protocols by subprocessing # to cURL. cURL is a much more capable and complete download tool than # a hand-rolled Ruby library, so we defer to its expertise. class Downloader # Custom user agent provided to cURL so that requests to URL shorteners # are properly tracked. # # Vagrant/1.7.4 (+https://www.vagrantup.com; ruby2.1.0) USER_AGENT = "Vagrant/#{VERSION} (+https://www.vagrantup.com; #{RUBY_ENGINE}#{RUBY_VERSION}) #{ENV['VAGRANT_USER_AGENT_PROVISIONAL_STRING']}".strip.freeze attr_accessor :source attr_reader :destination attr_accessor :headers def initialize(source, destination, options=nil) options ||= {} @logger = Log4r::Logger.new("vagrant::util::downloader") @source = source.to_s @destination = destination.to_s begin url = URI.parse(@source) if url.scheme && url.scheme.start_with?("http") && url.user auth = "#{CGI.unescape(url.user)}" auth += ":#{CGI.unescape(url.password)}" if url.password url.user = nil url.password = nil options[:auth] ||= auth @source = url.to_s end rescue URI::InvalidURIError # Ignore, since its clearly not HTTP end # Get the various optional values @auth = options[:auth] @ca_cert = options[:ca_cert] @ca_path = options[:ca_path] @continue = options[:continue] @headers = Array(options[:headers]) @insecure = options[:insecure] @ui = options[:ui] @client_cert = options[:client_cert] @location_trusted = options[:location_trusted] @checksums = { :md5 => options[:md5], :sha1 => options[:sha1], :sha256 => options[:sha256], :sha384 => options[:sha384], :sha512 => options[:sha512] }.compact @extra_download_options = options[:box_extra_download_options] || [] # If on Windows SSL revocation checks should be best effort. More context # for this usage can be found in the following links: # # https://github.com/curl/curl/issues/3727 # https://github.com/curl/curl/pull/4981 if Platform.windows? @ssl_revoke_best_effort = !options[:disable_ssl_revoke_best_effort] end end # This executes the actual download, downloading the source file # to the destination with the given options used to initialize this # class. # # If this method returns without an exception, the download # succeeded. An exception will be raised if the download failed. def download! # This variable can contain the proc that'll be sent to # the subprocess execute. data_proc = nil extra_subprocess_opts = {} if @ui # If we're outputting progress, then setup the subprocess to # tell us output so we can parse it out. extra_subprocess_opts[:notify] = :stderr data_proc = Vagrant::Util::CurlHelper.capture_output_proc(@logger, @ui, @source) end @logger.info("Downloader starting download: ") @logger.info(" -- Source: #{@source}") @logger.info(" -- Destination: #{@destination}") retried = false begin # Get the command line args and the subprocess opts based # on our downloader settings. options, subprocess_options = self.options options += ["--output", @destination] options << @source # Merge in any extra options we set subprocess_options.merge!(extra_subprocess_opts) # Go! execute_curl(options, subprocess_options, &data_proc) rescue Errors::DownloaderError => e # If we already retried, raise it. raise if retried @logger.error("Exit code: #{e.extra_data[:code]}") # If its any error other than 33, it is an error. raise if e.extra_data[:code].to_i != 33 # Exit code 33 means that the server doesn't support ranges. # In this case, try again without resume. @logger.error("Error is server doesn't support byte ranges. Retrying from scratch.") @continue = false retried = true retry ensure # If we're outputting to the UI, clear the output to # avoid lingering progress meters. if @ui @ui.clear_line # Windows doesn't clear properly for some reason, so we just # output one more newline. @ui.detail("") if Platform.windows? end end validate_download!(@source, @destination, @checksums) # Everything succeeded true end # Does a HEAD request of the URL and returns the output. def head options, subprocess_options = self.options options.unshift("-I") options << @source @logger.info("HEAD: #{@source}") result = execute_curl(options, subprocess_options) result.stdout end protected # Apply any checksum validations based on provided # options content # # @param source [String] Source of file # @param path [String, Pathname] local file path # @param checksums [Hash] User provided options # @option checksums [String] :md5 Compare MD5 checksum # @option checksums [String] :sha1 Compare SHA1 checksum # @return [Boolean] def validate_download!(source, path, checksums) checksums.each do |type, expected| actual = FileChecksum.new(path, type).checksum @logger.debug("Validating checksum (#{type}) for #{source}. " \ "expected: #{expected} actual: #{actual}") if actual.casecmp(expected) != 0 raise Errors::DownloaderChecksumError.new( source: source, path: path, type: type, expected_checksum: expected, actual_checksum: actual ) end end true end def execute_curl(options, subprocess_options, &data_proc) options = options.dup options.unshift("-q") options << subprocess_options # Create the callback that is called if we are interrupted interrupted = false int_callback = Proc.new do @logger.info("Downloader interrupted!") interrupted = true end # Execute! result = Busy.busy(int_callback) do Subprocess.execute("curl", *options, &data_proc) end # If the download was interrupted, then raise a specific error raise Errors::DownloaderInterrupted if interrupted # If it didn't exit successfully, we need to parse the data and # show an error message. if result.exit_code != 0 @logger.warn("Downloader exit code: #{result.exit_code}") check = result.stderr.match(/\n*curl:\s+\((?\d+)\)\s*(?.*)$/) if check && check[:code] == "416" # All good actually. 416 means there is no more bytes to download @logger.warn("Downloader got a 416, but is likely fine. Continuing on...") else if !check err_msg = result.stderr else err_msg = check[:error] end raise Errors::DownloaderError, code: result.exit_code, message: err_msg end end result end # Returns the various cURL and subprocess options. # # @return [Array] def options # Build the list of parameters to execute with cURL options = [ "--fail", "--location", "--max-redirs", "10", "--verbose", "--user-agent", USER_AGENT, ] options += ["--cacert", @ca_cert] if @ca_cert options += ["--capath", @ca_path] if @ca_path options += ["--continue-at", "-"] if @continue options << "--insecure" if @insecure options << "--cert" << @client_cert if @client_cert options << "-u" << @auth if @auth options << "--location-trusted" if @location_trusted options << "--ssl-revoke-best-effort" if @ssl_revoke_best_effort options.concat(@extra_download_options) if @headers Array(@headers).each do |header| options << "-H" << header end end # Specify some options for the subprocess subprocess_options = {} # If we're in Vagrant, then we use the packaged CA bundle if Vagrant.in_installer? subprocess_options[:env] ||= {} subprocess_options[:env]["CURL_CA_BUNDLE"] = ENV["CURL_CA_BUNDLE"] end return [options, subprocess_options] end end end end ================================================ FILE: lib/vagrant/util/env.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util class Env def self.with_original_env original_env = ENV.to_hash if defined?(::Bundler) && defined?(::Bundler::ORIGINAL_ENV) ENV.replace(::Bundler::ORIGINAL_ENV) end ENV.update(Vagrant.original_env) yield ensure ENV.replace(original_env.to_hash) end # Execute the given command, removing any Ruby-specific environment # variables. This is an "enhanced" version of `Bundler.with_clean_env`, # which only removes Bundler-specific values. We need to remove all # values, specifically: # # - _ORIGINAL_GEM_PATH # - GEM_PATH # - GEM_HOME # - GEM_ROOT # - BUNDLE_BIN_PATH # - BUNDLE_GEMFILE # - RUBYLIB # - RUBYOPT # - RUBY_ENGINE # - RUBY_ROOT # - RUBY_VERSION # # This will escape Vagrant's environment entirely, which is required if # calling an executable that lives in another Ruby environment. The # original environment restored at the end of this call. # # @param [Proc] block # the block to execute with the cleaned environment def self.with_clean_env with_original_env do if ENV["BUNDLE_ORIG_MANPATH"] ENV["MANPATH"] = ENV["BUNDLE_ORIG_MANPATH"] end ENV.delete_if { |k,_| k[0,7] == "BUNDLE_" } if ENV.has_key? "RUBYOPT" ENV["RUBYOPT"] = ENV["RUBYOPT"].sub("-rbundler/setup", "") ENV["RUBYOPT"] = ENV["RUBYOPT"].sub("-I#{File.expand_path('..', __FILE__)}", "") end yield end end end end end ================================================ FILE: lib/vagrant/util/experimental.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util class Experimental class << self # A method for determining if the experimental flag has been enabled with # any features # # @return [Boolean] def enabled? if !defined?(@_experimental) experimental = features_requested if experimental.size >= 1 && experimental.first != "0" @_experimental = true else @_experimental = false end end @_experimental end # A method for determining if all experimental features have been enabled # by either a global enabled value "1" or all features explicitly enabled. # # @return [Boolean] def global_enabled? if !defined?(@_global_enabled) experimental = features_requested if experimental.size == 1 && experimental.first == "1" @_global_enabled = true else @_global_enabled = false end end @_global_enabled end # A method for Vagrant internals to determine if a given feature # has been abled by the user, is a valid feature flag and can be used. # # @param [String] feature # @return [Boolean] - A hash containing the original array and if it is valid def feature_enabled?(feature) experimental = features_requested feature = feature.to_s return global_enabled? || experimental.include?(feature) end # Returns the features requested for the experimental flag # # @return [Array] - Returns an array of requested experimental features def features_requested if !defined?(@_requested_features) @_requested_features = ENV["VAGRANT_EXPERIMENTAL"].to_s.downcase.split(',') end @_requested_features end # A function to guard experimental blocks of code from being executed # # @param [Array] features - Array of features to guard a method with # @param [Block] block - Block of ruby code to be guarded against def guard_with(*features, &block) yield if block_given? && features.any? {|f| feature_enabled?(f)} end # @private # Reset the cached values for platform. This is not considered a public # API and should only be used for testing. def reset! instance_variables.each(&method(:remove_instance_variable)) end end end end end ================================================ FILE: lib/vagrant/util/file_checksum.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This is an "interface" that should be implemented by any digest class # passed into FileChecksum. Note that this isn't strictly enforced at # the moment, and this class isn't directly used. It is merely here for # documentation of structure of the class. require "vagrant/errors" class DigestClass def update(string); end def hexdigest; end end 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 # 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 end end # NOTE: This class was not originally namespaced # with the Util module so this is left for backwards # compatibility. FileChecksum = Vagrant::Util::FileChecksum ================================================ FILE: lib/vagrant/util/file_mode.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util class FileMode # This returns the file permissions as a string from # an octal number. def self.from_octal(octal) perms = sprintf("%o", octal) perms.reverse[0..2].reverse end end end end ================================================ FILE: lib/vagrant/util/file_mutex.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # Utility to provide a simple mutex via file lock class FileMutex # Create a new FileMutex instance # # @param mutex_path [String] path for file def initialize(mutex_path) @mutex_path = mutex_path end # Execute provided block within lock and unlock # when completed def with_lock(&block) lock begin block.call rescue => e raise e ensure unlock end end # Attempt to acquire the lock def lock if lock_file.flock(File::LOCK_EX|File::LOCK_NB) === false raise Errors::VagrantLocked, lock_file_path: @mutex_path end end # Unlock the file def unlock lock_file.flock(File::LOCK_UN) lock_file.close File.delete(@mutex_path) if File.file?(@mutex_path) end protected def lock_file return @lock_file if @lock_file && !@lock_file.closed? @lock_file = File.open(@mutex_path, "w+") end end end end ================================================ FILE: lib/vagrant/util/guest_hosts.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # Helper methods for modfiying guests /etc/hosts file module GuestHosts module Unix DEAFAULT_LOOPBACK_CHECK_LIMIT = 5.freeze # Add hostname to a loopback address on /etc/hosts if not already there # Will insert name at the first free address of the form 127.0.X.1, up to # the loop_bound # # @param [Communicator] # @param [String] full hostanme # @param [int] (option) defines the upper bound for searching for an available loopback address def add_hostname_to_loopback_interface(comm, name, loop_bound=DEAFAULT_LOOPBACK_CHECK_LIMIT) basename = name.split(".", 2)[0] comm.sudo <<-EOH.gsub(/^ {14}/, '') grep -w '#{name}' /etc/hosts || { for i in #{[*1..loop_bound].join(' ')}; do grep -w "127.0.${i}.1" /etc/hosts || { echo "127.0.${i}.1 #{name} #{basename}" >> /etc/hosts break } done } EOH end end # Linux specific inspection helpers module Linux include Unix # Remove any line in /etc/hosts that contains hostname, # then add hostname with associated ip # # @param [Communicator] # @param [String] full hostanme # @param [String] target ip def replace_host(comm, name, ip) basename = name.split(".", 2)[0] comm.sudo <<-EOH.gsub(/^ {14}/, '') sed -i '/#{name}/d' /etc/hosts sed -i'' '1i '#{ip}'\\t#{name}\\t#{basename}' /etc/hosts EOH end end # BSD specific inspection helpers module BSD include Unix # Remove any line in /etc/hosts that contains hostname, # then add hostname with associated ip # # @param [Communicator] # @param [String] full hostanme # @param [String] target ip def replace_host(comm, name, ip) basename = name.split(".", 2)[0] comm.sudo <<-EOH.gsub(/^ {14}/, '') sed -i.bak '/#{name}/d' /etc/hosts sed -i.bak '1i\\\n#{ip}\t#{name}\t#{basename}\n' /etc/hosts EOH end end end end end ================================================ FILE: lib/vagrant/util/guest_inspection.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # Helper methods for inspecting guests to determine if specific services # or applications are installed and in use module GuestInspection # Linux specific inspection helpers module Linux ## systemd helpers # systemd is in use # # @return [Boolean] def systemd?(comm) comm.test("ps -o comm= 1 | grep systemd", sudo: true) end # systemd-networkd.service is in use # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @return [Boolean] def systemd_networkd?(comm) comm.test("systemctl -q is-active systemd-networkd.service", sudo: true) end # NetworkManager.service is in use # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @return [Boolean] def systemd_network_manager?(comm) comm.test("systemctl -q is-active NetworkManager.service", sudo: true) end # Check if a unit file with the given name is defined. Name can # be a pattern or explicit name. # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @param [String] name Name or pattern to search # @return [Boolean] def systemd_unit_file?(comm, name) comm.test("systemctl -q list-unit-files | grep \"#{name}\"") end # Check if a unit is currently active within systemd # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @param [String] name Name or pattern to search # @return [Boolean] def systemd_unit?(comm, name) comm.test("systemctl -q list-units | grep \"#{name}\"") end # Check if given service is controlled by systemd # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @param [String] service_name Name of the service to check # @return [Boolean] def systemd_controlled?(comm, service_name) comm.test("systemctl -q is-active #{service_name}", sudo: true) end # systemd hostname set is via hostnamectl # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @return [Boolean] # NOTE: This test includes actually calling `hostnamectl` to verify # that it is in working order. This prevents attempts to use the # hostnamectl command when it is available, but dbus is not which # renders the command useless def hostnamectl?(comm) comm.test("command -v hostnamectl && hostnamectl") end ## netplan helpers # netplan is installed # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @return [Boolean] def netplan?(comm) comm.test("command -v netplan") end # is networkd isntalled # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @return [Boolean] def networkd?(comm) comm.test("command -v networkd") end ## nmcli helpers # nmcli is installed # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @return [Boolean] def nmcli?(comm) comm.test("command -v nmcli") end # NetworkManager currently controls device # # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @param device_name [String] # @return [Boolean] def nm_controlled?(comm, device_name) comm.test("nmcli -t d show #{device_name}") && !comm.test("nmcli -t d show #{device_name} | grep unmanaged") end end end end end ================================================ FILE: lib/vagrant/util/guest_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # Helper methods for configuring guest networks module GuestNetworks module Linux NETWORK_MANAGER_DEVICE_DIRECTORY = "/etc/NetworkManager/system-connections".freeze def configure_network_manager(machine, networks, **opts) comm = machine.communicate nm_directory = opts.fetch(:nm_directory, NETWORK_MANAGER_DEVICE_DIRECTORY) interfaces = machine.guest.capability(:network_interfaces) net_configs = machine.config.vm.networks.find_all { |type, _| type.to_s.end_with?("_network") }.map(&:last) # Get IDs of currently configured devices current_devs = get_current_devices(comm) networks.each.with_index do |network, i| net_opts = (net_configs[i] || {}).merge(network) net_opts[:type] = net_opts[:type].to_s net_opts[:device] = interfaces[network[:interface]] if !net_opts[:mac_address] comm.execute("cat /sys/class/net/#{net_opts[:device]}/address") do |type, data| net_opts[:mac_address] = data if type == :stdout end end tmpl_opts = { interface_name: net_opts[:device], type: net_opts[:type], mac_address: net_opts[:mac_address], uuid: SecureRandom.uuid } if net_opts[:type] != "dhcp" begin addr = IPAddr.new("#{net_opts[:ip]}") if addr.ipv4? tmpl_opts[:ipv4] = addr.to_string masked = addr.mask(net_opts[:netmask]) tmpl_opts[:ipv4_mask] = masked.prefix tmpl_opts[:ipv4_gateway] = masked.succ.to_string else tmpl_opts[:ipv6] = addr.to_string masked = addr.mask(net_opts[:netmask]) tmpl_opts[:ipv6_mask] = masked.prefix tmpl_opts[:ipv6_gateway] = masked.succ.to_string end rescue IPAddr::Error => err raise NetworkAddressInvalid, address: net_opts[:ip], mask: net_opts[:netmask], error: err.to_s end end entry = TemplateRenderer.render("networking/network_manager/network_manager_device", options: tmpl_opts) remote_path = "/tmp/vagrant-network-entry-#{net_opts[:device]}-#{Time.now.to_i}-#{i}" final_path = "#{nm_directory}/#{net_opts[:device]}.nmconnection" Tempfile.open("vagrant-nm-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close comm.upload(f.path, remote_path) end # Remove the device if it already exists if device_id = current_devs[net_opts[:device]] [ "nmcli d disconnect '#{net_opts[:device]}'", "nmcli c delete '#{device_id}'", ].each do |cmd| comm.sudo(cmd, error_check: false) end end # Apply the config [ "chown root:root '#{remote_path}'", "chmod 0600 '#{remote_path}'", "mv '#{remote_path}' '#{final_path}'", "nmcli c load '#{final_path}'", "nmcli d connect '#{net_opts[:device]}'" ].each do |cmd| comm.sudo(cmd) end end end # Get all network devices currently managed by NetworkManager. # @param [Vagrant::Plugin::V2::Communicator] comm Guest communicator # @return [Hash] A hash of current device names and their associated IDs. def get_current_devices(comm) {}.tap do |cd| comm.execute("nmcli -t c show") do |type, data| if type == :stdout data.strip.lines.map(&:chomp).each do |line| next if line.strip.empty? _, id, _, dev = line.strip.split(':') cd[dev] = id end end end end end end end end end ================================================ FILE: lib/vagrant/util/hash_with_indifferent_access.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # A hash with indifferent access. Mostly taken from Thor/Rails (thanks). # Normally I'm not a fan of using an indifferent access hash since Symbols # are basically memory leaks in Ruby, but since Vagrant is typically a quick # one-off binary run and it doesn't use too many hash keys where this is # used, the effect should be minimal. # # hash[:foo] #=> 'bar' # hash['foo'] #=> 'bar' # class HashWithIndifferentAccess < ::Hash def initialize(hash={}, &block) super(&block) hash.each do |key, value| self[convert_key(key)] = value end end def [](key) super(convert_key(key)) end def []=(key, value) super(convert_key(key), value) end def delete(key) super(convert_key(key)) end def values_at(*indices) indices.collect { |key| self[convert_key(key)] } end def merge(other) dup.merge!(other) end def merge!(other) other.each do |key, value| self[convert_key(key)] = value end self end def key?(key) super(convert_key(key)) end alias_method :include?, :key? alias_method :has_key?, :key? alias_method :member?, :key? protected def convert_key(key) key.is_a?(Symbol) ? key.to_s : key end end end end ================================================ FILE: lib/vagrant/util/install_cli_autocomplete.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # Generic installation of content to shell config file class InstallShellConfig PREPEND_STRING = "# >>>> Vagrant command completion (start)".freeze APPEND_STRING = "# <<<< Vagrant command completion (end)".freeze attr_accessor :prepend_string attr_accessor :string_insert attr_accessor :append_string attr_accessor :config_paths def initialize(string_insert, config_paths) @prepend_string = PREPEND_STRING @string_insert = string_insert @append_string = APPEND_STRING @config_paths = config_paths @logger = Log4r::Logger.new("vagrant::util::install_shell_config") end # Searches a users home dir for a shell config file based on a # given home dir and a configured set of config paths. If there # are multiple config paths, it will return the first match. # # @param [string] path to users home dir # @return [string] path to shell config file if exists def shell_installed(home) @logger.info("Searching for config in home #{home}") @config_paths.each do |path| config_file = File.join(home, path) if File.exist?(config_file) @logger.info("Found config file #{config_file}") return config_file end end return nil end # Searches a given file for the existence of a set prepend string. # This can be used to find if vagrant has inserted some strings to a file # # @param [string] path to a file (config file) # @return [boolean] true if the prepend string is found in the file def is_installed(path) File.foreach(path) do |line| if line.include?(@prepend_string) @logger.info("Found completion already installed in #{path}") return true end end return false end # Given a path to the users home dir, will install some given strings # marked by a prepend and append string # # @param [string] path to users home dir # @return [string] path to shell config file that was modified if exists def install(home) path = shell_installed(home) if path && !is_installed(path) File.open(path, "a") do |f| f.write("\n") f.write(@prepend_string) f.write("\n") f.write(@string_insert) f.write("\n") f.write(@append_string) f.write("\n") end end return path end end # Install autocomplete script to zsh config located as .zshrc class InstallZSHShellConfig < InstallShellConfig def initialize string_insert = """fpath=(#{File.join(Vagrant.source_root, "contrib", "zsh")} $fpath)\ncompinit""".freeze config_paths = [".zshrc".freeze].freeze super(string_insert, config_paths) end end # Install autocomplete script to bash config located as .bashrc or .bash_profile class InstallBashShellConfig < InstallShellConfig def initialize string_insert = ". #{File.join(Vagrant.source_root, 'contrib', 'bash', 'completion.sh')}".freeze config_paths = [".bashrc".freeze, ".bash_profile".freeze].freeze super(string_insert, config_paths) end end # Install autocomplete script for supported shells class InstallCLIAutocomplete SUPPORTED_SHELLS = { "zsh" => Vagrant::Util::InstallZSHShellConfig.new(), "bash" => Vagrant::Util::InstallBashShellConfig.new() } def self.install(shells=[]) shells = SUPPORTED_SHELLS.keys() if shells.empty? home = Dir.home written_paths = [] shells.map do |shell| if SUPPORTED_SHELLS[shell] written_paths.push(SUPPORTED_SHELLS[shell].install(home)) else raise ArgumentError, "shell must be in #{SUPPORTED_SHELLS.keys()}" end end.compact return written_paths end end end end ================================================ FILE: lib/vagrant/util/io.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/platform" module Vagrant module Util class IO # The chunk size for reading from subprocess IO. READ_CHUNK_SIZE = 4096 # Reads data from an IO object while it can, returning the data it reads. # When it encounters a case when it can't read anymore, it returns the # data. # # @return [String] def self.read_until_block(io) data = "" while true begin if Platform.windows? # Windows doesn't support non-blocking reads on # file descriptors or pipes so we have to get # a bit more creative. # Check if data is actually ready on this IO device. # We have to do this since `readpartial` will actually block # until data is available, which can cause blocking forever # in some cases. results = ::IO.select([io], nil, nil, 1.0) break if !results || results[0].empty? # Read! data << io.readpartial(READ_CHUNK_SIZE).encode( "UTF-8", Encoding.default_external, invalid: :replace, undef: :replace ) else # Do a simple non-blocking read on the IO object data << io.read_nonblock(READ_CHUNK_SIZE) end rescue EOFError, Errno::EAGAIN, ::IO::WaitReadable break end end data end end end end ================================================ FILE: lib/vagrant/util/ipv4_interfaces.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util module IPv4Interfaces def ipv4_interfaces Socket.getifaddrs.select do |ifaddr| ifaddr.addr && ifaddr.addr.ipv4? end.map do |ifaddr| [ifaddr.name, ifaddr.addr.ip_address] end end extend IPv4Interfaces end end end ================================================ FILE: lib/vagrant/util/is_port_open.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "socket" module Vagrant module Util # Contains the method {#is_port_open?} to check if a port is open # (listening) or closed (not in use). This method isn't completely # fool-proof, but it works enough of the time to be useful. module IsPortOpen # Checks if a port is open (listening) on a given host and port. # # @param [String] host Hostname or IP address. # @param [Integer] port Port to check. # @return [Boolean] `true` if the port is open (listening), `false` # otherwise. def is_port_open?(host, port) begin Socket.tcp(host, port, connect_timeout: 0.1).close true rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, \ Errno::ENETUNREACH, Errno::EACCES, Errno::ENOTCONN, Errno::EALREADY false end end extend IsPortOpen end end end ================================================ FILE: lib/vagrant/util/keypair.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "base64" require "ed25519" require "securerandom" require "vagrant/util/retryable" module Vagrant module Util class Keypair # Magic string header AUTH_MAGIC = "openssh-key-v1".freeze # Header of private key file content PRIVATE_KEY_START = "-----BEGIN OPENSSH PRIVATE KEY-----\n".freeze # Footer of private key file content PRIVATE_KEY_END = "-----END OPENSSH PRIVATE KEY-----\n".freeze # Check if provided key is a supported key type # # @param [Symbol] key Key type to check # @return [Boolean] key type is supported def self.valid_type?(key) VALID_TYPES.keys.include?(key) end # @return [Array] list of supported key types def self.available_types PREFER_KEY_TYPES.values end # Create a new keypair # # @param [String] password Password for the key or nil for no password (only supported for rsa type) # @param [Symbol] type Key type to generate # @return [Array] Public key, openssh private key, openssh public key with comment def self.create(password=nil, type: :rsa) if !VALID_TYPES.key?(type) raise ArgumentError, "Invalid key type requested (supported types: #{available_types.map(&:inspect).join(", ")})" end VALID_TYPES[type].create(password) end class Ed25519 # Key type identifier KEY_TYPE = "ssh-ed25519".freeze # Encodes given string # # @param [String] s String to encode # @return [String] def self.string(s) [s.length].pack("N") + s end # Encodes given string with padding to block size # # @param [String] s String to encode # @param [Integer] blocksize Defined block size # @return [String] def self.padded_string(s, blocksize) pad = blocksize - (s.length % blocksize) string(s + Array(1..pad).pack("c*")) end # Creates an ed25519 SSH key pair # @return [Array] Public key, openssh private key, openssh public key with comment # @note Password support was not included as it's not actively used anywhere. If it ends up being # something that's needed, it can be revisited def self.create(password=nil) if password raise NotImplementedError, "Ed25519 key pair generation does not support passwords" end # Generate the key base_key = ::Ed25519::SigningKey.generate # Define the comment used for the key comment = "vagrant" # Grab the raw public key public_key = base_key.verify_key.to_bytes # Encode the public key for use building the openssh private key encoded_public_key = string(KEY_TYPE) + string(public_key) # Format the public key into the openssh public key format for writing openssh_public_key = "#{KEY_TYPE} #{Base64.encode64(encoded_public_key).gsub("\n", "")} #{comment}" # Agent encoded private key is used when building the openssh private key # (https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-4.2.3) # (https://dnaeon.github.io/openssh-private-key-binary-format/) agent_private_key = [ ([SecureRandom.random_number((2**32)-1)] * 2).pack("NN"), # checkint, random uint32 value, twice (used for encryption verification) encoded_public_key, # includes the key type and public key string(base_key.seed + public_key), # private key with public key concatenated string(comment), # comment for the key ].join # Build openssh private key data (https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key) private_key = [ AUTH_MAGIC + "\0", # Magic string string("none"), # cipher name, no encryption, so none string("none"), # kdf name, no encryption, so none string(""), # kdf options/data, no encryption, so empty string [1].pack("N"), # Number of keys (just one) string(encoded_public_key), # The public key padded_string(agent_private_key, 8) # Private key encoded with agent rules, padded for 8 byte block size ].join # Create the openssh private key content openssh_private_key = [ PRIVATE_KEY_START, Base64.encode64(private_key), PRIVATE_KEY_END, ].join return [public_key, openssh_private_key, openssh_public_key] end end class Rsa extend Retryable # Key type identifier KEY_TYPE = "ssh-rsa" # Creates an SSH keypair and returns it. # # @param [String] password Password for the key, or nil for no password. # @return [Array] PEM-encoded public and private key, # respectively. The final element is the OpenSSH encoded public # key. def self.create(password=nil) # This sometimes fails with RSAError. It is inconsistent and strangely # sleeps seem to fix it. We just retry this a few times. See GH-5056 rsa_key = nil retryable(on: OpenSSL::PKey::RSAError, sleep: 2, tries: 5) do rsa_key = OpenSSL::PKey::RSA.new(2048) end public_key = rsa_key.public_key private_key = rsa_key.to_pem if password cipher = OpenSSL::Cipher.new('des3') private_key = rsa_key.to_pem(cipher, password) end # Generate the binary necessary for the OpenSSH public key. binary = [7].pack("N") binary += "ssh-rsa" ["e", "n"].each do |m| val = public_key.send(m) data = val.to_s(2) first_byte = data[0,1].unpack("c").first if val < 0 data[0] = [0x80 & first_byte].pack("c") elsif first_byte < 0 data = 0.chr + data end binary += [data.length].pack("N") + data end openssh_key = "ssh-rsa #{Base64.encode64(binary).gsub("\n", "")} vagrant" public_key = public_key.to_pem return [public_key, private_key, openssh_key] end end # Base class for Ecdsa type keys to subclass class Ecdsa # Encodes given string # # @param [String] s String to encode # @return [String] def self.string(s) [s.length].pack("N") + s end # Encodes given string with padding to block size # # @param [String] s String to encode # @param [Integer] blocksize Defined block size # @return [String] def self.padded_string(s, blocksize) pad = blocksize - (s.length % blocksize) string(s + Array(1..pad).pack("c*")) end # Creates an ed25519 SSH key pair # @return [Array] Public key, openssh private key, openssh public key with comment # @note Password support was not included as it's not actively used anywhere. If it ends up being # something that's needed, it can be revisited def self.create(password=nil) if password raise NotImplementedError, "Ecdsa key pair generation does not support passwords" end # Generate the key base_key = OpenSSL::PKey::EC.generate(self.const_get(:OPENSSL_CURVE)) # Define the comment used for the key comment = "vagrant" # Grab the raw public key public_key = base_key.public_key.to_bn.to_s(2) # Encode the public key for use building the openssh private key encoded_public_key = string(self.const_get(:KEY_TYPE)) + string(self.const_get(:OPENSSH_CURVE)) + string(public_key) # Format the public key into the openssh public key format for writing openssh_public_key = "#{self.const_get(:KEY_TYPE)} #{Base64.encode64(encoded_public_key).gsub("\n", "")} #{comment}" pk_value = base_key.private_key.to_s(2) # Pad the start of the key if required if pk_value.length % 8 == 0 pk_value = "\0#{pk_value}" end # Agent encoded private key is used when building the openssh private key # (https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-4.2.3) # (https://dnaeon.github.io/openssh-private-key-binary-format/) agent_private_key = [ ([SecureRandom.random_number((2**32)-1)] * 2).pack("NN"), # checkint, random uint32 value, twice (used for encryption verification) encoded_public_key, # includes the key type and public key string(pk_value), # private key string(comment), # comment for the key ].join # Build openssh private key data (https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key) private_key = [ AUTH_MAGIC + "\0", # Magic string string("none"), # cipher name, no encryption, so none string("none"), # kdf name, no encryption, so none string(""), # kdf options/data, no encryption, so empty string [1].pack("N"), # Number of keys (just one) string(encoded_public_key), # The public key padded_string(agent_private_key, 8) # Private key encoded with agent rules, padded for 8 byte block size ].join # Create the openssh private key content openssh_private_key = [ PRIVATE_KEY_START, Base64.encode64(private_key), PRIVATE_KEY_END, ].join return [public_key, openssh_private_key, openssh_public_key] end end class Ecdsa256 < Ecdsa KEY_TYPE = "ecdsa-sha2-nistp256".freeze OPENSSH_CURVE = "nistp256".freeze OPENSSL_CURVE = "prime256v1".freeze end class Ecdsa384 < Ecdsa KEY_TYPE = "ecdsa-sha2-nistp384".freeze OPENSSH_CURVE = "nistp384".freeze OPENSSL_CURVE = "secp384r1".freeze end class Ecdsa521 < Ecdsa KEY_TYPE = "ecdsa-sha2-nistp521".freeze OPENSSH_CURVE = "nistp521".freeze OPENSSL_CURVE = "secp521r1".freeze end # Supported key types. VALID_TYPES = { ecdsa256: Ecdsa256, ecdsa384: Ecdsa384, ecdsa521: Ecdsa521, ed25519: Ed25519, rsa: Rsa }.freeze # Ordered mapping of openssh key type name to lookup name. The # order defined here is based on preference. Note that ecdsa # ordering is based on performance PREFER_KEY_TYPES = { Ed25519::KEY_TYPE => :ed25519, Ecdsa256::KEY_TYPE => :ecdsa256, Ecdsa521::KEY_TYPE => :ecdsa521, Ecdsa384::KEY_TYPE => :ecdsa384, Rsa::KEY_TYPE => :rsa, }.freeze end end end ================================================ FILE: lib/vagrant/util/line_buffer.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util class LineBuffer # Maximum number of characters to buffer before sending # to callback without detecting a new line MAX_LINE_LENGTH = 5000.freeze # Create a new line buffer. The registered block # will be called when a new line is encountered on # provided input, or the max line length is reached def initialize(&callback) raise ArgumentError, "Expected callback but received none" if callback.nil? @mu = Mutex.new @callback = callback @buffer = "" end # Add string data to output # # @param [String] str String of data to output # @return [self] def <<(str) @mu.synchronize do while i = str.index("\n") @callback.call((@buffer + str[0, i+1]).rstrip) @buffer.clear str = str[i+1, str.length].to_s end @buffer << str.to_s if @buffer.length > MAX_LINE_LENGTH @callback.call(@buffer.dup) @buffer.clear end end self end # Closes the buffer. Any remaining data that has # been buffered will be given to the callback. # Once closed the instance will no longer be usable. # # @return [self] def close @mu.synchronize do # Send any remaining output on the buffer @callback.call(@buffer.dup) if !@buffer.empty? # Disable this buffer instance @callback = nil @buffer.clear @buffer.freeze end self end end end end ================================================ FILE: lib/vagrant/util/line_ending_helpers.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util module LineEndingHelpers # Converts line endings to unix-style line endings in the # given string. # # @param [String] string Original string # @return [String] The fixed string def dos_to_unix(string) string.gsub("\r\n", "\n") end end end end ================================================ FILE: lib/vagrant/util/logging_formatter.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/credential_scrubber" require "log4r/formatter/formatter" module Vagrant module Util # Wrapper for logging formatting to provide # information scrubbing prior to being written # to output target class LoggingFormatter < Log4r::BasicFormatter # @return [Log4r::PatternFormatter] attr_reader :formatter # Creates a new formatter wrapper instance. # # @param [Log4r::Formatter] def initialize(formatter) @formatter = formatter end # Format event and scrub output def format(event) msg = formatter.format(event) CredentialScrubber.desensitize(msg) end end class HCLogFormatter < Log4r::BasicFormatter MAX_MESSAGE_LENGTH = 4096 def format(event) message = format_object(event.data). force_encoding('UTF-8'). scrub("?") if message.length > MAX_MESSAGE_LENGTH message = Array.new.tap { |a| until message.empty? a << "continued..." unless a.empty? a << message.slice!(0, MAX_MESSAGE_LENGTH) end } else message = [message] end message.map do |msg| d = { "@timestamp" => Time.now.strftime("%Y-%m-%dT%H:%M:%S.%6N%:z"), "@level" => Log4r::LNAMES[event.level].downcase, "@module" => event.fullname, "@name" => event.name, "@message" => msg, } d["@caller"] = event.tracer[0] if event.tracer d.to_json + "\n" end end end class HCLogOutputter < Log4r::StderrOutputter def write(data) data.each do |d| super(d) end end end end end ================================================ FILE: lib/vagrant/util/map_command_options.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util class MapCommandOptions # Given a hash map of user specified argments, will generate # a list. Set the key to the command flag, and the value to # it's value. If the value is boolean (true), only the flag is # added. eg. # {a: "opt-a", b: true} -> ["--a", "opt-a", "--b"] # # @param [Hash] map of commands # @param [String] string prepended to cmd line flags (keys) # # @return[Array] commands in list form def self.map_to_command_options(map, cmd_flag="--") opt_list = [] if map == nil return opt_list end map.each do |k, v| # If the value is true (bool) add the key as the cmd flag if v.is_a?(TrueClass) opt_list.push("#{cmd_flag}#{k}") # If the value is a string, add the key as the flag, and value as the flags argument elsif v.is_a?(String) opt_list.push("#{cmd_flag}#{k}") opt_list.push(v) end end return opt_list end end end end ================================================ FILE: lib/vagrant/util/mime.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'mime/types' require 'securerandom' module Vagrant module Util module Mime class Multipart # @return [Array] collection of content part of the multipart mime attr_accessor :content # @return [String] type of the content attr_accessor :content_type # @return [Hash] headers for the mime attr_accessor :headers # @param [String] (optional) mime content type # @param [String] (optional) mime version def initialize(content_type="multipart/mixed") @content_id = "#{Time.now.to_i}@#{SecureRandom.alphanumeric(24)}.local" @boundary = "Boundary_#{SecureRandom.alphanumeric(24)}" @content_type = MIME::Types[content_type].first @content = [] @headers = { "Content-ID"=> "<#{@content_id}>", "Content-Type"=> "#{content_type}; boundary=#{@boundary}", } end # Add an entry to the multipart mime # # @param entry to add def add(entry) content << entry end # Output MimeEntity as a string # # @return [String] mime data def to_s output_string = "" headers.each do |k, v| output_string += "#{k}: #{v}\n" end output_string += "\n--#{@boundary}\n" @content.each do |entry| output_string += entry.to_s output_string += "\n--#{@boundary}\n" end output_string end end class Entity # @return [String] entity content attr_reader :content # @return [String] type of the entity content attr_reader :content_type # @return [String] content disposition attr_accessor :disposition # @param [String] entity content # @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" => content_type)) end @content = content @content_type = MIME::Types[content_type].first @content_id = "#{Time.now.to_i}@#{SecureRandom.alphanumeric(24)}.local" end # Output MimeEntity as a string # # @return [String] mime data def to_s output_string = "Content-ID: <#{@content_id}>\n" output_string += "Content-Type: #{@content_type}\n" if disposition output_string += "Content-Disposition: #{@disposition}\n" end output_string += "\n#{content}" output_string end end end end end ================================================ FILE: lib/vagrant/util/network_ip.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ipaddr" module Vagrant module Util module NetworkIP DEFAULT_MASK = "255.255.255.0".freeze LOGGER = Log4r::Logger.new("vagrant::util::NetworkIP") # Returns the network address of the given IP and subnet. # # @return [String] def network_address(ip, subnet) begin IPAddr.new(ip).mask(subnet).to_s rescue IPAddr::InvalidPrefixError LOGGER.warn("Provided mask '#{subnet}' is invalid. Falling back to using mask '#{DEFAULT_MASK}'") IPAddr.new(ip).mask(DEFAULT_MASK).to_s end end end end end ================================================ FILE: lib/vagrant/util/numeric.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Util class Numeric # Authors Note: This conversion has been borrowed from the ActiveSupport Numeric class # Conversion helper constants KILOBYTE = 1024 MEGABYTE = KILOBYTE * 1024 GIGABYTE = MEGABYTE * 1024 TERABYTE = GIGABYTE * 1024 PETABYTE = TERABYTE * 1024 EXABYTE = PETABYTE * 1024 BYTES_CONVERSION_MAP = {KB: KILOBYTE, MB: MEGABYTE, GB: GIGABYTE, TB: TERABYTE, PB: PETABYTE, EB: EXABYTE} # Regex borrowed from the vagrant-disksize config class SHORTHAND_MATCH_REGEX = /^(?[0-9]+)\s?(?KB|MB|GB|TB)?$/ class << self LOGGER = Log4r::Logger.new("vagrant::util::numeric") # A helper that converts a shortcut string to its bytes representation. # The expected format of `str` is essentially: "XX" # Where `XX` is shorthand for KB, MB, GB, TB, PB, or EB. For example, 50 megabytes: # # str = "50MB" # # @param [String] - str # @return [Integer,nil] - bytes - returns nil if method fails to convert to bytes def string_to_bytes(str) bytes = nil str = str.to_s.strip matches = SHORTHAND_MATCH_REGEX.match(str) if matches number = matches[:number].to_i unit = matches[:unit].to_sym if BYTES_CONVERSION_MAP.key?(unit) bytes = number * BYTES_CONVERSION_MAP[unit] else LOGGER.error("An invalid unit or format was given, string_to_bytes cannot convert #{str}") end end bytes end # Convert bytes to a user friendly string representation # # @param [Numeric] bytes Number of bytes to represent # @return [String] user friendly output def bytes_to_string(bytes) # We want to locate the size that will return the # smallest whole value number BYTES_CONVERSION_MAP.sort { |a, b| b.last <=> a.last }.each do |suffix, size| val = bytes.to_f / size next if val < 1 val = sprintf("%.2f", val) val.slice!(-1, 1) while val.end_with?("0") val.slice!(-1, 1) if val.end_with?(".") return "#{val}#{suffix}" end "#{bytes} byte#{"s" if bytes > 1}" end # Rounds actual value to two decimal places # # @param [Integer] bytes # @return [Integer] megabytes - bytes representation in megabytes def bytes_to_megabytes(bytes) (bytes / MEGABYTE.to_f).round(2) end # @private # Reset the cached values for platform. This is not considered a public # API and should only be used for testing. def reset! instance_variables.each(&method(:remove_instance_variable)) end end end end end ================================================ FILE: lib/vagrant/util/platform.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "rbconfig" require "shellwords" require "tempfile" require "tmpdir" require "log4r" require "vagrant/util/subprocess" require "vagrant/util/powershell" require "vagrant/util/which" module Vagrant module Util # This class just contains some platform checking code. class Platform class << self # Detect architecture of host system # # @return [String] def architecture if !defined?(@_host_architecture) if ENV["VAGRANT_HOST_ARCHITECTURE"].to_s != "" return @_host_architecture = ENV["VAGRANT_HOST_ARCHITECTURE"] end @_host_architecture = case RbConfig::CONFIG["target_cpu"] when "amd64", "x86_64", "x64" "amd64" when "386", "i386", "x86" "i386" when "arm64", "aarch64" "arm64" else RbConfig::CONFIG["target_cpu"] end end @_host_architecture end def logger if !defined?(@_logger) @_logger = Log4r::Logger.new("vagrant::util::platform") end @_logger end def cygwin? if !defined?(@_cygwin) @_cygwin = ENV["VAGRANT_DETECTED_OS"].to_s.downcase.include?("cygwin") || platform.include?("cygwin") || ENV["OSTYPE"].to_s.downcase.include?("cygwin") end @_cygwin end def msys? if !defined?(@_msys) @_msys = ENV["VAGRANT_DETECTED_OS"].to_s.downcase.include?("msys") || platform.include?("msys") || ENV["OSTYPE"].to_s.downcase.include?("msys") end @_msys end def wsl? if !defined?(@_wsl) @_wsl = false SilenceWarnings.silence! do # Find 'microsoft' in /proc/version indicative of WSL if File.file?('/proc/version') osversion = File.open('/proc/version', &:gets) if osversion.downcase.include?("microsoft") @_wsl = true end end end end @_wsl end [:darwin, :bsd, :freebsd, :linux, :solaris].each do |type| define_method("#{type}?") do platform.include?(type.to_s) end end def windows? return @_windows if defined?(@_windows) @_windows = %w[mingw mswin].any? { |t| platform.include?(t) } return @_windows end # Checks if the user running Vagrant on Windows has administrative # privileges. # # From: https://support.microsoft.com/en-us/kb/243330 # SID: S-1-5-19 # # @return [Boolean] def windows_admin? return @_windows_admin if defined?(@_windows_admin) @_windows_admin = -> { ps_cmd = '(new-object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)' output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd) return output == 'True' }.call return @_windows_admin end # Checks if Hyper-V is accessible to the local user. It will check # if user is in the "Hyper-V Administrators" group, is a Domain # administrator, and finally will run a manual interaction with # Hyper-V to determine if Hyper-V is usable for the current user. # # From: https://support.microsoft.com/en-us/kb/243330 # SID: S-1-5-32-578 # Name: BUILTIN\Hyper-V Administrators # SID: S-1-5-21DOMAIN-512 # Name: Domain Admins # # @return [Boolean] def windows_hyperv_admin? return @_windows_hyperv_admin if defined?(@_windows_hyperv_admin) if ENV["VAGRANT_IS_HYPERV_ADMIN"] return @_windows_hyperv_admin = true end ps_cmd = "Write-Output ([System.Security.Principal.WindowsIdentity]::GetCurrent().Groups | " \ "Select-Object Value | ConvertTo-JSON)" output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd) if output groups = begin JSON.load(output) rescue JSON::ParserError [] end admin_group = groups.detect do |g| g["Value"].to_s == "S-1-5-32-578" || (g["Value"].start_with?("S-1-5-21") && g["Value"].to_s.end_with?("-512")) end if admin_group return @_windows_hyperv_admin = true end end ps_cmd = "$x = (Get-VMHost).Name; if($x -eq [System.Net.Dns]::GetHostName()){ Write-Output 'true'}" output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd) result = output == "true" return @_windows_hyperv_admin = result end # Checks if Hyper-V is enabled on the host system and returns true # if enabled. # # @return [Boolean] def windows_hyperv_enabled? return @_windows_hyperv_enabled if defined?(@_windows_hyperv_enabled) @_windows_hyperv_enabled = -> { check_commands = Array.new.tap do |c| c << "(Get-WindowsOptionalFeature -FeatureName Microsoft-Hyper-V-Hypervisor -Online).State" c << "(Get-WindowsFeature -FeatureName Microsoft-Hyper-V-Hypervisor).State" end check_commands.each do |ps_cmd| begin output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd) return true if output == "Enabled" rescue Errors::PowerShellInvalidVersion logger.warn("Invalid PowerShell version detected during Hyper-V enable check") return false rescue Errors::PowerShellError logger.warn("Powershell command not found or error on execution of command") return false end end return false }.call return @_windows_hyperv_enabled end # This takes any path and converts it from a Windows path to a # Cygwin style path. # # @param [String] path # @return [String] def cygwin_path(path) begin cygpath = Vagrant::Util::Which.which("cygpath") if cygpath.nil? # If Which can't find it, just attempt to invoke it directly cygpath = "cygpath" else cygpath.gsub!("/", '\\') end process = Subprocess.execute( cygpath, "-u", "-a", path.to_s) return process.stdout.chomp rescue Errors::CommandUnavailableWindows => e # Sometimes cygpath isn't available (msys). Instead, do what we # can with bash tricks. process = Subprocess.execute( "bash", "--noprofile", "--norc", "-c", "cd #{Shellwords.escape(path)} && pwd") return process.stdout.chomp end end # This takes any path and converts it from a Windows path to a # msys style path. # # @param [String] path # @return [String] def msys_path(path) begin # We have to revert to the old env # path here, otherwise it looks like # msys2 ends up using the wrong cygpath # binary and ends up with a `/cygdrive` # when it doesn't exist in msys2 original_path_env = ENV['PATH'] ENV['PATH'] = ENV['VAGRANT_OLD_ENV_PATH'] cygwin_path(path) ensure ENV['PATH'] = original_path_env end end # This takes any path and converts it to a full-length Windows # path on Windows machines in Cygwin. # # @return [String] def cygwin_windows_path(path) return path if !cygwin? # Replace all "\" with "/", otherwise cygpath doesn't work. path = unix_windows_path(path) # Call out to cygpath and gather the result process = Subprocess.execute("cygpath", "-w", "-l", "-a", path.to_s) return process.stdout.chomp end # This takes any path and converts Windows-style path separators # to Unix-like path separators. # @return [String] def unix_windows_path(path) path.gsub("\\", "/") end # This checks if the filesystem is case sensitive. This is not a # 100% correct check, since it is possible that the temporary # directory runs a different filesystem than the root directory. # However, this works in many cases. def fs_case_sensitive? return @_fs_case_sensitive if defined?(@_fs_case_sensitive) @_fs_case_sensitive = Dir.mktmpdir("vagrant-fs-case-sensitive") do |dir| tmp_file = File.join(dir, "FILE") File.open(tmp_file, "w") do |f| f.write("foo") end # The filesystem is case sensitive if the lowercased version # of the filename is NOT reported as existing. !File.file?(File.join(dir, "file")) end return @_fs_case_sensitive end # This expands the path and ensures proper casing of each part # of the path. def fs_real_path(path, **opts) path = Pathname.new(File.expand_path(path)) if path.exist? && !fs_case_sensitive? # If the path contains a Windows short path, then we attempt to # expand. The require below is embedded here since it requires # windows to work. if windows? && path.to_s =~ /~\d(\/|\\)/ require_relative "windows_path" path = Pathname.new(WindowsPath.longname(path.to_s)) end # Build up all the parts of the path original = [] while !path.root? original.unshift(path.basename.to_s) path = path.parent end # Traverse each part and join it into the resulting path original.each do |single| Dir.entries(path).each do |entry| begin single = single.encode("filesystem").to_s rescue ArgumentError => err Vagrant.global_logger.warn("path encoding failed - part=#{single} err=#{err.class} msg=#{err}") # NOTE: Depending on the Windows environment the above # encode will generate an "input string invalid" when # attempting to encode. If that happens, continue on end if entry.downcase == single.downcase path = path.join(entry) end end end end if windows? # Fix the drive letter to be uppercase. path = path.to_s if path[1] == ":" path[0] = path[0].upcase end path = Pathname.new(path) end path end # Converts a given path to UNC format by adding a prefix and converting slashes. # @param [String] path Path to convert to UNC for Windows # @return [String] def windows_unc_path(path) path = path.gsub("/", "\\") # Convert to UNC path if path =~ /^[a-zA-Z]:\\?$/ # If the path is just a drive letter, then return that as-is path + "\\" elsif path.start_with?("\\\\") # If the path already starts with `\\` assume UNC and return as-is path else "\\\\?\\" + path.gsub("/", "\\") end end # Returns a boolean noting whether the terminal supports color. # output. def terminal_supports_colors? return @_terminal_supports_colors if defined?(@_terminal_supports_colors) @_terminal_supports_colors = -> { if windows? return true if ENV.key?("ANSICON") return true if cygwin? return true if ENV["TERM"] == "cygwin" return false end return true }.call return @_terminal_supports_colors end def platform return @_platform if defined?(@_platform) @_platform = RbConfig::CONFIG["host_os"].downcase return @_platform end # Determine if given path is within the WSL rootfs. Returns # true if within the subsystem, or false if outside the subsystem. # # @param [String] path Path to check # @return [Boolean] path is within subsystem def wsl_path?(path) wsl? && !path.to_s.downcase.start_with?("/mnt/") end # Compute the path to rootfs of currently active WSL. # # @return [String] A path to rootfs of a current WSL instance. def wsl_rootfs return @_wsl_rootfs if defined?(@_wsl_rootfs) if wsl? # Mark our filesystem with a temporary file having an unique name. marker = Tempfile.new(Time.now.to_i.to_s) logger = Log4r::Logger.new("vagrant::util::platform::wsl") # Check for lxrun installation first lxrun_path = [wsl_windows_appdata_local, "lxss"].join("\\") paths = [lxrun_path] logger.debug("checking registry for WSL installation path") paths += PowerShell.execute_cmd( '(Get-ChildItem HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss ' \ '| ForEach-Object {Get-ItemProperty $_.PSPath}).BasePath').to_s.split("\r\n").map(&:strip) paths.delete_if{|path| path.to_s.empty?} paths.each do |path| # Lowercase the drive letter, skip the next symbol (which is a # colon from a Windows path) and convert path to UNIX style. check_path = "/mnt/#{path[0, 1].downcase}#{path[2..-1].tr('\\', '/')}/rootfs" begin process = Subprocess.execute("wslpath", "-u", "-a", path) check_path = "#{process.stdout.chomp}/rootfs" if process.exit_code == 0 rescue Errors::CommandUnavailable => e # pass end logger.debug("checking `#{path}` for current WSL instance") begin # https://blogs.msdn.microsoft.com/wsl/2016/06/15/wsl-file-system-support # Current WSL instance doesn't have an access to its mount from # within itself despite all others are available. That's the # hacky way we're using to determine current instance. # For example we have three WSL instances: # A -> C:\User\USER\AppData\Local\Packages\A\LocalState\rootfs # B -> C:\User\USER\AppData\Local\Packages\B\LocalState\rootfs # C -> C:\User\USER\AppData\Local\Packages\C\LocalState\rootfs # If we're in "A" WSL at the moment, then its path will not be # accessible since it's mounted for exactly the instance we're # in. All others can be opened. Dir.open(check_path) do |fs| # A fallback for a case if our trick will stop working. For # that we've created a temporary file with an unique name in # a current WSL and now seeking it among all WSL. if File.exist?("#{fs.path}/#{marker.path}") @_wsl_rootfs = path break end end rescue Errno::EACCES @_wsl_rootfs = path # You can create and simultaneously run multiple WSL instances, # comment out the "break", run this script within each one and # it'll return only single value. break rescue Errno::ENOENT # Warn about data discrepancy between Winreg and file system # states. For the sake of justice, it's worth mentioning that # it is possible only when someone will manually break WSL by # removing a directory of its base path (kinda "stupid WSL # uninstallation by removing hidden and system directory"). logger.warn("WSL instance at `#{path} is broken or no longer exists") end # All other exceptions have to be raised since they will mean # something unpredictably terrible. end marker.close! raise Vagrant::Errors::WSLRootFsNotFoundError if @_wsl_rootfs.nil? end # Attach the rootfs leaf to the path if @_wsl_rootfs != lxrun_path @_wsl_rootfs = "#{@_wsl_rootfs}\\rootfs" end logger.debug("detected `#{@_wsl_rootfs}` as current WSL instance") @_wsl_rootfs end # Convert a WSL path to the local Windows path. This is useful # for conversion when calling out to Windows executables from # the WSL # # @param [String, Pathname] path Path to convert # @return [String] def wsl_to_windows_path(path) path = path.to_s if wsl? && wsl_windows_access? && !path.match(/^[a-zA-Z]:/) path = File.expand_path(path) begin process = Subprocess.execute("wslpath", "-w", "-a", path) return process.stdout.chomp if process.exit_code == 0 rescue Errors::CommandUnavailable => e # pass end if wsl_path?(path) parts = path.split("/") parts.delete_if(&:empty?) root_path = wsl_rootfs # lxrun splits home separate so we need to account # for it's specialness here when we build the path if root_path.end_with?("lxss") && !(["root", "home"].include?(parts.first)) root_path = "#{root_path}\\rootfs" end path = [root_path, *parts].join("\\") else path = path.sub("/mnt/", "") parts = path.split("/") parts.first << ":" path = parts.join("\\") end end path end # Takes a windows path and formats it to the # 'unix' style (i.e. `/cygdrive/c` or `/c/`) # # @param [Pathname, String] path Path to convert # @param [Hash] hash of arguments # @return [String] def format_windows_path(path, *args) path = cygwin_path(path) if cygwin? path = msys_path(path) if msys? path = wsl_to_windows_path(path) if wsl? if windows? || wsl? path = windows_unc_path(path) if !args.include?(:disable_unc) end path end # Automatically convert a given path to a Windows path. Will only # be applied if running on a Windows host. If running on Windows # host within the WSL, the actual Windows path will be returned. # # @param [Pathname, String] path Path to convert # @return [String] def windows_path(path) path = cygwin_windows_path(path) path = wsl_to_windows_path(path) if windows? || wsl? path = windows_unc_path(path) end path end # Allow Vagrant to access Vagrant managed machines outside the # Windows Subsystem for Linux # # @return [Boolean] def wsl_windows_access? if !defined?(@_wsl_windows_access) @_wsl_windows_access = wsl? && ENV["VAGRANT_WSL_ENABLE_WINDOWS_ACCESS"] end @_wsl_windows_access end # The allowed windows system path Vagrant can manage from the Windows # Subsystem for Linux # # @return [Pathname] def wsl_windows_accessible_path if !defined?(@_wsl_windows_accessible_path) access_path = ENV["VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH"] if access_path.to_s.empty? begin process = Subprocess.execute("wslpath", "-u", "-a", wsl_windows_home) access_path = process.stdout.chomp if process.exit_code == 0 rescue Errors::CommandUnavailable => e # pass end end if access_path.to_s.empty? access_path = wsl_windows_home.gsub("\\", "/").sub(":", "") access_path[0] = access_path[0].downcase access_path = "/mnt/#{access_path}" end @_wsl_windows_accessible_path = Pathname.new(access_path) end @_wsl_windows_accessible_path end # Checks given path to determine if Vagrant is allowed to bypass checks # # @param [String] path Path to check # @return [Boolean] Vagrant is allowed to bypass checks def wsl_windows_access_bypass?(path) wsl? && wsl_windows_access? && path.to_s.start_with?(wsl_windows_accessible_path.to_s) end # Mount pattern for extracting local mount information MOUNT_PATTERN = /^(?.+?) on (?.+?) type (?.+?) \((?.+)\)/.freeze # Get list of local mount paths that are DrvFs file systems # # @return [Array] # @todo(chrisroberts): Constantize types for check def wsl_drvfs_mounts if !defined?(@_wsl_drvfs_mounts) @_wsl_drvfs_mounts = [] if wsl? result = Util::Subprocess.execute("mount") result.stdout.each_line do |line| info = line.match(MOUNT_PATTERN) if info && (info[:type] == "drvfs" || info[:type] == "9p") @_wsl_drvfs_mounts << info[:mount] end end end end @_wsl_drvfs_mounts end # Check if given path is located on DrvFs file system # # @param [String, Pathname] path Path to check # @return [Boolean] def wsl_drvfs_path?(path) if wsl? wsl_drvfs_mounts.each do |mount_path| return true if path.to_s.start_with?(mount_path) end end false end # If running within the Windows Subsystem for Linux, this will provide # simple setup to allow sharing of the user's VAGRANT_HOME directory # within the subsystem # # @param [Environment] env # @param [Logger] logger Optional logger to display information def wsl_init(env, logger=nil) if wsl? if ENV["VAGRANT_WSL_ENABLE_WINDOWS_ACCESS"] wsl_validate_matching_vagrant_versions! shared_user = ENV["VAGRANT_WSL_WINDOWS_ACCESS_USER"] if shared_user.to_s.empty? shared_user = wsl_windows_username end if logger logger.warn("Windows Subsystem for Linux detected. Allowing access to user: #{shared_user}") logger.warn("Vagrant will be allowed to control Vagrant managed machines within the user's home path.") end if ENV["VAGRANT_HOME"] || ENV["VAGRANT_WSL_DISABLE_VAGRANT_HOME"] logger.warn("VAGRANT_HOME environment variable already set. Not overriding!") if logger else home_path = wsl_windows_accessible_path.to_s ENV["VAGRANT_HOME"] = File.join(home_path, ".vagrant.d") if logger logger.info("Overriding VAGRANT_HOME environment variable to configured windows user. (#{ENV["VAGRANT_HOME"]})") end true end else if env.local_data_path.to_s.start_with?("/mnt/") raise Vagrant::Errors::WSLVagrantAccessError end end end end # Fetch the Windows username currently in use # # @return [String, Nil] def wsl_windows_username if !@_wsl_windows_username result = Util::Subprocess.execute("cmd.exe", "/c", "echo %USERNAME%") if result.exit_code == 0 @_wsl_windows_username = result.stdout.strip end end @_wsl_windows_username end # Fetch the Windows user home directory # # @return [String, Nil] def wsl_windows_home if !@_wsl_windows_home result = Util::Subprocess.execute("cmd.exe", "/c" "echo %USERPROFILE%") if result.exit_code == 0 @_wsl_windows_home = result.stdout.gsub("\"", "").strip end end @_wsl_windows_home end # Fetch the Windows user local app data directory # # @return [String, Nil] def wsl_windows_appdata_local if !@_wsl_windows_appdata_local result = Util::Subprocess.execute("cmd.exe", "/c", "echo %LOCALAPPDATA%") if result.exit_code == 0 @_wsl_windows_appdata_local = result.stdout.gsub("\"", "").strip end end @_wsl_windows_appdata_local end # Confirm Vagrant versions installed within the WSL and the Windows system # are the same. Raise error if they do not match. def wsl_validate_matching_vagrant_versions! valid = false if Util::Which.which("vagrant.exe") result = Util::Subprocess.execute("vagrant.exe", "--version") if result.exit_code == 0 windows_version = result.stdout.match(/Vagrant (?[\w.-]+)/) if windows_version windows_version = windows_version[:version].strip valid = windows_version == Vagrant::VERSION end end if !valid raise Vagrant::Errors::WSLVagrantVersionMismatch, wsl_version: Vagrant::VERSION, windows_version: windows_version || "unknown" end end end # systemd is in use def systemd? if !defined?(@_systemd) if !windows? result = Vagrant::Util::Subprocess.execute("ps", "-o", "comm=", "1") @_systemd = result.stdout.chomp == "systemd" else @_systemd = false end end @_systemd end # @private # Reset the cached values for platform. This is not considered a public # API and should only be used for testing. def reset! instance_variables.each(&method(:remove_instance_variable)) end end end end end ================================================ FILE: lib/vagrant/util/powershell.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "base64" require "tmpdir" require_relative "subprocess" require_relative "which" module Vagrant module Util # Executes PowerShell scripts. # # This is primarily a convenience wrapper around Subprocess that # properly sets powershell flags for you. class PowerShell # NOTE: Version checks are only on Major MINIMUM_REQUIRED_VERSION = 3 # Number of seconds to wait while attempting to get powershell version DEFAULT_VERSION_DETECTION_TIMEOUT = 30 # Names of the powershell executable POWERSHELL_NAMES = ["pwsh", "powershell"].map(&:freeze).freeze # Paths to powershell executable POWERSHELL_PATHS = [ "%SYSTEMROOT%/System32/WindowsPowerShell/v1.0", "%WINDIR%/System32/WindowsPowerShell/v1.0", "%PROGRAMFILES%/PowerShell/7", "%PROGRAMFILES%/PowerShell/6" ].map(&:freeze).freeze LOGGER = Log4r::Logger.new("vagrant::util::powershell") # @return [String|nil] a powershell executable, depending on environment def self.executable if !defined?(@_powershell_executable) prefer_name = ENV["VAGRANT_PREFERRED_POWERSHELL"].to_s.sub(".exe", "") if !POWERSHELL_NAMES.include?(prefer_name) prefer_name = POWERSHELL_NAMES.first end LOGGER.debug("preferred powershell executable name: #{prefer_name}") # First start with detecting executable on configured path found_shells = Hash.new.tap do |found| POWERSHELL_NAMES.each do |psh| psh_path = Which.which(psh) psh_path = Which.which(psh + ".exe") if !psh_path next if !psh_path LOGGER.debug("detected powershell for #{psh.inspect} - #{psh_path}") found[psh] = psh_path end end # Done if preferred shell was found if found_shells.key?(prefer_name) LOGGER.debug("using preferred powershell #{prefer_name.inspect} - #{found_shells[prefer_name]}") return @_powershell_executable = found_shells[prefer_name] end # Now attempt with paths paths = POWERSHELL_PATHS.map do |ppath| result = Util::Subprocess.execute("cmd.exe", "/c", "echo #{ppath}") result.stdout.gsub("\"", "").strip if result.exit_code == 0 end.compact paths.each do |psh_path| POWERSHELL_NAMES.each do |psh| next if found_shells.key?(psh) path = File.join(psh_path, psh) [path, "#{path}.exe", path.sub(/^([A-Za-z]):/, "/mnt/\\1")].each do |full_path| if File.executable?(full_path) found_shells[psh] = full_path break end end end end # Done if preferred shell was found if found_shells.key?(prefer_name) LOGGER.debug("using preferred powershell #{prefer_name.inspect} - #{found_shells[prefer_name]}") return @_powershell_executable = found_shells[prefer_name] end # Iterate names and return first found POWERSHELL_NAMES.each do |psh| LOGGER.debug("using powershell #{prefer_name.inspect} - #{found_shells[prefer_name]}") return @_powershell_executable = found_shells[psh] if found_shells.key?(psh) end end @_powershell_executable end # @return [Boolean] powershell executable available on PATH def self.available? !executable.nil? end # Execute a powershell script. # # @param [String] path Path to the PowerShell script to execute. # @param [Array] args Command arguments # @param [Hash] opts Options passed to execute # @option opts [Hash] :env Custom environment variables # @return [Subprocess::Result] def self.execute(path, *args, **opts, &block) validate_install! if opts.delete(:sudo) || opts.delete(:runas) powerup_command(path, args, opts) else if mpath = opts.delete(:module_path) m_env = opts.fetch(:env, {}) m_env["PSModulePath"] = "$env:PSModulePath+';#{mpath}'" opts[:env] = m_env end if env = opts.delete(:env) env = env.map{|k,v| "$env:#{k}=#{v}"}.join(";") + "; " end command = [ executable, "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "#{env}&('#{path}')", args ].flatten # Append on the options hash since Subprocess doesn't use # Ruby 2.0 style options yet. command << opts Subprocess.execute(*command, &block) end end # Execute a powershell command. # # @param [String] command PowerShell command to execute. # @param [Hash] opts Extra options # @option opts [Hash] :env Custom environment variables # @return [nil, String] Returns nil if exit code is non-zero. # Returns stdout string if exit code is zero. def self.execute_cmd(command, **opts) validate_install! if mpath = opts.delete(:module_path) m_env = opts.fetch(:env, {}) m_env["PSModulePath"] = "$env:PSModulePath+';#{mpath}'" opts[:env] = m_env end if env = opts.delete(:env) env = env.map{|k,v| "$env:#{k}=#{v}"}.join(";") + "; " end c = [ executable, "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "#{env}#{command}" ].flatten.compact r = Subprocess.execute(*c) return nil if r.exit_code != 0 return r.stdout.chomp end # Execute a powershell command and return a result # # @param [String] command PowerShell command to execute. # @param [Hash] opts A collection of options for subprocess::execute # @option opts [Hash] :env Custom environment variables # @param [Block] block Ruby block def self.execute_inline(*command, **opts, &block) validate_install! if mpath = opts.delete(:module_path) m_env = opts.fetch(:env, {}) m_env["PSModulePath"] = "$env:PSModulePath+';#{mpath}'" opts[:env] = m_env end if env = opts.delete(:env) env = env.map{|k,v| "$env:#{k}=#{v}"}.join(";") + "; " end command = command.join(' ') c = [ executable, "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "#{env}#{command}" ].flatten.compact c << opts Subprocess.execute(*c, &block) end # Returns the version of PowerShell that is installed. # # @return [String] def self.version if !defined?(@_powershell_version) command = [ executable, "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Write-Output $PSVersionTable.PSVersion.Major" ].flatten version = nil timeout = ENV["VAGRANT_POWERSHELL_VERSION_DETECTION_TIMEOUT"].to_i if timeout < 1 timeout = DEFAULT_VERSION_DETECTION_TIMEOUT end begin r = Subprocess.execute(*command, notify: [:stdout, :stderr], timeout: timeout, ) {|io_name,data| version = data} rescue Vagrant::Util::Subprocess::TimeoutExceeded LOGGER.debug("Timeout exceeded while attempting to determine version of Powershell.") end @_powershell_version = version end @_powershell_version end # Validates that powershell is installed, available, and # at or above minimum required version # # @return [Boolean] # @raises [] def self.validate_install! if !defined?(@_powershell_validation) raise Errors::PowerShellNotFound if !available? if version.to_i < MINIMUM_REQUIRED_VERSION raise Errors::PowerShellInvalidVersion, minimum_version: MINIMUM_REQUIRED_VERSION, installed_version: version ? version : "N/A" end @_powershell_validation = true end @_powershell_validation end # Powerup the given command to perform privileged operations. # # @param [String] path # @param [Array] args # @return [Array] def self.powerup_command(path, args, opts) Dir.mktmpdir("vagrant") do |dpath| all_args = [path] + args.flatten.map{ |a| a.gsub(/^['"](.+)['"]$/, "\\1") } arg_list = "\"" + all_args.join("\" \"") + "\"" stdout = File.join(dpath, "stdout.txt") stderr = File.join(dpath, "stderr.txt") script = "& #{arg_list} ; exit $LASTEXITCODE;" script_content = Base64.strict_encode64(script.encode("UTF-16LE", "UTF-8")) # Wrap so we can redirect output to read later wrapper = "$p = Start-Process -FilePath powershell -ArgumentList @('-NoLogo', '-NoProfile', " \ "'-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', '#{script_content}') " \ "-PassThru -WindowStyle Hidden -Wait -RedirectStandardOutput '#{stdout}' -RedirectStandardError '#{stderr}'; " \ "if($p){ exit $p.ExitCode; }else{ exit 1 }" wrapper_content = Base64.strict_encode64(wrapper.encode("UTF-16LE", "UTF-8")) powerup = "$p = Start-Process -FilePath powershell -ArgumentList @('-NoLogo', '-NoProfile', " \ "'-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', '#{wrapper_content}') " \ "-PassThru -WindowStyle Hidden -Wait -Verb RunAs; if($p){ exit $p.ExitCode; }else{ exit 1 }" cmd = [ executable, "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", powerup ] result = Subprocess.execute(*cmd.push(opts)) r_stdout = result.stdout if File.exist?(stdout) r_stdout += File.read(stdout) end r_stderr = result.stderr if File.exist?(stderr) r_stderr += File.read(stderr) end Subprocess::Result.new(result.exit_code, r_stdout, r_stderr) end end # @private # Reset the cached values for platform. This is not considered a public # API and should only be used for testing. def self.reset! instance_variables.each(&method(:remove_instance_variable)) end end end end ================================================ FILE: lib/vagrant/util/presence.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util module Presence extend self # Determines if the given object is "present". A String is considered # present if the stripped contents are not empty. An Array/Hash is # considered present if they have a length of more than 1. "true" is # always present and `false` and `nil` are always not present. Any other # object is considered to be present. # # @return [true, false] def present?(obj) case obj when String !obj.strip.empty? when Symbol !obj.to_s.strip.empty? when Array !obj.compact.empty? when Hash !obj.empty? when TrueClass, FalseClass obj when NilClass false when Object true end end # Returns the presence of the object. If the object is {present?}, it is # returned. Otherwise `false` is returned. # # @return [Object, false] def presence(obj) if present?(obj) obj else false end end end end end ================================================ FILE: lib/vagrant/util/retryable.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module Vagrant module Util module Retryable # Retries a given block a specified number of times in the # event the specified exception is raised. If the retries # run out, the final exception is raised. # # This code is adapted slightly from the following blog post: # http://blog.codefront.net/2008/01/14/retrying-code-blocks-in-ruby-on-exceptions-whatever/ def retryable(opts=nil) logger = nil opts = { tries: 1, on: Exception }.merge(opts || {}) begin return yield rescue *opts[:on] => e if (opts[:tries] -= 1) > 0 logger = Log4r::Logger.new("vagrant::util::retryable") logger.info("Retryable exception raised: #{e.inspect}") sleep opts[:sleep].to_f if opts[:sleep] retry end raise end end end end end ================================================ FILE: lib/vagrant/util/safe_chdir.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'thread' module Vagrant module Util class SafeChdir @@chdir_lock = Mutex.new # Safely changes directory of this process by putting a lock around # it so that it is thread safe. This will yield a block and when the # block exits it changes back to the original directory. # # @param [String] dir Dir to change to temporarily def self.safe_chdir(dir) lock = @@chdir_lock begin @@chdir_lock.synchronize {} rescue ThreadError # If we already hold the lock, just create a new lock so we # definitely don't block and don't get an error. lock = Mutex.new end lock.synchronize do Dir.chdir(dir) do return yield end end end end end end ================================================ FILE: lib/vagrant/util/safe_env.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util class SafeEnv # This yields an environment hash to change and catches any issues # while changing the environment variables and raises a helpful error # to end users. def self.change_env yield ENV rescue Errno::EINVAL raise Errors::EnvInval end end end end ================================================ FILE: lib/vagrant/util/safe_exec.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # This module provides a `safe_exec` method which is a drop-in # replacement for `Kernel.exec` which addresses a specific issue # which manifests on OS X 10.5 (GH-51) and perhaps other operating systems. # This issue causes `exec` to fail if there is more than one system # thread. In that case, `safe_exec` automatically falls back to # forking. class SafeExec @@logger = Log4r::Logger.new("vagrant::util::safe_exec") def self.exec(command, *args) # Create a list of things to rescue from. Since this is OS # specific, we need to do some defined? checks here to make # sure they exist. rescue_from = [] rescue_from << Errno::EOPNOTSUPP if defined?(Errno::EOPNOTSUPP) rescue_from << Errno::E045 if defined?(Errno::E045) rescue_from << SystemCallError fork_instead = false begin if fork_instead if Vagrant::Util::Platform.windows? @@logger.debug("Using subprocess because windows platform") args = args.dup << {notify: [:stdout, :stderr]} result = Vagrant::Util::Subprocess.execute(command, *args) do |type, data| case type when :stdout @@logger.info(data, new_line: false) when :stderr @@logger.info(data, new_line: false) end end Kernel.exit(result.exit_code) else pid = fork Kernel.exec(command, *args) Process.wait(pid) end else if Vagrant::Util::Platform.windows? # Re-generate strings to ensure common encoding @@logger.debug("Converting command and arguments to common UTF-8 encoding for exec.") @@logger.debug("Command: `#{command.inspect}` Args: `#{args.inspect}`") begin command = "#{command}".encode("UTF-8") rescue Encoding::UndefinedConversionError => e @@logger.warn("Failed to convert command - #{e.class}: #{e} (`#{command}`)") end args = args.map do |arg| begin "#{arg}".encode("UTF-8") rescue Encoding::UndefinedConversionError => e @@logger.warn("Failed to convert command argument - #{e.class}: #{e} (`#{arg}`)") arg end end @@logger.debug("Converted - Command: `#{command.inspect}` Args: `#{args.inspect}`") end Kernel.exec(command, *args) end rescue *rescue_from # We retried already, raise the issue and be done raise if fork_instead # The error manifested itself, retry with a fork. fork_instead = true retry end end end end end ================================================ FILE: lib/vagrant/util/safe_puts.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # This module provides a `safe_puts` method which outputs to # the given IO object, and rescues any broken pipe errors and # ignores them. This is useful in cases where you're outputting # to stdout, for example, and the stdout is closed, but you want to # keep running. module SafePuts # Uses `puts` on the given IO object and safely ignores any # Errno::EPIPE. # # @param [String] message Message to output. # @param [Hash] opts Options hash. def safe_puts(message=nil, opts=nil) message ||= "" opts = { io: $stdout, printer: :puts }.merge(opts || {}) begin opts[:io].send(opts[:printer], message) rescue Errno::EPIPE # This is what makes this a `safe` puts. return end end end end end ================================================ FILE: lib/vagrant/util/scoped_hash_override.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # This allows for hash options to be overridden by a scope key # prefix. An example speaks best here. Imagine the following hash: # # original = { # id: "foo", # mitchellh__id: "bar", # mitchellh__other: "foo" # } # # scoped = scoped_hash_override(original, "mitchellh") # # scoped == { # id: "bar", # other: "foo" # } # module ScopedHashOverride def scoped_hash_override(original, scope) # Convert the scope to a string in case a symbol was given since # we use string comparisons for everything. scope = scope.to_s # Shallow copy the hash for the result result = original.dup original.each do |key, value| parts = key.to_s.split("__", 2) # If we don't have the proper parts, then bail next if parts.length != 2 # If this is our scope, then override if parts[0] == scope result[parts[1].to_sym] = value end end result end end end end ================================================ FILE: lib/vagrant/util/shell_quote.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util module ShellQuote # This will auto-escape the text with the given quote mark type. # # @param [String] text Text to escape # @param [String] quote The quote character, such as " def self.escape(text, quote) text.gsub(/#{quote}/) do |m| "#{m}\\#{m}#{m}" end end end end end ================================================ FILE: lib/vagrant/util/silence_warnings.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util module SilenceWarnings # This silences any Ruby warnings. def self.silence! original = $VERBOSE $VERBOSE = nil return yield ensure $VERBOSE = original end end end end ================================================ FILE: lib/vagrant/util/ssh.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require 'childprocess' require "vagrant/util/file_mode" require "vagrant/util/platform" require "vagrant/util/safe_exec" require "vagrant/util/safe_puts" require "vagrant/util/subprocess" require "vagrant/util/which" module Vagrant module Util # This is a class that has helpers on it for dealing with SSH. These # helpers don't depend on any part of Vagrant except what is given # via the parameters. class SSH extend SafePuts LOGGER = Log4r::Logger.new("vagrant::util::ssh") # Checks that the permissions for a private key are valid, and fixes # them if possible. SSH requires that permissions on the private key # are 0600 on POSIX based systems. This will make a best effort to # fix these permissions if they are not properly set. # # @param [Pathname] key_path The path to the private key. def self.check_key_permissions(key_path) # Don't do anything if we're on Windows, since Windows doesn't worry # about key permissions. return if Platform.windows? || Platform.wsl_windows_access_bypass?(key_path) LOGGER.debug("Checking key permissions: #{key_path}") stat = key_path.stat if !stat.owned? && Process.uid != 0 # The SSH key must be owned by ourselves, unless we're root raise Errors::SSHKeyBadOwner, key_path: key_path end if FileMode.from_octal(stat.mode) != "600" LOGGER.info("Attempting to correct key permissions to 0600") key_path.chmod(0600) # Re-stat the file to get the new mode, and verify it worked stat = key_path.stat if FileMode.from_octal(stat.mode) != "600" raise Errors::SSHKeyBadPermissions, key_path: key_path end end rescue Errno::EPERM # This shouldn't happen since we verify we own the file, but # it is possible in theory, so we raise an error. raise Errors::SSHKeyBadPermissions, key_path: key_path end # Halts the running of this process and replaces it with a full-fledged # SSH shell into a remote machine. # # Note: This method NEVER returns. The process ends after this. # # @param [Hash] ssh_info This is the SSH information. For the keys # required please see the documentation of {Machine#ssh_info}. # @param [Hash] opts These are additional options that are supported # by exec. def self.exec(ssh_info, opts={}) # Ensure the platform supports ssh. On Windows there are several programs which # include ssh, notably git, mingw and cygwin, but make sure ssh is in the path! # First try using the original path provided if ENV["VAGRANT_PREFER_SYSTEM_BIN"] != "0" ssh_path = Which.which("ssh", original_path: true) end # If we didn't find an ssh executable, see if we shipped one if !ssh_path ssh_path = Which.which("ssh") if ssh_path && Platform.windows? && (Platform.cygwin? || Platform.msys?) LOGGER.warn("Failed to locate native SSH executable. Using vendored version.") LOGGER.warn("If display issues are encountered, install the ssh package for your environment.") end end if !ssh_path if Platform.windows? raise Errors::SSHUnavailableWindows, host: ssh_info[:host], port: ssh_info[:port], username: ssh_info[:username], key_path: ssh_info[:private_key_path].join(", ") end raise Errors::SSHUnavailable end if Platform.windows? # On Windows, we need to detect whether SSH is actually "plink" # underneath the covers. In this case, we tell the user. r = Subprocess.execute(ssh_path) if r.stdout.include?("PuTTY Link") || r.stdout.include?("Plink: command-line connection utility") raise Errors::SSHIsPuttyLink, host: ssh_info[:host], port: ssh_info[:port], username: ssh_info[:username], key_path: ssh_info[:private_key_path].join(", ") end end # If plain mode is enabled then we don't do any authentication (we don't # set a user or an identity file) plain_mode = opts[:plain_mode] options = {} options[:host] = ssh_info[:host] options[:port] = ssh_info[:port] options[:username] = ssh_info[:username] options[:private_key_path] = ssh_info[:private_key_path] log_level = ssh_info[:log_level] || "FATAL" # Command line options command_options = [ "-p", options[:port].to_s, "-o", "LogLevel=#{log_level}"] if ssh_info[:compression] command_options += ["-o", "Compression=yes"] end if ssh_info[:dsa_authentication] command_options += ["-o", "DSAAuthentication=yes"] end # Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the # IdentitiesOnly option. Also, we don't enable it in plain mode or if # if keys_only is false so that SSH and Net::SSH properly search our identities # and tries to do it itself. if !Platform.solaris? && !plain_mode && ssh_info[:keys_only] command_options += ["-o", "IdentitiesOnly=yes"] end # no strict hostkey checking unless paranoid if ssh_info[:verify_host_key] == :never || !ssh_info[:verify_host_key] command_options += [ "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"] end if !ssh_info[:disable_deprecated_algorithms] command_options += [ "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", ] end # If we're not in plain mode and :private_key_path is set attach the private key path(s). if !plain_mode && options[:private_key_path] options[:private_key_path].each do |path| private_key_arr = [] if path.include?('%') if path.include?(' ') && Platform.windows? LOGGER.warn("Paths with spaces and % on windows is not supported and will fail to read the file") end # Use '-o' instead of '-i' because '-i' does not call # percent_expand in misc.c, but '-o' does. when passing the path, # replace '%' in the path with '%%' to escape the '%' path = path.to_s.gsub('%', '%%') private_key_arr = ["-o", "IdentityFile=\"#{path}\""] else # Pass private key file directly with '-i', which properly supports # paths with spaces on Windows guests private_key_arr = ["-i", path] end command_options += private_key_arr end end if ssh_info[:forward_x11] # Both are required so that no warnings are shown regarding X11 command_options += [ "-o", "ForwardX11=yes", "-o", "ForwardX11Trusted=yes"] end if ssh_info[:config] command_options += ["-F", ssh_info[:config]] end if ssh_info[:proxy_command] command_options += ["-o", "ProxyCommand=#{ssh_info[:proxy_command]}"] end if ssh_info[:forward_env] command_options += ["-o", "SendEnv=#{ssh_info[:forward_env].join(" ")}"] end # Configurables -- extra_args should always be last due to the way the # ssh args parser works. e.g. if the user wants to use the -t option, # any shell command(s) she'd like to run on the remote server would # have to be the last part of the 'ssh' command: # # $ ssh localhost -t -p 2222 "cd mydirectory; bash" # # Without having extra_args be last, the user loses this ability command_options += ["-o", "ForwardAgent=yes"] if ssh_info[:forward_agent] # Note about :extra_args # ssh_info[:extra_args] comes from a machines ssh config in a Vagrantfile, # where as opts[:extra_args] comes from running the ssh command command_options += Array(ssh_info[:extra_args]) if ssh_info[:extra_args] command_options.concat(opts[:extra_args]) if opts[:extra_args] # Build up the host string for connecting host_string = options[:host] host_string = "#{options[:username]}@#{host_string}" if !plain_mode command_options.unshift(host_string) # On Cygwin we want to get rid of any DOS file warnings because # we really don't care since both work. ENV["nodosfilewarning"] = "1" if Platform.cygwin? # If an ssh command is defined, use that. If an ssh binary was # discovered on the path, use that. Otherwise fail to just trying `ssh` ssh = ssh_info[:ssh_command] || ssh_path || 'ssh' # Invoke SSH with all our options if !opts[:subprocess] LOGGER.info("Invoking SSH: #{ssh} #{command_options.inspect}") _raw_exec(ssh, command_options, ssh_info, opts) return else LOGGER.info("Executing SSH in subprocess: #{ssh} #{command_options.inspect}") return _raw_subprocess(ssh, command_options, ssh_info, opts) end end def self._raw_exec(ssh, command_options, ssh_info, opts) SafeExec.exec(ssh, *command_options) end def self._raw_subprocess(ssh, command_options, ssh_info, opts) # If we're still here, it means we're supposed to subprocess # out to ssh rather than exec it. process = ChildProcess.build(ssh, *command_options) process.io.inherit! # Forward configured environment variables. if ssh_info[:forward_env] ssh_info[:forward_env].each do |key| process.environment[key] = ENV[key] end end process.start process.wait return process.exit_code end end end end ================================================ FILE: lib/vagrant/util/stacked_proc_runner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # Represents the "stacked proc runner" behavior which is used a # couple places within Vagrant. This allows procs to "stack" on # each other, then all execute in a single action. An example of # its uses can be seen in the {Config} class. module StackedProcRunner # Returns the proc stack. This should always be called as the # accessor of the stack. The instance variable itself should _never_ # be used. # # @return [Array] def proc_stack @_proc_stack ||= [] end # Adds (pushes) a proc to the stack. The actual proc added here is # not executed, but merely stored. # # @param [Proc] block def push_proc(&block) proc_stack << block end # Executes all the procs on the stack, passing in the given arguments. # The stack is not cleared afterwards. It is up to the user of this # mixin to clear the stack by calling `proc_stack.clear`. def run_procs!(*args) proc_stack.each do |proc| proc.call(*args) end end end end end ================================================ FILE: lib/vagrant/util/string_block_editor.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util # This class modifies strings by creating and managing Vagrant-owned # "blocks" via wrapping them in specially formed comments. # # This is useful when modifying a file that someone else owns and adding # automatic entries into it. Example: /etc/exports or some other # configuration file. # # Vagrant marks ownership of a block in the string by wrapping it in # VAGRANT-BEGIN and VAGRANT-END comments with a unique ID. Example: # # foo # # VAGRANT-BEGIN: id # some contents # created by vagrant # # VAGRANT-END: id # # The goal of this class is to be able to insert and remove these # blocks without modifying anything else in the string. # # The strings usually come from files but it is up to the caller to # manage the file resource. class StringBlockEditor # The current string value. This is the value that is modified by # the methods below. # # @return [String] attr_reader :value def initialize(string) @value = string end # This returns the keys (or ids) that are in the string. # # @return [] def keys regexp = /^#\s*VAGRANT-BEGIN:\s*(.+?)$\r?\n?(.*)$\r?\n?^#\s*VAGRANT-END:\s(\1)$/m @value.scan(regexp).map do |match| match[0] end end # This deletes the block with the given key if it exists. def delete(key) key = Regexp.quote(key) regexp = /^#\s*VAGRANT-BEGIN:\s*#{key}$.*^#\s*VAGRANT-END:\s*#{key}$\r?\n?/m @value.gsub!(regexp, "") end # This gets the value of the block with the given key. def get(key) key = Regexp.quote(key) regexp = /^#\s*VAGRANT-BEGIN:\s*#{key}$\r?\n?(.*?)\r?\n?^#\s*VAGRANT-END:\s*#{key}$\r?\n?/m match = regexp.match(@value) return nil if !match match[1] end # This inserts a block with the given key and value. # # @param [String] key # @param [String] value def insert(key, value) # Insert the new block into the value new_block = < ex # Raise our own version of the error so that users of the class # don't need to be aware of ChildProcess raise LaunchError.new(ex.message) end # If running with the detach option, no need to capture IO or # ensure program exists. if @options[:detach] return end # Make sure the stdin does not buffer process.io.stdin.sync = true if RUBY_PLATFORM != "java" # On Java, we have to close after. See down the method... # Otherwise, we close the writers right here, since we're # not on the writing side. stdout_writer.close stderr_writer.close end # Create a dictionary to store all the output we see. io_data = { stdout: "", stderr: "" } # Record the start time for timeout purposes start_time = Time.now.to_i open_readers = [stdout, stderr] open_writers = notify_stdin ? [process.io.stdin] : [] @logger.debug("Selecting on IO") while true results = ::IO.select(open_readers, open_writers, nil, 0.1) results ||= [] readers = results[0] writers = results[1] # Check if we have exceeded our timeout raise TimeoutExceeded, process.pid if timeout && (Time.now.to_i - start_time) > timeout # Check the readers to see if they're ready if readers && !readers.empty? readers.each do |r| # Read from the IO object data = IO.read_until_block(r) # We don't need to do anything if the data is empty next if data.empty? io_name = r == stdout ? :stdout : :stderr @logger.trace("#{io_name}: #{data.chomp}") io_data[io_name] += data yield io_name, data if block_given? && notify_table[io_name] end end # Break out if the process exited. We have to do this before # attempting to write to stdin otherwise we'll get a broken pipe # error. break if process.exited? # Check the writers to see if they're ready, and notify any listeners if writers && !writers.empty? && block_given? yield :stdin, process.io.stdin # if the callback closed stdin, we should remove it, because # IO.select() will throw if called with a closed io. if process.io.stdin.closed? open_writers = [] end end end # Wait for the process to end. begin remaining = (timeout || 32000) - (Time.now.to_i - start_time) remaining = 0 if remaining < 0 @logger.debug("Waiting for process to exit. Remaining to timeout: #{remaining}") process.poll_for_exit(remaining) rescue ChildProcess::TimeoutError raise TimeoutExceeded, process.pid end @logger.debug("Exit status: #{process.exit_code}") # Read the final output data, since it is possible we missed a small # amount of text between the time we last read data and when the # process exited. [stdout, stderr].each do |io| # Read the extra data, ignoring if there isn't any extra_data = IO.read_until_block(io) next if extra_data == "" # Log it out and accumulate io_name = io == stdout ? :stdout : :stderr io_data[io_name] += extra_data @logger.trace("#{io_name}: #{extra_data.chomp}") # Yield to any listeners any remaining data yield io_name, extra_data if block_given? && notify_table[io_name] end if RUBY_PLATFORM == "java" # On JRuby, we need to close the writers after the process, # for some reason. See GH-711. stdout_writer.close stderr_writer.close end # Return an exit status container return Result.new(process.exit_code, io_data[:stdout], io_data[:stderr]) ensure if process && process.alive? && !@options[:detach] # Make sure no matter what happens, the process exits process.stop(2) end end protected # An error which raises when a process fails to start class LaunchError < StandardError; end # An error which occurs when the process doesn't end within # the given timeout. class TimeoutExceeded < StandardError attr_reader :pid def initialize(pid) super() @pid = pid end end # Container class to store the results of executing a subprocess. class Result attr_reader :exit_code attr_reader :stdout attr_reader :stderr def initialize(exit_code, stdout, stderr) @exit_code = exit_code @stdout = stdout @stderr = stderr end end private # This is, quite possibly, the saddest function in all of Vagrant. # # If a user is running Vagrant via Bundler (but not via the official # installer), we want to reset to the "original" environment so that when # shelling out to other Ruby processes (specifically), the original # environment is restored. This is super important for things like # rbenv and chruby, who rely on environment variables to locate gems, but # Bundler stomps on those environment variables like an angry T-Rex after # watching Jurassic Park 2 and realizing they replaced you with CGI. # # If a user is running in Vagrant via the official installer, BUT trying # to execute a subprocess *outside* of the installer, we want to reset to # the "original" environment. In this case, the Vagrant installer actually # knows what the original environment was and replaces it completely. # # Finally, we reset any Bundler-specific environment variables, since the # subprocess being called could, itself, be Bundler. And Bundler does not # behave very nicely in these circumstances. # # This function was added in Vagrant 1.7.3, but there is a failsafe # because the author doesn't trust himself that this functionality won't # break existing assumptions, so users can specify # `VAGRANT_SKIP_SUBPROCESS_JAILBREAK` and none of the above will happen. # # This function modifies the given hash in place! # # @return [nil] def jailbreak(env = {}) return if ENV.key?("VAGRANT_SKIP_SUBPROCESS_JAILBREAK") if defined?(::Bundler) && defined?(::Bundler::ORIGINAL_ENV) env.replace(::Bundler::ORIGINAL_ENV) end env.merge!(Vagrant.original_env) # Bundler does this, so I guess we should as well, since I think it # other subprocesses that use Bundler will reload it env["MANPATH"] = ENV["BUNDLE_ORIG_MANPATH"] # Replace all current environment BUNDLE_ variables to nil ENV.each do |k,_| env[k] = nil if k[0,7] == "BUNDLE_" end # If RUBYOPT was set, unset it with Bundler if ENV.key?("RUBYOPT") env["RUBYOPT"] = ENV["RUBYOPT"].sub("-rbundler/setup", "") end nil end end end end ================================================ FILE: lib/vagrant/util/template_renderer.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'ostruct' require "pathname" require 'erubi' module Vagrant module Util # This class is used to render the ERB templates in the # `GEM_ROOT/templates` directory. class TemplateRenderer < OpenStruct class << self # Render a given template and return the result. This method optionally # takes a block which will be passed the renderer prior to rendering, which # allows the caller to set any view variables within the renderer itself. # # @return [String] Rendered template def render(*args) render_with(:render, *args) end # Render a given string and return the result. This method optionally # takes a block which will be passed the renderer prior to rendering, which # allows the caller to set any view variables within the renderer itself. # # @param [String] template The template data string. # @return [String] Rendered template def render_string(*args) render_with(:render_string, *args) end # Method used internally to DRY out the other renderers. This method # creates and sets up the renderer before calling a specified method on it. def render_with(method, template, data={}) renderer = new(template, data) yield renderer if block_given? renderer.send(method.to_sym) end end def initialize(template, data = {}) super() @template_root = data.delete(:template_root) @template_root ||= Vagrant.source_root.join("templates") @template_root = Pathname.new(@template_root) data[:template] = template data.each do |key, value| send("#{key}=", value) end end # Renders the template using the class instance as the binding. Because the # renderer inherits from `OpenStruct`, additional view variables can be # added like normal accessors. # # @return [String] def render old_template = template result = nil File.open(full_template_path, 'r') do |f| self.template = f.read result = render_string end result ensure self.template = old_template end # Renders a template, handling the template as a string, but otherwise # acting the same way as {#render}. # # @return [String] def render_string binding.eval(Erubi::Engine.new(template, trim: true).src) end # Returns the full path to the template, taking into account the gem directory # and adding the `.erb` extension to the end. # # @return [String] def full_template_path @template_root.join("#{template}.erb").to_s.squeeze("/") end end end end ================================================ FILE: lib/vagrant/util/uploader.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "uri" require "log4r" require "vagrant/util/busy" require "vagrant/util/platform" require "vagrant/util/subprocess" require "vagrant/util/curl_helper" module Vagrant module Util # This class uploads files using various protocols by subprocessing # to cURL. cURL is a much more capable and complete download tool than # a hand-rolled Ruby library, so we defer to its expertise. class Uploader # @param [String] destination Valid URL to upload file to # @param [String] file Location of file to upload on disk # @param [Hash] options # @option options [Vagrant::UI] :ui UI interface for output # @option options [String, Symbol] :method Request method for upload def initialize(destination, file, options=nil) options ||= {} @logger = Log4r::Logger.new("vagrant::util::uploader") @destination = destination.to_s @file = file.to_s @ui = options[:ui] @request_method = options[:method] if !@request_method @request_method = "PUT" end @request_method = @request_method.to_s.upcase end def upload! data_proc = Vagrant::Util::CurlHelper.capture_output_proc(@logger, @ui) @logger.info("Uploader starting upload: ") @logger.info(" -- Source: #{@file}") @logger.info(" -- Destination: #{@destination}") options = build_options subprocess_options = {notify: :stderr} begin execute_curl(options, subprocess_options, &data_proc) rescue Errors::UploaderError => e raise ensure @ui.clear_line if @ui end end protected def build_options options = [@destination, "--request", @request_method, "--upload-file", @file, "--fail"] return options end def execute_curl(options, subprocess_options, &data_proc) options = options.dup options << subprocess_options # Create the callback that is called if we are interrupted interrupted = false int_callback = Proc.new do @logger.info("Uploader interrupted!") interrupted = true end # Execute! result = Busy.busy(int_callback) do Subprocess.execute("curl", *options, &data_proc) end # If the upload was interrupted, then raise a specific error raise Errors::UploaderInterrupted if interrupted # If it didn't exit successfully, we need to parse the data and # show an error message. if result.exit_code != 0 @logger.warn("Uploader exit code: #{result.exit_code}") check = result.stderr.match(/\n*curl:\s+\((?\d+)\)\s*(?.*)$/) if !check err_msg = result.stderr else err_msg = check[:error] end raise Errors::UploaderError, exit_code: result.exit_code, message: err_msg end if @ui @ui.clear_line # Windows doesn't clear properly for some reason, so we just # output one more newline. @ui.detail("") if Platform.windows? end result end end end end ================================================ FILE: lib/vagrant/util/which.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/platform" module Vagrant module Util class Which # Cross-platform way of finding an executable in the PATH. # # which('ruby') #=> /usr/bin/ruby # # This code is adapted from the following post by mislav: # http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby # # @param [String] cmd The command to search for in the PATH. # @param [Hash] opts Optional flags # @option [Boolean] :original_path Search within original path if available # @return [String] The full path to the executable or `nil` if not found. def self.which(cmd, **opts) exts = nil if !Platform.windows? || ENV['PATHEXT'].nil? # If the PATHEXT variable is empty, we're on *nix and need to find # the exact filename exts = [''] elsif File.extname(cmd).length != 0 # On Windows: if filename contains an extension, we must match that # exact filename exts = [''] else # On Windows: otherwise try to match all possible executable file # extensions (.EXE .COM .BAT etc.) exts = ENV['PATHEXT'].split(';') end if opts[:original_path] search_path = ENV.fetch('VAGRANT_OLD_ENV_PATH', ENV['PATH']) else search_path = ENV['PATH'] end SilenceWarnings.silence! do search_path.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '').split(File::PATH_SEPARATOR).each do |path| exts.each do |ext| exe = "#{path}#{File::SEPARATOR}#{cmd}#{ext}" return exe if File.executable? exe end end end return nil end end end end ================================================ FILE: lib/vagrant/util/windows_path.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fiddle/import" module Vagrant module Util module WindowsPath module API extend Fiddle::Importer dlload 'kernel32.dll' extern("int GetLongPathNameA(char*, char*, int)", :stdcall) end # Converts a Windows shortname to a long name. This only works # for ASCII paths currently and doesn't use the wide character # support. def self.longname(name) # We loop over the API call in case we didn't allocate enough # buffer space. In general it is usually enough. bufferlen = 250 buffer = nil while true buffer = ' ' * bufferlen len = API.GetLongPathNameA(name.to_s, buffer, buffer.size) if bufferlen < len # If the length returned is larger than our buffer length, # it is the API telling us it needs more space. Allocate it # and retry. bufferlen = len continue end break end return buffer.rstrip.chomp("\0") end end end end ================================================ FILE: lib/vagrant/util.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant module Util autoload :ANSIEscapeCodeRemover, 'vagrant/util/ansi_escape_code_remover' autoload :Busy, 'vagrant/util/busy' autoload :Caps, 'vagrant/util/caps' autoload :CheckpointClient, 'vagrant/util/checkpoint_client' autoload :CommandDeprecation, 'vagrant/util/command_deprecation' autoload :Counter, 'vagrant/util/counter' autoload :CredentialScrubber, 'vagrant/util/credential_scrubber' autoload :CurlHelper, 'vagrant/util/curl_helper' autoload :DeepMerge, 'vagrant/util/deep_merge' autoload :Directory, 'vagrant/util/directory' autoload :Downloader, 'vagrant/util/downloader' autoload :Env, 'vagrant/util/env' autoload :Experimental, 'vagrant/util/experimental' autoload :FileChecksum, 'vagrant/util/file_checksum' autoload :FileMode, 'vagrant/util/file_mode' autoload :FileMutex, 'vagrant/util/file_mutex' autoload :GuestHosts, 'vagrant/util/guest_hosts' autoload :GuestInspection, 'vagrant/util/guest_inspection' autoload :GuestNetworks, 'vagrant/util/guest_networks' autoload :HashWithIndifferentAccess, 'vagrant/util/hash_with_indifferent_access' autoload :HCLogFormatter, 'vagrant/util/logging_formatter' autoload :HCLogOutputter, 'vagrant/util/logging_formatter' autoload :InstallShellConfig, 'vagrant/util/install_cli_autocomplete' autoload :InstallZSHShellConfig, 'vagrant/util/install_cli_autocomplete' autoload :InstallBashShellConfig, 'vagrant/util/install_cli_autocomplete' autoload :InstallCLIShellConfig, 'vagrant/util/install_cli_autocomplete' autoload :IO, 'vagrant/util/io' autoload :IPV4Interfaces, 'vagrant/util/ipv4_interfaces' autoload :IsPortOpen, 'vagrant/util/is_port_open' autoload :Keypair, 'vagrant/util/keypair' autoload :LineBuffer, 'vagrant/util/line_buffer' autoload :LineEndingHelpers, 'vagrant/util/line_ending_helpers' autoload :LoggingFormatter, 'vagrant/util/logging_formatter' autoload :MapCommandOptions, 'vagrant/util/map_command_options' autoload :Mime, 'vagrant/util/mime' autoload :NetworkIP, 'vagrant/util/network_ip' autoload :Numeric, 'vagrant/util/numeric' autoload :Platform, 'vagrant/util/platform' autoload :Powershell, 'vagrant/util/powershell' autoload :Presence, 'vagrant/util/presence' autoload :Retryable, 'vagrant/util/retryable' autoload :SafeChdir, 'vagrant/util/safe_chdir' autoload :SafeEnv, 'vagrant/util/safe_env' autoload :SafeExec, 'vagrant/util/safe_exec' autoload :SafePuts, 'vagrant/util/safe_puts' autoload :ScopedHashOverride, 'vagrant/util/scoped_hash_override' autoload :ShellQuote, 'vagrant/util/shell_quote' autoload :SilenceWarnings, 'vagrant/util/silence_warnings' autoload :SSH, 'vagrant/util/ssh' autoload :StackedProcRunner, 'vagrant/util/stacked_proc_runner' autoload :StringBlockEditor, 'vagrant/util/string_block_editor' autoload :Subprocess, 'vagrant/util/subprocess' autoload :TemplateRenderer, 'vagrant/util/template_renderer' autoload :Uploader, 'vagrant/util/uploader' autoload :Which, 'vagrant/util/which' autoload :WindowsPath, 'vagrant/util/windows_path' end end ================================================ FILE: lib/vagrant/vagrantfile.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/template_renderer" require "log4r" module Vagrant # This class provides a way to load and access the contents # of a Vagrantfile. # # This class doesn't actually load Vagrantfiles, parse them, # merge them, etc. That is the job of {Config::Loader}. This # class, on the other hand, has higher-level operations on # a loaded Vagrantfile such as looking up the defined machines, # loading the configuration of a specific machine/provider combo, # etc. class Vagrantfile # This is the configuration loaded as-is given the loader and # keys to #initialize. attr_reader :config # Initializes by loading a Vagrantfile. # # @param [Config::Loader] loader Configuration loader that should # already be configured with the proper Vagrantfile locations. # This usually comes from {Vagrant::Environment} # @param [Array] keys The Vagrantfiles to load and the # order to load them in (keys within the loader). def initialize(loader, keys) @keys = keys @loader = loader @config, _ = loader.load(keys) @logger = Log4r::Logger.new("vagrant::vagrantfile") end # Returns a {Machine} for the given name and provider that # is represented by this Vagrantfile. # # @param [Symbol] name Name of the machine. # @param [Symbol] provider The provider the machine should # be backed by (required for provider overrides). # @param [BoxCollection] boxes BoxCollection to look up the # box Vagrantfile. # @param [Pathname] data_path Path where local machine data # can be stored. # @param [Environment] env The environment running this machine # @return [Machine] def machine(name, provider, boxes, data_path, env) # Load the configuration for the machine results = machine_config(name, provider, boxes, data_path) box = results[:box] config = results[:config] config_errors = results[:config_errors] config_warnings = results[:config_warnings] provider_cls = results[:provider_cls] provider_options = results[:provider_options] # If there were warnings or errors we want to output them if !config_warnings.empty? || !config_errors.empty? # The color of the output depends on whether we have warnings # or errors... level = config_errors.empty? ? :warn : :error output = Util::TemplateRenderer.render( "config/messages", warnings: config_warnings, errors: config_errors).chomp env.ui.send(level, I18n.t("vagrant.general.config_upgrade_messages", name: name, output: output)) # If we had errors, then we bail raise Errors::ConfigUpgradeErrors if !config_errors.empty? end # Get the provider configuration from the final loaded configuration provider_config = config.vm.get_provider_config(provider) # Create machine data directory if it doesn't exist # XXX: Permissions error here. FileUtils.mkdir_p(data_path) # Create the machine and cache it for future calls. This will also # return the machine from this method. return Machine.new(name, provider, provider_cls, provider_config, provider_options, config, data_path, box, env, self) end # Returns the configuration for a single machine. # # When loading a box Vagrantfile, it will be prepended to the # key order specified when initializing this class. Sub-machine # and provider-specific overrides are appended at the end. The # actual order is: # # - box # - keys specified for #initialize # - sub-machine # - provider # # The return value is a hash with the following keys (symbols) # and values: # # - box: the {Box} backing the machine # - config: the actual configuration # - config_errors: list of errors, if any # - config_warnings: list of warnings, if any # - provider_cls: class of the provider backing the machine # - provider_options: options for the provider # # @param [Symbol] name Name of the machine. # @param [Symbol] provider The provider the machine should # be backed by (required for provider overrides). # @param [BoxCollection] boxes BoxCollection to look up the # box Vagrantfile. # @param [Pathname] data_path Machine data path # @return [Hash] Various configuration parameters for a # machine. See the main documentation body for more info. def machine_config(name, provider, boxes, data_path=nil, validate_provider=true) keys = @keys.dup sub_machine = @config.vm.defined_vms[name] if !sub_machine raise Errors::MachineNotFound, name: name, provider: provider end provider_plugin = nil provider_cls = nil provider_options = {} box_formats = nil if provider != nil provider_plugin = Vagrant.plugin("2").manager.providers[provider] if !provider_plugin && validate_provider providers = Vagrant.plugin("2").manager.providers.to_hash.keys if providers providers_str = providers.join(', ') else providers_str = "N/A" end if providers.include? provider.downcase raise Errors::ProviderNotFoundSuggestion, machine: name, provider: provider, suggestion: provider.downcase, providers: providers_str end raise Errors::ProviderNotFound, machine: name, provider: provider, providers: providers_str end if validate_provider provider_cls = provider_plugin[0] provider_options = provider_plugin[1] box_formats = provider_options[:box_format] || provider # Test if the provider is usable or not begin provider_cls.usable?(true) rescue Errors::VagrantError => e raise Errors::ProviderNotUsable, machine: name.to_s, provider: provider.to_s, message: e.to_s end else box_formats = provider end end # Add the sub-machine configuration to the loader and keys vm_config_key = "#{object_id}_machine_#{name}" @loader.set(vm_config_key, sub_machine.config_procs) keys << vm_config_key # Load once so that we can get the proper box value config, config_warnings, config_errors = @loader.load(keys) # Track the original box so we know if we changed box = nil initial_box = original_box = config.vm.box initial_version = original_version = config.vm.box_version # Check if this machine has a local box metadata file # describing the existing guest. If so, load it and # set the box name and version to allow the actual # box in use to be discovered. if data_path meta_file = data_path.join("box_meta") if meta_file.file? box_meta = JSON.parse(meta_file.read) config.vm.box = box_meta["name"] config.vm.box_version = box_meta["version"] end end # The proc below loads the box and provider overrides. This is # in a proc because it may have to recurse if the provider override # changes the box. load_box_proc = lambda do local_keys = keys.dup # Load the box Vagrantfile, if there is one if !config.vm.box.to_s.empty? && boxes box = boxes.find(config.vm.box, box_formats, config.vm.box_version, config.vm.box_architecture) if box box_vagrantfile = find_vagrantfile(box.directory) if box_vagrantfile && !config.vm.ignore_box_vagrantfile box_config_key = "#{boxes.object_id}_#{box.name}_#{box.provider}".to_sym @loader.set(box_config_key, box_vagrantfile) local_keys.unshift(box_config_key) config, config_warnings, config_errors = @loader.load(local_keys) elsif box_vagrantfile && config.vm.ignore_box_vagrantfile @logger.warn("Ignoring #{box.name} provided Vagrantfile inside box") end end end # Load provider overrides provider_overrides = config.vm.get_provider_overrides(provider) if !provider_overrides.empty? config_key = "#{object_id}_vm_#{name}_#{config.vm.box}_#{provider}".to_sym @loader.set(config_key, provider_overrides) local_keys << config_key config, config_warnings, config_errors = @loader.load(local_keys) end # If the box changed, then we need to reload if original_box != config.vm.box || original_version != config.vm.box_version # TODO: infinite loop protection? original_box = config.vm.box original_version = config.vm.box_version load_box_proc.call end end # Load the box and provider overrides load_box_proc.call # NOTE: In cases where the box_meta file contains stale information # and the reference box no longer exists, fall back to initial # configuration and attempt to load that if box.nil? @logger.warn("Failed to locate #{config.vm.box} with version #{config.vm.box_version}") @logger.warn("Performing lookup with initial values #{initial_box} with version #{initial_version}") config.vm.box = original_box = initial_box config.vm.box_version = original_box = initial_version load_box_proc.call end # Ensure box attributes are set to original values in # case they were modified by the local box metadata config.vm.box = original_box config.vm.box_version = original_version return { box: box, provider_cls: provider_cls, provider_options: provider_options.dup, config: config, config_warnings: config_warnings, config_errors: config_errors, } end # Returns a list of the machines that are defined within this # Vagrantfile. # # @return [Array] def machine_names @config.vm.defined_vm_keys.dup end # Returns a list of the machine names as well as the options that # were specified for that machine. # # @return [Hash] def machine_names_and_options {}.tap do |r| @config.vm.defined_vms.each do |name, subvm| r[name] = subvm.options || {} end end end # Returns the name of the machine that is designated as the # "primary." # # In the case of a single-machine environment, this is just the # single machine name. In the case of a multi-machine environment, # then this is the machine that is marked as primary, or nil if # no primary machine was specified. # # @return [Symbol] def primary_machine_name # If it is a single machine environment, then return the name return machine_names.first if machine_names.length == 1 # If it is a multi-machine environment, then return the primary @config.vm.defined_vms.each do |name, subvm| return name if subvm.options[:primary] end # If no primary was specified, nil it is nil end protected def find_vagrantfile(search_path) ["Vagrantfile", "vagrantfile"].each do |vagrantfile| current_path = search_path.join(vagrantfile) return current_path if current_path.file? end nil end end end ================================================ FILE: lib/vagrant/version.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module Vagrant # This will always be up to date with the current version of Vagrant, # since it is used to generate the gemspec and is also the source of # the version for `vagrant -v` VERSION = File.read( File.expand_path("../../../version.txt", __FILE__)).chomp end ================================================ FILE: lib/vagrant.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # Load the shared helpers first to make the custom # require helper available. require "vagrant/shared_helpers" require "log4r" # Add patches to log4r to support trace level require "vagrant/patches/log4r" require "vagrant/patches/net-ssh" require "vagrant/patches/rubygems" require "vagrant/patches/timeout_error" # Set our log levels and include trace require 'log4r/configurator' Log4r::Configurator.custom_levels(*(["TRACE"] + Log4r::Log4rConfig::LogLevels)) # Update the default formatter within the log4r library to ensure # sensitive values are being properly scrubbed from logger data class Log4r::BasicFormatter alias_method :vagrant_format_object, :format_object def format_object(obj) Vagrant::Util::CredentialScrubber.desensitize(vagrant_format_object(obj)) end end require "optparse" module Vagrant # This is a customized OptionParser for Vagrant plugins. It # will automatically add any default CLI options defined # outside of command implementations to the local option # parser instances in use class OptionParser < ::OptionParser def initialize(*_) super Vagrant.default_cli_options.each do |opt_proc| opt_proc.call(self) end end end end # Inject the option parser into the vagrant plugins # module so it is automatically used when defining # command options module VagrantPlugins OptionParser = Vagrant::OptionParser end # Load in our helpers and utilities require "rubygems" require "vagrant/util" require "vagrant/plugin/manager" # Enable logging if it is requested. We do this before # anything else so that we can setup the output before # any logging occurs. # NOTE: We must do this little hack to allow # rest-client to write using the `<<` operator. # See https://github.com/rest-client/rest-client/issues/34#issuecomment-290858 # for more information class VagrantLogger < Log4r::Logger def << msg debug(msg.strip) end end if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != "" level = Log4r::LNAMES.index(ENV["VAGRANT_LOG"].upcase) if level.nil? level = Log4r::LNAMES.index("FATAL") end if !level # We directly write to stderr here because the VagrantError system # is not setup yet. $stderr.puts "Invalid VAGRANT_LOG level is set: #{ENV["VAGRANT_LOG"]}" $stderr.puts "" $stderr.puts "Please use one of the standard log levels: debug, info, warn, or error" exit 1 end # Set the logging level on all "vagrant" namespaced # logs as long as we have a valid level. if level ["vagrant", "vagrantplugins"].each do |lname| logger = VagrantLogger.new(lname) if ENV["VAGRANT_LOG_FILE"] && ENV["VAGRANT_LOG_FILE"] != "" logger.outputters = Log4r::FileOutputter.new("vagrant", filename: ENV["VAGRANT_LOG_FILE"]) else logger.outputters = Log4r::Outputter.stderr end logger.level = level end Log4r::RootLogger.instance.level = level base_formatter = Log4r::BasicFormatter.new if ENV["VAGRANT_LOG_TIMESTAMP"] base_formatter = Log4r::PatternFormatter.new( pattern: "%d [%5l] %m", date_pattern: "%F %T" ) end Log4r::Outputter.stderr.formatter = Vagrant::Util::LoggingFormatter.new(base_formatter) end end require 'json' require 'pathname' require 'stringio' require 'childprocess' require 'i18n' # OpenSSL must be loaded here since when it is loaded via `autoload` # there are issues with ciphers not being properly loaded. require 'openssl' # Always make the version available require 'vagrant/version' global_logger = Log4r::Logger.new("vagrant::global") Vagrant.global_logger = global_logger global_logger.info("Vagrant version: #{Vagrant::VERSION}") global_logger.info("Ruby version: #{RUBY_VERSION}") global_logger.info("RubyGems version: #{Gem::VERSION}") ENV.each do |k, v| next if k.start_with?("VAGRANT_OLD") global_logger.info("#{k}=#{v.inspect}") if k.start_with?("VAGRANT_") end # If the vagrant_ssl library exists, a recent version # of openssl is in use and its needed to load all the # providers needed vagrant_ssl_locations = [ File.expand_path("vagrant/vagrant_ssl.so", __dir__), File.expand_path("vagrant/vagrant_ssl.bundle", __dir__) ] if vagrant_ssl_locations.any? { |f| File.exist?(f) } global_logger.debug("vagrant ssl helper found for loading ssl providers") begin require "vagrant/vagrant_ssl" Vagrant.vagrant_ssl_load global_logger.debug("ssl providers successfully loaded") rescue LoadError => err global_logger.warn("failed to load ssl providers, attempting to continue (#{err})") rescue => err global_logger.warn("unexpected failure loading ssl providers, attempting to continue (#{err})") end else global_logger.warn("vagrant ssl helper was not found, continuing...") end # We need these components always so instead of an autoload we # just require them explicitly here. require "vagrant/plugin" require "vagrant/registry" module Vagrant autoload :Action, 'vagrant/action' autoload :Alias, 'vagrant/alias' autoload :BatchAction, 'vagrant/batch_action' autoload :Box, 'vagrant/box' autoload :BoxCollection, 'vagrant/box_collection' autoload :BoxMetadata, 'vagrant/box_metadata' autoload :Bundler, 'vagrant/bundler' autoload :CLI, 'vagrant/cli' autoload :CapabilityHost, 'vagrant/capability_host' autoload :Config, 'vagrant/config' autoload :Environment, 'vagrant/environment' autoload :Errors, 'vagrant/errors' autoload :Guest, 'vagrant/guest' autoload :Host, 'vagrant/host' autoload :Machine, 'vagrant/machine' autoload :MachineIndex, 'vagrant/machine_index' autoload :MachineState, 'vagrant/machine_state' autoload :Plugin, 'vagrant/plugin' autoload :Registry, 'vagrant/registry' autoload :UI, 'vagrant/ui' autoload :Util, 'vagrant/util' autoload :Vagrantfile, 'vagrant/vagrantfile' autoload :VERSION, 'vagrant/version' # These are the various plugin versions and their components in # a lazy loaded Hash-like structure. PLUGIN_COMPONENTS = Registry.new.tap do |c| c.register(:"1") { Plugin::V1::Plugin } c.register([:"1", :command]) { Plugin::V1::Command } c.register([:"1", :communicator]) { Plugin::V1::Communicator } c.register([:"1", :config]) { Plugin::V1::Config } c.register([:"1", :guest]) { Plugin::V1::Guest } c.register([:"1", :host]) { Plugin::V1::Host } c.register([:"1", :provider]) { Plugin::V1::Provider } c.register([:"1", :provisioner]) { Plugin::V1::Provisioner } c.register(:"2") { Plugin::V2::Plugin } c.register([:"2", :command]) { Plugin::V2::Command } c.register([:"2", :communicator]) { Plugin::V2::Communicator } c.register([:"2", :config]) { Plugin::V2::Config } c.register([:"2", :guest]) { Plugin::V2::Guest } c.register([:"2", :host]) { Plugin::V2::Host } c.register([:"2", :provider]) { Plugin::V2::Provider } c.register([:"2", :provisioner]) { Plugin::V2::Provisioner } c.register([:"2", :push]) { Plugin::V2::Push } c.register([:"2", :synced_folder]) { Plugin::V2::SyncedFolder } c.register(:remote) { Plugin::Remote::Plugin } end # Configure a Vagrant environment. The version specifies the version # of the configuration that is expected by the block. The block, based # on that version, configures the environment. # # Note that the block isn't run immediately. Instead, the configuration # block is stored until later, and is run when an environment is loaded. # # @param [String] version Version of the configuration def self.configure(version, &block) Config.run(version, &block) end # This checks if a plugin with the given name is available (installed # and enabled). This can be used from the Vagrantfile to easily branch # based on plugin availability. def self.has_plugin?(name, version=nil) return false unless Vagrant.plugins_enabled? if !version # We check the plugin names first because those are cheaper to check return true if plugin("2").manager.registered.any? { |p| p.name == name } end # Now check the plugin gem names require "vagrant/plugin/manager" Plugin::Manager.instance.plugin_installed?(name, version) end # Returns a superclass to use when creating a plugin for Vagrant. # Given a specific version, this returns a proper superclass to use # to register plugins for that version. # # Optionally, if you give a specific component, then it will return # the proper superclass for that component as well. # # Plugins and plugin components should subclass the classes returned by # this method. This method lets Vagrant core control these superclasses # and change them over time without affecting plugins. For example, if # the V1 superclass happens to be "Vagrant::V1," future versions of # Vagrant may move it to "Vagrant::Plugins::V1" and plugins will not be # affected. # # @param [String] version # @param [String] component # @return [Class] def self.plugin(version, component=nil) # Build up the key and return a result key = version.to_s.to_sym key = [key, component.to_s.to_sym] if component result = PLUGIN_COMPONENTS.get(key) # If we found our component then we return that return result if result # If we didn't find a result, then raise an exception, depending # on if we got a component or not. raise ArgumentError, "Plugin superclass not found for version/component: " + "#{version} #{component}" end # @deprecated def self.require_plugin(name) puts "require_plugin is deprecated and has no effect any longer." puts "Use `vagrant plugin` commands to manage plugins. This warning will" puts "be removed in the next version of Vagrant." end # This checks if Vagrant is installed in a specific version. # # Example: # # Vagrant.version?(">= 2.1.0") # def self.version?(*requirements) req = Gem::Requirement.new(*requirements) req.satisfied_by?(Gem::Version.new(VERSION)) end # This allows a Vagrantfile to specify the version of Vagrant that is # required. You can specify a list of requirements which will all be checked # against the running Vagrant version. # # This should be specified at the _top_ of any Vagrantfile. # # Examples are shown below: # # require_version(">= 1.3.5") # require_version(">= 1.3.5", "< 1.4.0") # require_version("~> 1.3.5") # def self.require_version(*requirements) logger = Log4r::Logger.new("vagrant::root") logger.info("Version requirements from Vagrantfile: #{requirements.inspect}") if version?(*requirements) logger.info(" - Version requirements satisfied!") return end raise Errors::VagrantVersionBad, requirements: requirements.join(", "), version: VERSION end # This allows plugin developers to access the original environment before # Vagrant even ran. This is useful when shelling out, especially to other # Ruby processes. # # @return [Hash] def self.original_env {}.tap do |h| ENV.each do |k,v| if k.start_with?("VAGRANT_OLD_ENV") key = k.sub(/^VAGRANT_OLD_ENV_/, "") if !key.empty? h[key] = v end end end end end end # Default I18n to load the en locale I18n.load_path << File.expand_path("templates/locales/en.yml", Vagrant.source_root) if I18n.config.respond_to?(:enforce_available_locales=) # Make sure only available locales are used. This will be the default in the # future but we need this to silence a deprecation warning from 0.6.9 I18n.config.enforce_available_locales = true end if Vagrant.enable_resolv_replace global_logger.info("resolv replacement has been enabled!") else global_logger.warn("resolv replacement has not been enabled!") end # A lambda that knows how to load plugins from a single directory. plugin_load_proc = lambda do |directory| # We only care about directories next false if !directory.directory? # If there is a plugin file in the top-level directory, then load # that up. plugin_file = directory.join("plugin.rb") if plugin_file.file? global_logger.debug("Loading core plugin: #{plugin_file}") load(plugin_file) next true end end # Go through the `plugins` directory and attempt to load any plugins. The # plugins are allowed to be in a directory in `plugins` or at most one # directory deep within the plugins directory. So a plugin can be at # `plugins/foo` or also at `plugins/foo/bar`, but no deeper. Vagrant.source_root.join("plugins").children(true).each do |directory| # Ignore non-directories next if !directory.directory? # Load from this directory, and exit if we successfully loaded a plugin next if plugin_load_proc.call(directory) # Otherwise, attempt to load from sub-directories directory.children(true).each(&plugin_load_proc) end ================================================ FILE: plugins/README.md ================================================ # Vagrant Core Plugins These are plugins that ship with Vagrant. Vagrant core uses its own plugin system to power a lot of the core pieces that ship with Vagrant. Each plugin will have its own README which explains its specific role. ## Generate proto ``` grpc_tools_ruby_protoc -I . --ruby_out=gen/plugin --grpc_out=gen/plugin ./plugin_server.proto ``` ================================================ FILE: plugins/commands/autocomplete/command/install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require 'vagrant/util/install_cli_autocomplete' module VagrantPlugins module CommandAutocomplete module Command class Install < Vagrant.plugin("2", :command) def execute options = { shells: [] } opts = OptionParser.new do |o| o.banner = "Usage: vagrant autocomplete install [-h] [shell name]" o.separator "" o.separator "Available shells: #{Vagrant::Util::InstallCLIAutocomplete::SUPPORTED_SHELLS.keys.join(' ')}" o.separator "" o.separator "Options:" o.separator "" o.on("-b", "--bash", "Install bash autocomplete") do |c| options[:shells].append("bash") end o.on("-z", "--zsh", "Install zsh autocomplete") do |c| options[:shells].append("zsh") end end # Parse the options argv = parse_options(opts) return if !argv raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length > 0 written_paths = Vagrant::Util::InstallCLIAutocomplete.install(options[:shells]) if written_paths && written_paths.length > 0 @env.ui.info(I18n.t("vagrant.autocomplete.installed", paths: written_paths.join("\n- "))) else @env.ui.info(I18n.t("vagrant.autocomplete.not_installed")) end # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/autocomplete/command/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "optparse" require 'vagrant/util/install_cli_autocomplete' module VagrantPlugins module CommandAutocomplete module Command class Root < Vagrant.plugin("2", :command) def self.synopsis "manages autocomplete installation on host" end def initialize(argv, env) super @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) @subcommands = Vagrant::Registry.new @subcommands.register(:install) do require File.expand_path("../install", __FILE__) Install end end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Print the help for all the box 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 def help opts = OptionParser.new do |opts| opts.banner = "Usage: vagrant autocomplete " opts.separator "" opts.separator "Available subcommands:" # Add the available subcommands as separators in order to print them # out as well. keys = [] keys = @subcommands.keys.map(&:to_s) keys.sort.each do |key| opts.separator " #{key}" end opts.separator "" opts.separator "For help on any individual subcommand run `vagrant autocomplete -h`" end @env.ui.info(opts.help, prefix: false) end end end end end ================================================ FILE: plugins/commands/autocomplete/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandAutocomplete class Plugin < Vagrant.plugin("2") name "autocomplete command" description <<-DESC The `autocomplete` manipulates Vagrant the autocomplete feature. DESC command("autocomplete") do require File.expand_path("../command/root", __FILE__) Command::Root end end end end ================================================ FILE: plugins/commands/box/command/add.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require_relative 'download_mixins' module VagrantPlugins module CommandBox module Command class Add < Vagrant.plugin("2", :command) include DownloadMixins def execute options = { architecture: :auto, } opts = OptionParser.new do |o| o.banner = "Usage: vagrant box add [options] " o.separator "" o.separator "Options:" o.separator "" o.on("-c", "--clean", "Clean any temporary download files") do |c| options[:clean] = c end o.on("-f", "--force", "Overwrite an existing box if it exists") do |f| options[:force] = f end build_download_options(o, options) o.on("--location-trusted", "Trust 'Location' header from HTTP redirects and use the same credentials for subsequent urls as for the initial one") do |l| options[:location_trusted] = l end o.on("-a", "--architecture ARCH", String, "Architecture the box should satisfy") do |a| options[:architecture] = a end o.on("--provider PROVIDER", String, "Provider the box should satisfy") do |p| options[:provider] = p end o.on("--box-version VERSION", String, "Constrain version of the added box") do |v| options[:version] = v end o.separator "" o.separator "The box descriptor can be the name of a box on HashiCorp's Vagrant Cloud," o.separator "or a URL, or a local .box file, or a local .json file containing" o.separator "the catalog metadata." o.separator "" o.separator "The options below only apply if you're adding a box file directly," o.separator "and not using a Vagrant server or a box structured like 'user/box':" o.separator "" o.on("--checksum CHECKSUM", String, "Checksum for the box") do |c| options[:checksum] = c end o.on("--checksum-type TYPE", String, "Checksum type (md5, sha1, sha256)") do |c| options[:checksum_type] = c.to_sym end o.on("--name BOX", String, "Name of the box") do |n| options[:name] = n end 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 url = argv[0] if argv.length == 2 options[:name] = argv[0] url = argv[1] end @env.action_runner.run(Vagrant::Action.action_box_add, { box_url: url, box_name: options[:name], box_provider: options[:provider], box_architecture: options[:architecture], box_version: options[:version], box_checksum_type: options[:checksum_type], box_checksum: options[:checksum], box_clean: options[:clean], 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_download_insecure: options[:insecure], box_download_location_trusted: options[:location_trusted], ui: Vagrant::UI::Prefixed.new(@env.ui, "box"), }) # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/box/command/download_mixins.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 ================================================ FILE: plugins/commands/box/command/list.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandBox module Command class List < Vagrant.plugin("2", :command) def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant box list [options]" o.separator "" o.separator "Options:" o.separator "" o.on("-i", "--box-info", "Displays additional information about the boxes") do |i| options[:info] = i end end # Parse the options argv = parse_options(opts) return if !argv boxes = @env.boxes.all if boxes.empty? @env.ui.warn(I18n.t("vagrant.commands.box.no_installed_boxes"), prefix: false) return 0 end list_boxes(boxes, options[:info]) # Success, exit status 0 0 end private def list_boxes(boxes, extra_info) # Find the longest box name longest_box = boxes.max_by { |x| x[0].length } longest_box_length = longest_box[0].length # Group boxes by name and version and start iterating boxes.group_by { |b| [b[0], b[1]] }.each do |box_info, box_data| name, version = box_info # Now group by provider so we can collect common architectures box_data.group_by { |b| b[2] }.each do |provider, data| architectures = data.map { |d| d.last }.compact.sort.uniq meta_info = [provider, version] if !architectures.empty? meta_info << "(#{architectures.join(", ")})" end @env.ui.info("#{name.ljust(longest_box_length)} (#{meta_info.join(", ")})") data.each do |arch_info| @env.ui.machine("box-name", name) @env.ui.machine("box-provider", provider) @env.ui.machine("box-version", version) @env.ui.machine("box-architecture", arch_info.last || "n/a") info_file = @env.boxes.find(name, provider, version, arch_info.last). directory.join("info.json") if info_file.file? info = JSON.parse(info_file.read) info.each do |k, v| @env.ui.machine("box-info", k, v) if extra_info @env.ui.info(" - #{k}: #{v}", prefix: false) end end end end end end end end end end end ================================================ FILE: plugins/commands/box/command/outdated.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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]" o.separator "" o.separator "Checks if there is a new version available for the box" o.separator "that you are using. If you pass in the --global flag," o.separator "all boxes will be checked for updates." o.separator "" o.separator "Options:" o.separator "" o.on("--global", "Check all boxes installed") do |g| options[:global] = g end o.on("-f", "--force", "Force checks for latest box updates") do |f| options[:force] = f end build_download_options(o, download_options) end argv = parse_options(opts) return if !argv # If we're checking the boxes globally, then do that. if options[:global] outdated_global(download_options) return 0 end with_target_vms(argv) do |machine| @env.action_runner.run(Vagrant::Action.action_box_outdated, { box_outdated_force: options[:force], box_outdated_refresh: true, box_outdated_success_ui: true, machine: machine, }.merge(download_options)) end return 0 end def outdated_global(download_options) @env.boxes.all.reverse.each do |name, version, provider| box = @env.boxes.find(name, provider, version) if !box&.metadata_url @env.ui.output(I18n.t( "vagrant.box_outdated_no_metadata", name: name, provider: provider)) next end md = nil begin md = box.load_metadata(download_options) rescue Vagrant::Errors::BoxMetadataDownloadError => e @env.ui.error(I18n.t( "vagrant.box_outdated_metadata_error", name: box.name, provider: box.provider, message: e.extra_data[:message])) next end box_versions = md.versions(provider: box.provider, architecture: box.architecture) if box_versions.empty? latest_box_version = box_versions.last.to_i else latest_box_version = box_versions.last end if !md.compatible_version_update?(box.version, latest_box_version, provider: box.provider, architecture: box.architecture) @env.ui.success(I18n.t( "vagrant.box_up_to_date", name: box.name, provider: box.provider, version: box.version)) else @env.ui.warn(I18n.t( "vagrant.box_outdated", name: box.name, provider: box.provider, current: box.version, latest: latest_box_version.to_s,)) end end end end end end end ================================================ FILE: plugins/commands/box/command/prune.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandBox module Command class Prune < Vagrant.plugin("2", :command) def execute options = {} options[:force] = false options[:dry_run] = false opts = OptionParser.new do |o| o.banner = "Usage: vagrant box prune [options]" o.separator "" o.separator "Options:" o.separator "" o.on("-p PROVIDER", "--provider PROVIDER", String, "The specific provider type for the boxes to destroy.") do |p| options[:provider] = p end o.on("-n", "--dry-run", "Only print the boxes that would be removed.") do |f| options[:dry_run] = f end o.on("--name NAME", String, "The specific box name to check for outdated versions.") do |name| options[:name] = name end o.on("-f", "--force", "Destroy without confirmation even when box is in use.") do |f| options[:force] = f end o.on("-k", "--keep-active-boxes", "When combined with `--force`, will keep boxes still actively in use.") do |k| options[:keep] = k end end # Parse the options argv = parse_options(opts) return if !argv boxes = @env.boxes.all.sort if boxes.empty? return @env.ui.warn(I18n.t("vagrant.commands.box.no_installed_boxes"), prefix: false) end delete_oldest_boxes(boxes, options[:provider], options[:force], options[:name], options[:dry_run], options[:keep]) # Success, exit status 0 0 end private def delete_oldest_boxes(boxes, only_provider, skip_confirm, only_name, dry_run, keep_used_boxes) # Find the longest box name longest_box = boxes.max_by { |x| x[0].length } longest_box_length = longest_box[0].length # Hash map to keep track of newest versions newest_boxes = Hash.new # First find the newest version for every installed box boxes.each do |name, version, provider| next if only_provider and only_provider != provider.to_s next if only_name and only_name != name # Nested to make sure it works for boxes with different providers if newest_boxes.has_key?(name) if newest_boxes[name].has_key?(provider) saved = Gem::Version.new(newest_boxes[name][provider]) current = Gem::Version.new(version) if current > saved newest_boxes[name][provider] = version end else newest_boxes[name][provider] = version end else newest_boxes[name] = Hash.new newest_boxes[name][provider] = version end end @env.ui.info("The following boxes will be kept..."); newest_boxes.each do |name, providers| providers.each do |provider, version| @env.ui.info("#{name.ljust(longest_box_length)} (#{provider}, #{version})") @env.ui.machine("box-name", name) @env.ui.machine("box-provider", provider) @env.ui.machine("box-version", version) end end @env.ui.info("", prefix: false) @env.ui.info("Checking for older boxes..."); # Track if we removed anything so the user can be informed removed_any_box = false boxes.each do |name, version, provider| next if !newest_boxes.has_key?(name) or !newest_boxes[name].has_key?(provider) current = Gem::Version.new(version) saved = Gem::Version.new(newest_boxes[name][provider]) if current < saved removed_any_box = true # Use the remove box action if dry_run @env.ui.info("Would remove #{name} #{provider} #{version}") else @env.action_runner.run(Vagrant::Action.action_box_remove, { box_name: name, box_provider: provider, box_version: version, force_confirm_box_remove: skip_confirm, keep_used_boxes: keep_used_boxes, box_remove_all_versions: false, }) end end end if !removed_any_box @env.ui.info("No old versions of boxes to remove..."); end end end end end end ================================================ FILE: plugins/commands/box/command/remove.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandBox module Command class Remove < Vagrant.plugin("2", :command) def execute options = {} options[:force] = false opts = OptionParser.new do |o| o.banner = "Usage: vagrant box remove " o.separator "" o.separator "Options:" o.separator "" o.on("-f", "--force", "Remove without confirmation.") do |f| options[:force] = f end o.on("-a", "--architecture ARCH", String, "The specific architecture for the box to remove") do |a| options[:architecture] = a end o.on("--provider PROVIDER", String, "The specific provider type for the box to remove") do |p| options[:provider] = p end o.on("--box-version VERSION", String, "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 o.on("--all-providers", "Remove all providers within a version of the box") do |a| options[:all_providers] = a end o.on("--all-architectures", "Remove all architectures within a provider a version of the box") do |a| options[:all_architectures] = a end 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 if argv.length == 2 # @deprecated @env.ui.warn("WARNING: The second argument to `vagrant box remove`") @env.ui.warn("is deprecated. Please use the --provider flag. This") @env.ui.warn("feature will stop working in the next version.") options[:provider] = argv[1] end @env.action_runner.run(Vagrant::Action.action_box_remove, { box_name: argv[0], box_architecture: options[:architecture], box_provider: options[:provider], box_version: options[:version], force_confirm_box_remove: options[:force], box_remove_all_versions: options[:all], box_remove_all_providers: options[:all_providers], box_remove_all_architectures: options[:all_architectures] }) # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/box/command/repackage.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require 'optparse' require "pathname" module VagrantPlugins module CommandBox module Command class Repackage < Vagrant.plugin("2", :command) def execute opts = OptionParser.new do |o| o.banner = "Usage: vagrant box repackage " end # Parse the options argv = parse_options(opts) return if !argv raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length != 3 box_name = argv[0] box_provider = argv[1].to_sym box_version = argv[2] # Verify the box exists that we want to repackage box = @env.boxes.find(box_name, box_provider, "= #{box_version}") if !box raise Vagrant::Errors::BoxNotFoundWithProviderAndVersion, name: box_name, provider: box_provider.to_s, version: box_version end # Repackage the box output_name = @env.vagrantfile.config.package.name || "package.box" output_path = Pathname.new(File.expand_path(output_name, FileUtils.pwd)) box.repackage(output_path) # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/box/command/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandBox module Command class Root < Vagrant.plugin("2", :command) def self.synopsis "manages boxes: installation, removal, etc." end def initialize(argv, env) super @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) @subcommands = Vagrant::Registry.new @subcommands.register(:add) do require File.expand_path("../add", __FILE__) Add end @subcommands.register(:list) do require File.expand_path("../list", __FILE__) List end @subcommands.register(:outdated) do require_relative "outdated" Outdated end @subcommands.register(:remove) do require File.expand_path("../remove", __FILE__) Remove end @subcommands.register(:prune) do require_relative "prune" Prune end @subcommands.register(:repackage) do require File.expand_path("../repackage", __FILE__) Repackage end @subcommands.register(:update) do require_relative "update" Update end end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Print the help for all the box 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 box []" 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 box -h`" end @env.ui.info(opts.help, prefix: false) end end end end end ================================================ FILE: plugins/commands/box/command/update.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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]" o.separator "" o.separator "Updates the box that is in use in the current Vagrant environment," o.separator "if there any updates available. This does not destroy/recreate the" o.separator "machine, so you'll have to do that to see changes." o.separator "" o.separator "To update a specific box (not tied to a Vagrant environment), use the" o.separator "--box flag." o.separator "" o.separator "Options:" o.separator "" o.on("--box BOX", String, "Update a specific box") do |b| options[:box] = b end o.on("--architecture ARCHITECTURE", String, "Update box with specific architecture") do |a| options[:architecture] = a end o.on("--provider PROVIDER", String, "Update box with specific provider") do |p| options[:provider] = p.to_sym end o.on("-f", "--force", "Overwrite an existing box if it exists") do |f| options[:force] = f end build_download_options(o, download_options) end argv = parse_options(opts) return if !argv if options[:box] update_specific(options[:box], options[:provider], options[:architecture], download_options, options[:force]) else update_vms(argv, options[:provider], download_options, options[:force]) end 0 end def update_specific(name, provider, architecture, download_options, force) box_info = Vagrant::Util::HashWithIndifferentAccess.new @env.boxes.all.each do |box_name, box_version, box_provider, box_architecture| next if name != box_name box_info[box_provider] ||= Vagrant::Util::HashWithIndifferentAccess.new box_info[box_provider][box_version] ||= [] box_info[box_provider][box_version].push(box_architecture.to_s).uniq! end if box_info.empty? raise Vagrant::Errors::BoxNotFound, name: name.to_s end if !provider if box_info.size > 1 raise Vagrant::Errors::BoxUpdateMultiProvider, name: name.to_s, providers: box_info.keys.map(&:to_s).sort.join(", ") end provider = box_info.keys.first elsif !box_info[provider] raise Vagrant::Errors::BoxNotFoundWithProvider, name: name.to_s, provider: provider.to_s, providers: box_info.keys.map(&:to_s).sort.join(", ") end version = box_info[provider].keys.sort_by{ |v| Gem::Version.new(v) }.last architecture_list = box_info[provider][version] if !architecture if architecture_list.size > 1 raise Vagrant::Errors::BoxUpdateMultiArchitecture, name: name.to_s, provider: provider.to_s, version: version.to_s, architectures: architecture_list.sort.join(", ") end architecture = architecture_list.first elsif !architecture_list.include?(architecture) raise Vagrant::Errors::BoxNotFoundWithProviderArchitecture, name: name.to_s, provider: provider.to_s, version: version.to_s, architecture: architecture, architectures: architecture_list.sort.join(", ") end # Architecture gets cast to a string when collecting information # above. Convert it back to a nil if it's empty architecture = nil if architecture.to_s.empty? box = @env.boxes.find(name, provider, version, architecture) box_update(box, "> #{version}", @env.ui, download_options, force) end def update_vms(argv, provider, download_options, force) machines = {} with_target_vms(argv, provider: provider) do |machine| if !machine.config.vm.box machine.ui.output(I18n.t( "vagrant.errors.box_update_no_name")) next end if !machine.box collection = Vagrant::BoxCollection.new(@env.boxes_path) machine.box = collection.find(machine.config.vm.box, provider || machine.provider_name || @env.default_provider, "> 0") if !machine.box machine.ui.output(I18n.t( "vagrant.errors.box_update_no_box", name: machine.config.vm.box)) next end end name = machine.box.name provider = machine.box.provider version = machine.config.vm.box_version || machine.box.version machines["#{name}_#{provider}_#{version}"] = machine end machines.each do |_, machine| box = machine.box version = machine.config.vm.box_version # 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 begin box_update(box, version, machine.ui, download_options, force) rescue Vagrant::Errors::BoxUpdateNoMetadata => e machine.ui.warn(e) next end end end def box_update(box, version, ui, download_options, force) 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}") ui.detail("Architecture: #{box.architecture.inspect}") if box.architecture update = box.has_update?(version, download_options: download_options) if !update ui.success(I18n.t( "vagrant.box_up_to_date_single", name: box.name, version: box.version)) return end ui.output(I18n.t( "vagrant.box_updating", name: update[0].name, provider: update[2].name, old: box.version, new: update[1].version)) @env.action_runner.run(Vagrant::Action.action_box_add, { box_url: box.metadata_url, box_provider: update[2].name, box_version: update[1].version, box_architecture: update[2].architecture, ui: ui, box_force: force, box_download_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 end end end ================================================ FILE: plugins/commands/box/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandBox class Plugin < Vagrant.plugin("2") name "box command" description "The `box` command gives you a way to manage boxes." command("box") do require File.expand_path("../command/root", __FILE__) Command::Root end end end end ================================================ FILE: plugins/commands/cap/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 "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 "" o.on("--check", "Only checks for a capability, does not execute") do |f| options[:check] = f end # TODO: Rename this back to `target` to maintain api o.on("-t", "--target-guest=TARGET", "Target guest to run against (if applicable)") do |t| options[:target] = t 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(options[:target] || []) 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 ================================================ FILE: plugins/commands/cap/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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_relative "command" Command end end end end ================================================ FILE: plugins/commands/cloud/auth/login.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module AuthCommand module Command class Login < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud auth login [options]" o.separator "" o.separator "Options:" o.separator "" o.on("-c", "--check", "Checks if currently logged in") do |c| options[:check] = c end o.on("-d", "--description DESCRIPTION", String, "Set description for the Vagrant Cloud token") do |d| options[:description] = d end o.on("-t", "--token TOKEN", String, "Set the Vagrant Cloud token") do |t| options[:token] = t end o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |l| options[:login] = l end end # Parse the options argv = parse_options(opts) return if !argv if !argv.empty? raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end client = Client.new(@env) client.username_or_email = options[:login] # Determine what task we're actually taking based on flags if options[:check] return execute_check(client) elsif options[:token] return execute_token(client, options[:token]) else if client.logged_in? @env.ui.success(I18n.t("cloud_command.check_logged_in")) else client_login(@env, options.slice(:login, :description)) end end 0 end def execute_check(client) if client.logged_in? @env.ui.success(I18n.t("cloud_command.check_logged_in")) return 0 else @env.ui.error(I18n.t("cloud_command.check_not_logged_in")) return 1 end end def execute_token(client, token) client.store_token(token) @env.ui.success(I18n.t("cloud_command.token_saved")) if client.logged_in? @env.ui.success(I18n.t("cloud_command.check_logged_in")) return 0 else @env.ui.error(I18n.t("cloud_command.invalid_token")) return 1 end end end end end end end ================================================ FILE: plugins/commands/cloud/auth/logout.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module AuthCommand module Command class Logout < Vagrant.plugin("2", :command) def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud auth logout" o.separator "" o.separator "Log out of Vagrant Cloud" end # Parse the options argv = parse_options(opts) return if !argv if !argv.empty? raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = Client.new(@env) @client.clear_token @env.ui.success(I18n.t("cloud_command.logged_out")) return 0 end end end end end end ================================================ FILE: plugins/commands/cloud/auth/middleware/add_authentication.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "cgi" require "uri" require "log4r" require Vagrant.source_root.join("plugins/commands/cloud/client/client") module VagrantPlugins module CloudCommand class AddAuthentication REPLACEMENT_HOSTS = [ "app.vagrantup.com".freeze, "atlas.hashicorp.com".freeze ].freeze TARGET_HOST = "vagrantcloud.com".freeze CUSTOM_HOST_NOTIFY_WAIT = 5 def self.custom_host_notified! @_host_notify = true end def self.custom_host_notified? if defined?(@_host_notify) @_host_notify else false end end def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::cloud::auth::authenticate-box-url") CloudCommand::Plugin.init! end def call(env) if ENV["VAGRANT_SERVER_ACCESS_TOKEN_BY_URL"] @logger.warn("Adding access token as GET parameter by user request") client = Client.new(env[:env]) token = client.token env[:box_urls].map! do |url| begin u = URI.parse(url) if u.host != TARGET_HOST && REPLACEMENT_HOSTS.include?(u.host) u.host = TARGET_HOST u.to_s else url end rescue URI::Error url end end server_uri = URI.parse(Vagrant.server_url.to_s) if token && !server_uri.host.to_s.empty? env[:box_urls].map! do |url| begin u = URI.parse(url) if u.host == server_uri.host if server_uri.host != TARGET_HOST && !self.class.custom_host_notified? env[:ui].warn(I18n.t("cloud_command.middleware.authentication.different_target", custom_host: server_uri.host, known_host: TARGET_HOST) + "\n") sleep CUSTOM_HOST_NOTIFY_WAIT self.class.custom_host_notified! end q = CGI.parse(u.query || "") current = q["access_token"] if current && current.empty? q["access_token"] = token end u.query = URI.encode_www_form(q) end u.to_s rescue URI::Error url end end end else env[:box_urls].map! do |url| begin u = URI.parse(url) q = CGI.parse(u.query || "") if !q["access_token"].empty? @logger.warn("Removing access token from URL parameter.") q.delete("access_token") if q.empty? u.query = nil else u.query = URI.encode_www_form(q) end u.to_s else @logger.warn("Authentication token not found as GET parameter.") url end rescue URI::Error url end end end @app.call(env) end.freeze end end end ================================================ FILE: plugins/commands/cloud/auth/middleware/add_downloader_authentication.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "cgi" require "uri" require "vagrant/util/credential_scrubber" require Vagrant.source_root.join("plugins/commands/cloud/client/client") require_relative "./add_authentication" # Similar to AddAuthentication this middleware will add authentication for interacting # with Vagrant cloud. It does this by adding Authentication headers to a # Vagrant::Util::Downloader object. module VagrantPlugins module CloudCommand class AddDownloaderAuthentication < AddAuthentication def initialize(app, env) super @logger = Log4r::Logger.new("vagrant::cloud::auth::add-download-authentication") end def call(env) if ENV["VAGRANT_SERVER_ACCESS_TOKEN_BY_URL"] @logger.warn("Authentication header not added due to user requested access token URL parameter") else client = Client.new(env[:env]) token = client.token Vagrant::Util::CredentialScrubber.sensitive(token) begin target_url = URI.parse(env[:downloader].source) if target_url.host != TARGET_HOST && REPLACEMENT_HOSTS.include?(target_url.host) target_url.host = TARGET_HOST env[:downloader].source = target_url.to_s end rescue URI::Error # if there is an error, use current target_url end server_uri = URI.parse(Vagrant.server_url.to_s) if token && !server_uri.host.to_s.empty? if target_url.host == server_uri.host if server_uri.host != TARGET_HOST && !self.class.custom_host_notified? env[:ui].warn(I18n.t("cloud_command.middleware.authentication.different_target", custom_host: server_uri.host, known_host: TARGET_HOST) + "\n") sleep CUSTOM_HOST_NOTIFY_WAIT self.class.custom_host_notified! end if Array(env[:downloader].headers).any? { |h| h.include?("Authorization") } @logger.info("Not adding an authentication header, one already found") else env[:downloader].headers << "Authorization: Bearer #{token}" end else @logger.debug("Not adding authentication header, host mismatch #{target_url.host} != #{server_uri.host}") end env[:downloader] end end @app.call(env) end.freeze end end end ================================================ FILE: plugins/commands/cloud/auth/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CloudCommand module AuthCommand class Plugin < Vagrant.plugin("2") name "vagrant cloud auth" description <<-DESC Authorization commands for Vagrant Cloud DESC command(:auth) do require_relative "root" Command::Root end end end end end ================================================ FILE: plugins/commands/cloud/auth/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CloudCommand module AuthCommand module Command class Root < Vagrant.plugin("2", :command) def self.synopsis "Manages Vagrant Cloud authorization related to Vagrant Cloud" end def initialize(argv, env) super @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) @subcommands = Vagrant::Registry.new @subcommands.register(:login) do require File.expand_path("../login", __FILE__) Command::Login end @subcommands.register(:logout) do require File.expand_path("../logout", __FILE__) Command::Logout end @subcommands.register(:whoami) do require File.expand_path("../whoami", __FILE__) Command::Whoami end end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Print the help for all the box 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 cloud auth []" opts.separator "" opts.separator "Authorization with Vagrant Cloud" 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 cloud auth -h`" end @env.ui.info(opts.help, prefix: false) end end end end end end ================================================ FILE: plugins/commands/cloud/auth/whoami.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module AuthCommand module Command class Whoami < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud auth whoami [token]" o.separator "" o.separator "Display currently logged in user" end # Parse the options argv = parse_options(opts) return if !argv if argv.size > 1 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end if argv.first token = argv.first else client = Client.new(@env) token = client.token end whoami(token) end def whoami(access_token) if access_token.to_s.empty? @env.ui.error(I18n.t("cloud_command.check_not_logged_in")) return 1 end begin account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) @env.ui.success("Currently logged in as #{account.username}") return 0 rescue VagrantCloud::Error::ClientError => e @env.ui.error(I18n.t("cloud_command.errors.whoami.read_error")) @env.ui.error(e) return 1 end return 1 end end end end end end ================================================ FILE: plugins/commands/cloud/box/create.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module BoxCommand module Command class Create < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud box create [options] organization/box-name" o.separator "" o.separator "Creates an empty box entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-d", "--description DESCRIPTION", String, "Full description of the box") do |d| options[:description] = d end o.on("-s", "--short-description DESCRIPTION", String, "Short description of the box") do |s| options[:short] = s end o.on("-p", "--[no-]private", "Makes box private") do |p| options[:private] = p end end # Parse the options argv = parse_options(opts) return if !argv if argv.empty? || argv.length > 1 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = client_login(@env) org, box_name = argv.first.split('/', 2) create_box(org, box_name, @client.token, options.slice(:description, :short, :private)) end # Create a new box # # @param [String] org Organization name of box # @param [String] box_name Name of box # @param [String] access_token User access token # @param [Hash] options Options for box filtering # @option options [String] :short Short description of box # @option options [String] :description Full description of box # @option options [Boolean] :private Set box visibility as private # @return [Integer] def create_box(org, box_name, access_token, options={}) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) box = account.organization(name: org).add_box(box_name) box.short_description = options[:short] if options.key?(:short) box.description = options[:description] if options.key?(:description) box.private = options[:private] if options.key?(:private) box.save @env.ui.success(I18n.t("cloud_command.box.create_success", org: org, box_name: box_name)) format_box_results(box, @env) 0 rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.box.create_fail", org: org, box_name: box_name)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/box/delete.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module BoxCommand module Command class Delete < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud box delete [options] organization/box-name" o.separator "" o.separator "Deletes box entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-f", "--[no-]force", "Do not prompt for deletion confirmation") do |f| options[:force] = f end end # Parse the options argv = parse_options(opts) return if !argv if argv.empty? || argv.length > 1 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end if !options[:force] @env.ui.warn(I18n.t("cloud_command.box.delete_warn", box: argv.first)) cont = @env.ui.ask(I18n.t("cloud_command.continue")) return 1 if cont.strip.downcase != "y" end @client = client_login(@env) org, box_name = argv.first.split('/', 2) delete_box(org, box_name, @client.token) end # Delete the requested box # # @param [String] org Organization name of box # @param [String] box_name Name of box # @param [String] access_token User access token # @return [Integer] def delete_box(org, box_name, access_token) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_box(account: account, org: org, box: box_name) do |box| box.delete @env.ui.success(I18n.t("cloud_command.box.delete_success", org: org, box_name: box_name)) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.box.delete_fail", org: org, box_name: box_name)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/box/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CloudCommand module BoxCommand class Plugin < Vagrant.plugin("2") name "vagrant cloud box" description <<-DESC Box life cycle commands for Vagrant Cloud DESC command(:box) do require_relative "root" Command::Root end end end end end ================================================ FILE: plugins/commands/cloud/box/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CloudCommand module BoxCommand module Command class Root < Vagrant.plugin("2", :command) def self.synopsis "Commands to manage boxes on Vagrant Cloud" end def initialize(argv, env) super @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) @subcommands = Vagrant::Registry.new @subcommands.register(:create) do require File.expand_path("../create", __FILE__) Command::Create end @subcommands.register(:delete) do require File.expand_path("../delete", __FILE__) Command::Delete end @subcommands.register(:show) do require File.expand_path("../show", __FILE__) Command::Show end @subcommands.register(:update) do require File.expand_path("../update", __FILE__) Command::Update end end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Print the help for all the box 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 cloud box []" opts.separator "" opts.separator "Commands to manage boxes on Vagrant Cloud" 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 cloud box -h`" end @env.ui.info(opts.help, prefix: false) end end end end end end ================================================ FILE: plugins/commands/cloud/box/show.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module BoxCommand module Command class Show < Vagrant.plugin("2", :command) include Util def execute options = { architectures: [], providers: [], quiet: true, versions: [], } opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud box show [options] organization/box-name" o.separator "" o.separator "Displays a boxes attributes on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("--architectures ARCH", String, "Filter results by architecture support (can be defined multiple times)") do |a| options[:architectures].push(a).uniq! end o.on("--versions VERSION", String, "Display box information for a specific version (can be defined multiple times)") do |v| options[:versions].push(v).uniq! end o.on("--providers PROVIDER", String, "Filter results by provider support (can be defined multiple times)") do |pv| options[:providers].push(pv).uniq! end o.on("--[no-]auth", "Authenticate with Vagrant Cloud if required before searching") do |l| options[:quiet] = !l end end # Parse the options argv = parse_options(opts) return if !argv if argv.empty? || argv.length > 1 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = client_login(@env, options.slice(:quiet)) org, box_name = argv.first.split('/', 2) show_box(org, box_name, @client&.token, options.slice(:architectures, :providers, :versions)) end # Display the requested box to the user # # @param [String] org Organization name of box # @param [String] box_name Name of box # @param [String] access_token User access token # @param [Hash] options Options for box filtering # @option options [String] :versions Specific verisons of box # @return [Integer] def show_box(org, box_name, access_token, options={}) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_box(account: account, org: org, box: box_name) do |box| list = [box] # If specific version(s) provided, filter out the version list = list.first.versions.find_all { |v| options[:versions].include?(v.version) } if !Array(options[:versions]).empty? # If specific provider(s) provided, filter out the provider(s) list = list.find_all { |item| if item.is_a?(VagrantCloud::Box) item.versions.any? { |v| v.providers.any? { |p| options[:providers].include?(p.name) } } else item.providers.any? { |p| options[:providers].include?(p.name) } end } if !Array(options[:providers]).empty? list = list.find_all { |item| if item.is_a?(VagrantCloud::Box) item.versions.any? { |v| v.providers.any? { |p| options[:architectures].include?(p.architecture) } } else item.providers.any? { |p| options[:architectures].include?(p.architecture) } end } if !Array(options[:architectures]).empty? if !list.empty? list.each do |b| format_box_results(b, @env, options.slice(:providers, :architectures)) @env.ui.output("") end 0 else @env.ui.warn(I18n.t("cloud_command.box.show_filter_empty", org: org, box_name: box_name, architectures: Array(options[:architectures]).empty? ? "N/A" : Array(options[:architectures]).join(", "), providers: Array(options[:providers]).empty? ? "N/A" : Array(options[:providers]).join(", "), versions: Array(options[:versions]).empty? ? "N/A" : Array(options[:versions]).join(", ") )) 1 end end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.box.show_fail", org: org, box_name:box_name)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/box/update.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module BoxCommand module Command class Update < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud box update [options] organization/box-name" o.separator "" o.separator "Updates a box entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-d", "--description DESCRIPTION", "Full description of the box") do |d| options[:description] = d end o.on("-s", "--short-description DESCRIPTION", "Short description of the box") do |s| options[:short] = s end o.on("-p", "--[no-]private", "Makes box private") do |p| options[:private] = p end end # Parse the options argv = parse_options(opts) return if !argv if argv.empty? || argv.length > 1 || options.slice(:description, :short, :private).length == 0 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = client_login(@env) org, box_name = argv.first.split('/', 2) update_box(org, box_name, @client.token, options.slice(:short, :description, :private)) end # Update an existing box # # @param [String] org Organization name of box # @param [String] box_name Name of box # @param [String] access_token User access token # @param [Hash] options Options for box filtering # @option options [String] :short Short description of box # @option options [String] :description Full description of box # @option options [Boolean] :private Set box visibility as private # @return [Integer] def update_box(org, box_name, access_token, options={}) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_box(account: account, org: org, box: box_name) do |box| box.short_description = options[:short] if options.key?(:short) box.description = options[:description] if options.key?(:description) box.private = options[:private] if options.key?(:private) box.save @env.ui.success(I18n.t("cloud_command.box.update_success", org: org, box_name: box_name)) format_box_results(box, @env) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.box.update_fail", org: org, box_name: box_name)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/client/client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant_cloud" require "vagrant/util/downloader" require "vagrant/util/presence" require Vagrant.source_root.join("plugins/commands/cloud/errors") module VagrantPlugins module CloudCommand class Client # @private # Reset the cached values for scrubber. This is not considered a public # API and should only be used for testing. def self.reset! class_variables.each(&method(:remove_class_variable)) end ###################################################################### # Class that deals with managing users 'local' token for Vagrant Cloud ###################################################################### APP = "app".freeze include Util include Vagrant::Util::Presence attr_accessor :client attr_accessor :username_or_email attr_accessor :password attr_reader :two_factor_default_delivery_method attr_reader :two_factor_delivery_methods # Initializes a login client with the given Vagrant::Environment. # # @param [Vagrant::Environment] env def initialize(env) @logger = Log4r::Logger.new("vagrant::cloud::client") @env = env if !defined?(@@client) @@client = VagrantCloud::Client.new( access_token: token, url_base: api_server_url ) end @client = @@client end # Removes the token, effectively logging the user out. def clear_token @logger.info("Clearing token") token_path.delete if token_path.file? end # Checks if the user is logged in by verifying their authentication # token. # # @return [Boolean] def logged_in? return false if !client&.access_token Vagrant::Util::CredentialScrubber.sensitive(client.access_token) with_error_handling do client.authentication_token_validate true end rescue Errors::Unauthorized false end # Login logs a user in and returns the token for that user. The token # is _not_ stored unless {#store_token} is called. # # @param [String] description # @param [String] code # @return [String] token The access token, or nil if auth failed. def login(description: nil, code: nil) @logger.info("Logging in '#{username_or_email}'") Vagrant::Util::CredentialScrubber.sensitive(password) with_error_handling do r = client.authentication_token_create(username: username_or_email, password: password, description: description, code: code) Vagrant::Util::CredentialScrubber.sensitive(r[:token]) @client = VagrantCloud::Client.new( access_token: r[:token], url_base: api_server_url ) r[:token] end end # Requests a 2FA code # @param [String] delivery_method def request_code(delivery_method) @env.ui.warn("Requesting 2FA code via #{delivery_method.upcase}...") Vagrant::Util::CredentialScrubber.sensitive(password) with_error_handling do r = client.authentication_request_2fa_code( username: username_or_email, password: password, delivery_method: delivery_method) two_factor = r[:two_factor] obfuscated_destination = two_factor[:obfuscated_destination] @env.ui.success("2FA code sent to #{obfuscated_destination}.") end end # Stores the given token locally, removing any previous tokens. # # @param [String] token def store_token(token) Vagrant::Util::CredentialScrubber.sensitive(token) @logger.info("Storing token in #{token_path}") token_path.open("w") do |f| f.write(token) end # Reset after we store the token since this is now _our_ token @client = VagrantCloud::Client.new(access_token: token, url_base: api_server_url) nil end # Reads the access token if there is one. This will first read the # `VAGRANT_CLOUD_TOKEN` environment variable and then fallback to the stored # access token on disk. # # @return [String] def token # If the client is defined, use the client for the access token # to allow proper token generation if required return client.access_token if client && !client.access_token.nil? if present?(ENV["VAGRANT_CLOUD_TOKEN"]) && token_path.exist? # Only show warning if it has not been previously shown if !defined?(@@double_token_warning) @env.ui.warn <<-EOH.strip Vagrant detected both the VAGRANT_CLOUD_TOKEN environment variable and a Vagrant login token are present on this system. The VAGRANT_CLOUD_TOKEN environment variable takes precedence over the locally stored token. To remove this error, either unset the VAGRANT_CLOUD_TOKEN environment variable or remove the login token stored on disk: ~/.vagrant.d/data/vagrant_login_token EOH @@double_token_warning = true end end if present?(ENV["VAGRANT_CLOUD_TOKEN"]) @logger.debug("Using authentication token from environment variable") t = ENV["VAGRANT_CLOUD_TOKEN"] elsif token_path.exist? @logger.debug("Using authentication token from disk at #{token_path}") t = token_path.read.strip elsif present?(ENV["ATLAS_TOKEN"]) @logger.warn("ATLAS_TOKEN detected within environment. Using ATLAS_TOKEN in place of VAGRANT_CLOUD_TOKEN.") t = ENV["ATLAS_TOKEN"] end if !t.nil? Vagrant::Util::CredentialScrubber.sensitive(t) return t end @logger.debug("No authentication token in environment or #{token_path}") nil end protected def with_error_handling(&block) yield rescue VagrantCloud::Error::ClientError => e @logger.debug("vagrantcloud request error:") @logger.debug(e.message) @logger.debug(e.backtrace.join("\n")) raise Errors::Unexpected, error: e.message rescue Excon::Error::Unauthorized @logger.debug("Unauthorized!") raise Errors::Unauthorized rescue Excon::Error::BadRequest => e @logger.debug("Bad request:") @logger.debug(e.message) @logger.debug(e.backtrace.join("\n")) parsed_response = JSON.parse(e.response.body) errors = parsed_response["errors"].join("\n") raise Errors::ServerError, errors: errors rescue Excon::Error::NotAcceptable => e @logger.debug("Got unacceptable response:") @logger.debug(e.message) @logger.debug(e.backtrace.join("\n")) parsed_response = JSON.parse(e.response.body) if two_factor = parsed_response['two_factor'] store_two_factor_information two_factor if two_factor_default_delivery_method != APP request_code two_factor_default_delivery_method end raise Errors::TwoFactorRequired end begin errors = parsed_response["errors"].join("\n") raise Errors::ServerError, errors: errors rescue JSON::ParserError; end @logger.debug("Got an unexpected error:") @logger.debug(e.inspect) raise Errors::Unexpected, error: e.inspect rescue SocketError @logger.info("Socket error") raise Errors::ServerUnreachable, url: Vagrant.server_url.to_s end def token_path @env.data_dir.join("vagrant_login_token") end def store_two_factor_information(two_factor) @two_factor_default_delivery_method = two_factor['default_delivery_method'] @two_factor_delivery_methods = two_factor['delivery_methods'] @env.ui.warn "2FA is enabled for your account." if two_factor_default_delivery_method == APP @env.ui.info "Enter the code from your authenticator." else @env.ui.info "Default method is " \ "'#{two_factor_default_delivery_method}'." end other_delivery_methods = two_factor_delivery_methods - [APP] if other_delivery_methods.any? other_delivery_methods_sentence = other_delivery_methods .map { |word| "'#{word}'" } .join(' or ') @env.ui.info "You can also type #{other_delivery_methods_sentence} " \ "to request a new code." end end end end end ================================================ FILE: plugins/commands/cloud/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CloudCommand module Errors class Error < Vagrant::Errors::VagrantError error_namespace("cloud_command.errors") end class ServerError < Error error_key(:server_error) end class ServerUnreachable < Error error_key(:server_unreachable) end class Unauthorized < Error error_key(:unauthorized) end class Unexpected < Error error_key(:unexpected_error) end class TwoFactorRequired < Error end end end end ================================================ FILE: plugins/commands/cloud/list.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module Command class List < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud list [options] organization" o.separator "" o.separator "Search for boxes managed by a specific user/organization" o.separator "" o.separator "Options:" o.separator "" o.on("-j", "--json", "Formats results in JSON") do |j| options[:check] = j end o.on("-l", "--limit", Integer, "Max number of search results (default is 25)") do |l| options[:check] = l end o.on("-p", "--provider", "Comma separated list of providers to filter search. Defaults to all.") do |p| options[:check] = p end o.on("-s", "--sort-by", "Column to sort list (created, downloads, updated)") do |s| options[:check] = s end end # Parse the options argv = parse_options(opts) return if !argv if argv.length > 1 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = client_login(@env) # TODO: This endpoint is not implemented yet 0 end end end end end ================================================ FILE: plugins/commands/cloud/locales/en.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: cloud_command: middleware: authentication: different_target: |- Vagrant has detected a custom Vagrant server in use for downloading box files. An authentication token is currently set which will be added to the box request. If the custom Vagrant server should not be receiving the authentication token, please unset it. Known Vagrant server: %{known_host} Custom Vagrant server: %{custom_host} Press ctrl-c to cancel... publish: box_save: Saving box information... upload_provider: Uploading provider with file %{file} release: Releasing box... complete: Complete! Published %{org}/%{box_name} confirm: warn: |- You are about to publish a box on Vagrant Cloud with the following options: box: |- %{org}/%{box_name}: (v%{version}) for provider '%{provider_name}' private: |- Private: true release: |- Automatic Release: true box_url: |- Remote Box file: %{url} box_description: |- Box Description: %{description} box_short_desc: |- Box Short Description: %{short_description} checksum_type: |- Checksum Type: %{checksum_type} checksum_value: |- Checksum Value: %{checksum_value} architecture: |- Box Architecture: %{architecture} default_architecture: |- Default Architecture: true version_desc: |- Version Description: %{version_description} continue: |- Do you wish to continue? [y/N] box: show_filter_empty: |- No matches found for %{org}/%{box_name} Filters applied: Architectures: %{architectures} Providers: %{providers} Versions: %{versions} create_success: |- Created box %{org}/%{box_name} delete_success: |- Deleted box %{org}/%{box_name} delete_warn: |- This will completely remove %{box} from Vagrant Cloud. This cannot be undone. update_success: |- Updated box %{org}/%{box_name} not_found: |- Failed to locate requested box: %{org}/%{box_name} search: no_results: |- No results found for `%{query}` upload: no_url: |- No URL was provided to upload the provider You will need to run the `vagrant cloud provider upload` command to provide a box provider: upload: |- Uploading box file for '%{org}/%{box_name}' (v%{version}) for provider: '%{provider}' upload_success: |- Uploaded provider %{provider} on %{org}/%{box_name} for version %{version} delete_multiple_architectures: |- Multiple architectures detected for %{provider} on %{org}/%{box_name}: delete_architectures_prompt: |- Please enter the architecture name to delete: delete_warn: |- This will completely remove provider %{provider} with architecture %{architecture} on version %{version} from %{box} on Vagrant Cloud. This cannot be undone. create_success: |- Created provider %{provider} with %{architecture} architecture on %{org}/%{box_name} for version %{version} delete_success: |- Deleted provider %{provider} with %{architecture} architecture on %{org}/%{box_name} for version %{version} update_success: |- Updated provider %{provider} on %{org}/%{box_name} for version %{version} not_found: |- Failed to locate %{provider_name} provider for %{org}/%{box_name} on version %{version} direct_disable: |- Vagrant is automatically disabling direct upload to backend storage. Uploads directly to backend storage are currently only supported for files 5G in size or smaller. Box file to upload is: %{size} version: create_success: |- Created version %{version} on %{org}/%{box_name} for version %{version} delete_success: |- Deleted version %{version} on %{org}/%{box_name} release_success: |- Released version %{version} on %{org}/%{box_name} revoke_success: |- Revoked version %{version} on %{org}/%{box_name} update_success: |- Updated version %{version} on %{org}/%{box_name} revoke_warn: |- This will revoke version %{version} from %{box} from Vagrant Cloud. This cannot be undone. release_warn: |- This will release version %{version} from %{box} to Vagrant Cloud and be available to download. delete_warn: |- This will completely remove version %{version} from %{box} from Vagrant Cloud. This cannot be undone. not_found: |- Failed to locate version %{version} for %{org}/%{box_name} errors: search: fail: |- Could not complete search request publish: fail: |- Failed to create box %{org}/%{box_name} box: create_fail: |- Failed to create box %{org}/%{box_name} delete_fail: |- Failed to delete box %{org}/%{box_name} show_fail: |- Could not get information about box %{org}/%{box_name} update_fail: |- Failed to update box %{org}/%{box_name} whoami: read_error: |- Failed to locate account information provider: create_fail: |- Failed to create '%{architecture}' variant of %{provider} provider for version %{version} of the %{org}/%{box_name} box update_fail: |- Failed to update '%{architecture}' variant of %{provider} provider for version %{version} of the %{org}/%{box_name} box delete_fail: |- Failed to delete '%{architecture}' variant of %{provider} provider for version %{version} of the %{org}/%{box_name} box upload_fail: |- Failed to upload '%{architecture}' variant of %{provider} provider for version %{version} of the %{org}/%{box_name} box version: create_fail: |- Failed to create version %{version} on box %{org}/%{box_name} delete_fail: |- Failed to delete version %{version} on box %{org}/%{box_name} release_fail: |- Failed to release version %{version} on box %{org}/%{box_name} revoke_fail: |- Failed to revoke version %{version} on box %{org}/%{box_name} update_fail: |- Failed to update version %{version} on box %{org}/%{box_name} server_error: |- The Vagrant Cloud server responded with a not-OK response: %{errors} server_unreachable: |- The Vagrant Cloud server is not currently accepting connections. Please check your network connection and try again later. unauthorized: |- Invalid username or password. Please try again. unexpected_error: |- An unexpected error occurred: %{error} check_logged_in: |- You are already logged in. check_not_logged_in: |- You are not currently logged in. Please run `vagrant login` and provide your login information to authenticate. command_header: |- In a moment we will ask for your username and password to HashiCorp's Vagrant Cloud. After authenticating, we will store an access token locally on disk. Your login details will be transmitted over a secure connection, and are never stored on disk locally. If you do not have an Vagrant Cloud account, sign up at https://www.vagrantcloud.com invalid_login: |- Invalid username or password. Please try again. invalid_token: |- Invalid token. Please try again. logged_in: |- You are now logged in. logged_out: |- You are logged out. token_saved: |- The token was successfully saved. ================================================ FILE: plugins/commands/cloud/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant_cloud' require Vagrant.source_root.join("plugins/commands/cloud/util") require Vagrant.source_root.join("plugins/commands/cloud/client/client") module VagrantPlugins module CloudCommand class Plugin < Vagrant.plugin("2") name "vagrant-cloud" description <<-DESC Provides the cloud command and internal API access to Vagrant Cloud. DESC command(:cloud) do require_relative "root" init! Command::Root end action_hook(:cloud_authenticated_boxes, :authenticate_box_url) do |hook| require_relative "auth/middleware/add_authentication" hook.prepend(AddAuthentication) end action_hook(:cloud_authenticated_boxes, :authenticate_box_downloader) do |hook| require_relative "auth/middleware/add_downloader_authentication" hook.prepend(AddDownloaderAuthentication) end protected def self.init! # Set this to match Vagant logging level so we get # desired request/response information within the # logger output ENV["VAGRANT_CLOUD_LOG"] = Vagrant.log_level return if defined?(@_init) I18n.load_path << File.expand_path("../locales/en.yml", __FILE__) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/commands/cloud/provider/create.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module ProviderCommand module Command class Create < Vagrant.plugin("2", :command) include Util def execute options = { architecture: Vagrant::Util::Platform.architecture, } opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud provider create [options] organization/box-name provider-name version [url]" o.separator "" o.separator "Creates a provider entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-a", "--architecture ARCH", String, "Architecture of guest box (defaults to current host architecture)") do |a| options[:architecture] = a end o.on("-c", "--checksum CHECKSUM_VALUE", String, "Checksum of the box for this provider. --checksum-type option is required.") do |c| options[:checksum] = c end o.on("-C", "--checksum-type TYPE", String, "Type of checksum used (md5, sha1, sha256, sha384, sha512). --checksum option is required.") do |c| options[:checksum_type] = c end o.on("--[no-]default-architecture", "Mark as default architecture for specific provider") do |d| options[:default_architecture] = d end end # Parse the options argv = parse_options(opts) return if !argv if argv.count < 3 || argv.count > 4 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = client_login(@env) org, box_name = argv.first.split('/', 2) provider_name = argv[1] version = argv[2] url = argv[3] create_provider(org, box_name, version, provider_name, url, @client.token, options) end # Create a provider for the box version # # @param [String] org Organization name # @param [String] box Box name # @param [String] version Box version # @param [String] provider Provider name # @param [String] url Provider asset URL # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options # @option options [String] :architecture Architecture of guest box # @option options [String] :checksum Checksum of the box asset # @option options [String] :checksum_type Type of the checksum # @option options [Boolean] :default_architecture Default architecture for named provider # @return [Integer] def create_provider(org, box, version, provider, url, access_token, options={}) if !url @env.ui.warn(I18n.t("cloud_command.upload.no_url")) end account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_version(account: account, org: org, box: box, version: version) do |version| provider = version.add_provider(provider, options[:architecture]) provider.checksum = options[:checksum] if options.key?(:checksum) provider.checksum_type = options[:checksum_type] if options.key?(:checksum_type) provider.architecture = options[:architecture] if options.key?(:architecture) provider.default_architecture = options[:default_architecture] if options.key?(:default_architecture) provider.url = url if url provider.save @env.ui.success(I18n.t("cloud_command.provider.create_success", architecture: options[:architecture], provider: provider.name, org: org, box_name: box, version: version.version)) format_box_results(provider, @env) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.provider.create_fail", architecture: options[:architecture], provider: provider.name, org: org, box_name: box, version: version)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/provider/delete.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module ProviderCommand module Command class Delete < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud provider delete [options] organization/box-name provider-name version [architecture]" o.separator "" o.separator "Deletes a provider entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-f", "--[no-]force", "Force deletion of box version provider without confirmation") do |f| options[:force] = f end end # Parse the options argv = parse_options(opts) return if !argv if argv.count < 3 || argv.count > 4 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end org, box_name = argv.first.split('/', 2) provider_name = argv[1] version = argv[2] architecture = argv[3] @client = client_login(@env) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: @client.token ) if architecture.nil? architecture = select_provider_architecture(account, org, box_name, version, provider_name) end @env.ui.warn(I18n.t("cloud_command.provider.delete_warn", architecture: architecture, provider: provider_name, version: version, box: argv.first)) if !options[:force] cont = @env.ui.ask(I18n.t("cloud_command.continue")) return 1 if cont.strip.downcase != "y" end delete_provider(org, box_name, version, provider_name, architecture, account, options) end def select_provider_architecture(account, org, box, version, provider) with_version(account: account, org: org, box: box, version: version) do |box_version| list = box_version.providers.map(&:architecture) return list.first if list.size == 1 @env.ui.info(I18n.t("cloud_command.provider.delete_multiple_architectures", org: org, box_name: box, provider: provider)) list.each do |provider_name| @env.ui.info(" * #{provider_name}") end selected = nil while selected.nil? user_input = @env.ui.ask(I18n.t("cloud_command.provider.delete_architectures_prompt") + " ") selected = user_input if list.include?(user_input) end return selected end end # Delete a provider for the box version # # @param [String] org Organization name # @param [String] box Box name # @param [String] version Box version # @param [String] provider Provider name # @param [String] architecture Architecture of guest # @param [VagrantCloud::Account] account VagrantCloud account # @param [Hash] options Currently unused # @return [Integer] def delete_provider(org, box, version, provider, architecture, account, options={}) with_provider(account: account, org: org, box: box, version: version, provider: provider, architecture: architecture) do |p| p.delete @env.ui.error(I18n.t("cloud_command.provider.delete_success", architecture: architecture, provider: provider, org: org, box_name: box, version: version)) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.provider.delete_fail", architecture: architecture, provider: provider, org: org, box_name: box, version: version)) @env.ui.error(e) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/provider/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CloudCommand module ProviderCommand class Plugin < Vagrant.plugin("2") name "vagrant cloud box" description <<-DESC Provider life cycle commands for Vagrant Cloud DESC command(:provider) do require_relative "root" Command::Root end end end end end ================================================ FILE: plugins/commands/cloud/provider/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CloudCommand module ProviderCommand module Command class Root < Vagrant.plugin("2", :command) def self.synopsis "Provider commands" end def initialize(argv, env) super @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) @subcommands = Vagrant::Registry.new @subcommands.register(:create) do require File.expand_path("../create", __FILE__) Command::Create end @subcommands.register(:delete) do require File.expand_path("../delete", __FILE__) Command::Delete end @subcommands.register(:update) do require File.expand_path("../update", __FILE__) Command::Update end @subcommands.register(:upload) do require File.expand_path("../upload", __FILE__) Command::Upload end end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Print the help for all the provider 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 cloud provider []" opts.separator "" opts.separator "For various provider actions with Vagrant Cloud" 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 cloud provider -h`" end @env.ui.info(opts.help, prefix: false) end end end end end end ================================================ FILE: plugins/commands/cloud/provider/update.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module ProviderCommand module Command class Update < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud provider update [options] organization/box-name provider-name version architecture [url]" o.separator "" o.separator "Updates a provider entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-a", "--architecture ARCH", String, "Update architecture value of guest box") do |a| options[:architecture] = a end o.on("-c", "--checksum CHECKSUM_VALUE", String, "Checksum of the box for this provider. --checksum-type option is required.") do |c| options[:checksum] = c end o.on("-C", "--checksum-type TYPE", String, "Type of checksum used (md5, sha1, sha256, sha384, sha512). --checksum option is required.") do |c| options[:checksum_type] = c end o.on("--[no-]default-architecture", "Mark as default architecture for specific provider") do |d| options[:default_architecture] = d end end # Parse the options argv = parse_options(opts) return if !argv if argv.count < 4 || argv.count > 5 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = client_login(@env) org, box_name = argv.first.split('/', 2) provider_name = argv[1] version = argv[2] architecture = argv[3] url = argv[4] update_provider(org, box_name, version, provider_name, architecture, url, @client.token, options) end # Update a provider for the box version # # @param [String] org Organization name # @param [String] box Box name # @param [String] version Box version # @param [String] provider Provider name # @param [String] architecture Architecture of guest # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options # @option options [String] :checksum Checksum of the box asset # @option options [String] :checksum_type Type of the checksum # @return [Integer] def update_provider(org, box, version, provider, architecture, url, access_token, options) if !url @env.ui.warn(I18n.t("cloud_command.upload.no_url")) end account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_provider(account: account, org: org, box: box, version: version, provider: provider, architecture: architecture) do |p| p.checksum = options[:checksum] if options.key?(:checksum) p.checksum_type = options[:checksum_type] if options.key?(:checksum_type) p.architecture = options[:architecture] if options.key?(:architecture) p.default_architecture = options[:default_architecture] if options.key?(:default_architecture) p.url = url if !url.nil? p.save @env.ui.success(I18n.t("cloud_command.provider.update_success", architecture: architecture, provider: provider, org: org, box_name: box, version: version)) format_box_results(p, @env) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.provider.update_fail", architecture: architecture, provider: provider, org: org, box_name: box, version: version)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/provider/upload.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require "vagrant/util/uploader" module VagrantPlugins module CloudCommand module ProviderCommand module Command class Upload < Vagrant.plugin("2", :command) include Util def execute options = {direct: true} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud provider upload [options] organization/box-name provider-name version architecture box-file" o.separator "" o.separator "Uploads a box file to Vagrant Cloud for a specific provider" o.separator "" o.separator "Options:" o.separator "" o.on("-D", "--[no-]direct", "Upload asset directly to backend storage") do |d| options[:direct] = d end end # Parse the options argv = parse_options(opts) return if !argv if argv.count != 5 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = client_login(@env) org, box_name = argv.first.split('/', 2) provider_name = argv[1] version = argv[2] architecture = argv[3] file = File.expand_path(argv[4]) upload_provider(org, box_name, version, provider_name, architecture, file, @client.token, options) end # Upload an asset for a box version provider # # @param [String] org Organization name # @param [String] box Box name # @param [String] version Box version # @param [String] provider Provider name # @param [String] architecture Architecture name # @param [String] file Path to asset # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options # @option options [Boolean] :direct Upload directly to backend storage # @return [Integer] def upload_provider(org, box, version, provider, architecture, file, access_token, options) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) # Include size check on file and disable direct if over 5G if options[:direct] fsize = File.stat(file).size if fsize > (5 * Vagrant::Util::Numeric::GIGABYTE) box_size = Vagrant::Util::Numeric.bytes_to_string(fsize) @env.ui.warn(I18n.t("cloud_command.provider.direct_disable", size: box_size)) options[:direct] = false end end with_provider(account: account, org: org, box: box, version: version, provider: provider, architecture: architecture) do |p| p.upload(direct: options[:direct]) do |upload_url| m = options[:direct] ? :put : :put uploader = Vagrant::Util::Uploader.new(upload_url, file, ui: @env.ui, method: m) ui = Vagrant::UI::Prefixed.new(@env.ui, "cloud") ui.output(I18n.t("cloud_command.provider.upload", org: org, box_name: box, version: version, provider: provider)) ui.info("Upload File: #{file}") uploader.upload! ui.success(I18n.t("cloud_command.provider.upload_success", org: org, box_name: box, version: version, provider: provider)) end 0 end rescue Vagrant::Errors::UploaderError, VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.provider.upload_fail", provider: provider, org: org, box_name: box, version: version)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/publish.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require "vagrant/util/uploader" module VagrantPlugins module CloudCommand module Command class Publish < Vagrant.plugin("2", :command) include Util def execute options = { architecture: Vagrant::Util::Platform.architecture, direct_upload: true, } opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud publish [options] organization/box-name version provider-name [provider-file]" o.separator "" o.separator "Create and release a new Vagrant Box on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-a", "--architecture ARCH", String, "Architecture of guest box (defaults to current host architecture)") do |a| options[:architecture] = a end o.on("--url URL", String, "Remote URL to download this provider (cannot be used with provider-file)") do |u| options[:url] = u end o.on("-d", "--description DESCRIPTION", String, "Full description of box") do |d| options[:description] = d end o.on("--version-description DESCRIPTION", String, "Description of the version to create") do |v| options[:version_description] = v end o.on("-f", "--[no-]force", "Disables confirmation to create or update box") do |f| options[:force] = f end o.on("-p", "--[no-]private", "Makes box private") do |p| options[:private] = p end o.on("-r", "--[no-]release", "Releases box") do |p| options[:release] = p end o.on("-s", "--short-description DESCRIPTION", String, "Short description of the box") do |s| options[:short_description] = s end o.on("-c", "--checksum CHECKSUM_VALUE", String, "Checksum of the box for this provider. --checksum-type option is required.") do |c| options[:checksum] = c end o.on("-C", "--checksum-type TYPE", String, "Type of checksum used (md5, sha1, sha256, sha384, sha512). --checksum option is required.") do |c| options[:checksum_type] = c end o.on("--[no-]direct-upload", "Upload asset directly to backend storage") do |d| options[:direct_upload] = d end o.on("--[no-]default-architecture", "Mark as default architecture for specific provider") do |d| options[:default_architecture] = d end end # Parse the options argv = parse_options(opts) return if !argv if argv.length < 3 || # missing required arguments argv.length > 4 || # too many arguments (argv.length < 4 && !options.key?(:url)) || # file argument required if url is not provided (argv.length > 3 && options.key?(:url)) # cannot provide url and file argument raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end org, box_name = argv.first.split('/', 2) _, version, provider_name, box_file = argv if box_file && !File.file?(box_file) raise Vagrant::Errors::BoxFileNotExist, file: box_file end @client = client_login(@env) params = options.slice(:private, :release, :url, :short_description, :description, :version_description, :checksum, :checksum_type, :architecture, :default_architecture) # Display output to user describing action to be taken display_preamble(org, box_name, version, provider_name, params) if !options[:force] cont = @env.ui.ask(I18n.t("cloud_command.continue")) return 1 if cont.strip.downcase != "y" end # Load up all the models we'll need to publish the asset box = load_box(org, box_name, @client.token) box_v = load_box_version(box, version) box_p = load_version_provider(box_v, provider_name, params[:architecture]) # Update all the data set_box_info(box, params.slice(:private, :short_description, :description)) set_version_info(box_v, params.slice(:version_description)) set_provider_info(box_p, params.slice( :architecture, :checksum, :checksum_type, :default_architecture, :url)) # Save any updated state @env.ui.warn(I18n.t("cloud_command.publish.box_save")) box.save # If we have a box file asset, upload it if box_file upload_box_file(box_p, box_file, options.slice(:direct_upload)) end # If configured to release the box, release it if options[:release] && !box_v.released? release_version(box_v) end # And we're done! @env.ui.success(I18n.t("cloud_command.publish.complete", org: org, box_name: box_name)) format_box_results(box_p, @env) 0 rescue VagrantCloud::Error => err @env.ui.error(I18n.t("cloud_command.errors.publish.fail", org: org, box_name: box_name)) @env.ui.error(err.message) 1 end # Upload the file for the given box provider # # @param [VagrantCloud::Box::Provider] provider Vagrant Cloud box version provider # @param [String] box_file Path to local asset for upload # @param [Hash] options # @option options [Boolean] :direct_upload Upload directly to backend storage # @return [nil] def upload_box_file(provider, box_file, options={}) box_file = File.absolute_path(box_file) @env.ui.info(I18n.t("cloud_command.publish.upload_provider", file: box_file)) # Include size check on file and disable direct if over 5G if options[:direct_upload] fsize = File.stat(box_file).size if fsize > (5 * Vagrant::Util::Numeric::GIGABYTE) box_size = Vagrant::Util::Numeric.bytes_to_string(fsize) @env.ui.warn(I18n.t("cloud_command.provider.direct_disable", size: box_size)) options[:direct_upload] = false end end provider.upload(direct: options[:direct_upload]) do |upload_url| Vagrant::Util::Uploader.new(upload_url, box_file, ui: @env.ui, method: :put).upload! end nil end # Release the box version # # @param [VagrantCloud::Box::Version] version Vagrant Cloud box version # @return [nil] def release_version(version) @env.ui.info(I18n.t("cloud_command.publish.release")) version.release nil end # Set any box related attributes that were provided # # @param [VagrantCloud::Box] box Vagrant Cloud box # @param [Hash] options # @option options [Boolean] :private Box access is private # @option options [String] :short_description Short description of box # @option options [String] :description Full description of box # @return [VagrantCloud::Box] def set_box_info(box, options={}) box.private = options[:private] if options.key?(:private) box.short_description = options[:short_description] if options.key?(:short_description) box.description = options[:description] if options.key?(:description) box end # Set any version related attributes that were provided # # @param [VagrantCloud::Box::Version] version Vagrant Cloud box version # @param [Hash] options # @option options [String] :version_description Description for this version # @return [VagrantCloud::Box::Version] def set_version_info(version, options={}) version.description = options[:version_description] if options.key?(:version_description) version end # Set any provider related attributes that were provided # # @param [VagrantCloud::Box::Provider] provider Vagrant Cloud box version provider # @param [Hash] options # @option options [String] architecture Guest architecture of box # @option options [String] :url Remote URL for self hosted # @option options [String] :checksum_type Type of checksum value provided # @option options [String] :checksum Checksum of the box asset # @option options [Boolean] :default_architecture Default architecture for named provider # @return [VagrantCloud::Box::Provider] def set_provider_info(provider, options={}) provider.url = options[:url] if options.key?(:url) provider.checksum_type = options[:checksum_type] if options.key?(:checksum_type) provider.checksum = options[:checksum] if options.key?(:checksum) provider.architecture = options[:architecture] if options.key?(:architecture) provider.default_architecture = options[:default_architecture] if options.key?(:default_architecture) provider end # Load the requested version provider # # @param [VagrantCloud::Box::Version] version The version of the Vagrant Cloud box # @param [String] provider_name Name of the provider # @return [VagrantCloud::Box::Provider] def load_version_provider(version, provider_name, architecture) provider = version.providers.detect { |pv| pv.name == provider_name && pv.architecture == architecture } return provider if provider version.add_provider(provider_name, architecture) end # Load the requested box version # # @param [VagrantCloud::Box] box The Vagrant Cloud box # @param [String] version Version of the box # @return [VagrantCloud::Box::Version] def load_box_version(box, version) v = box.versions.detect { |v| v.version == version } return v if v box.add_version(version) end # Load the requested box # # @param [String] org Organization name for box # @param [String] box_name Name of the box # @param [String] access_token User access token # @return [VagrantCloud::Box] def load_box(org, box_name, access_token) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) box = account.organization(name: org).boxes.detect { |b| b.name == box_name } return box if box account.organization(name: org).add_box(box_name) end # Display publishing information to user before starting process # # @param [String] org Organization name # @param [String] box_name Name of the box to publish # @param [String] version Version of the box to publish # @param [String] provider_name Name of the provider being published # @param [Hash] options # @option options [String] :architecture Name of architecture of provider being published# # @option options [Boolean] :private Box is private # @option options [Boolean] :release Box should be released # @option options [String] :url Remote URL for self-hosted boxes # @option options [Boolean] :default_architecture Architecture is default for provider name # @option options [String] :description Description of the box # @option options [String] :short_description Short description of the box # @option options [String] :version_description Description of the box version # @return [nil] def display_preamble(org, box_name, version, provider_name, options={}) @env.ui.warn(I18n.t("cloud_command.publish.confirm.warn")) @env.ui.info(I18n.t("cloud_command.publish.confirm.box", org: org, box_name: box_name, version: version, provider_name: provider_name)) @env.ui.info(I18n.t("cloud_command.publish.confirm.private")) if options[:private] @env.ui.info(I18n.t("cloud_command.publish.confirm.release")) if options[:release] @env.ui.info(I18n.t("cloud_command.publish.confirm.architecture", architecture: options[:architecture])) @env.ui.info(I18n.t("cloud_command.publish.confirm.default_architecture")) if options[:default_architecture] @env.ui.info(I18n.t("cloud_command.publish.confirm.box_url", url: options[:url])) if options[:url] @env.ui.info(I18n.t("cloud_command.publish.confirm.box_description", description: options[:description])) if options[:description] @env.ui.info(I18n.t("cloud_command.publish.confirm.box_short_desc", short_description: options[:short_description])) if options[:short_description] @env.ui.info(I18n.t("cloud_command.publish.confirm.version_desc", version_description: options[:version_description])) if options[:version_description] nil end end end end end ================================================ FILE: plugins/commands/cloud/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CloudCommand module Command class Root < Vagrant.plugin("2", :command) def self.synopsis "manages everything related to Vagrant Cloud" end def initialize(argv, env) super @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) @subcommands = Vagrant::Registry.new @subcommand_helptext = {} @subcommands.register(:auth) do require File.expand_path("../auth/root", __FILE__) AuthCommand::Command::Root end @subcommand_helptext[:auth] = "For various authorization operations on Vagrant Cloud" @subcommands.register(:box) do require File.expand_path("../box/root", __FILE__) BoxCommand::Command::Root end @subcommand_helptext[:box] = "For managing a Vagrant box entry on Vagrant Cloud" # TODO: Uncomment this when API endpoint exists #@subcommands.register(:list) do # require File.expand_path("../list", __FILE__) # List #end #@subcommand_helptext[:list] = "Displays a list of Vagrant boxes that the current user manages" @subcommands.register(:search) do require File.expand_path("../search", __FILE__) Search end @subcommand_helptext[:search] = "Search Vagrant Cloud for available boxes" @subcommands.register(:provider) do require File.expand_path("../provider/root", __FILE__) ProviderCommand::Command::Root end @subcommand_helptext[:provider] = "For managing a Vagrant box's provider options" @subcommands.register(:publish) do require File.expand_path("../publish", __FILE__) Publish end @subcommand_helptext[:publish] = "A complete solution for creating or updating a new box on Vagrant Cloud" @subcommands.register(:version) do require File.expand_path("../version/root", __FILE__) VersionCommand::Command::Root end @subcommand_helptext[:version] = "For managing a Vagrant box's versions" end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Print the help for all the box 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 cloud []" opts.separator "" opts.separator "The cloud command can be used for taking actions against" opts.separator "Vagrant Cloud like searching or uploading a Vagrant Box" 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.ljust(15)} #{@subcommand_helptext[key.to_sym]}" end opts.separator "" opts.separator "For help on any individual subcommand run `vagrant cloud -h`" end @env.ui.info(opts.help, prefix: false) end end end end end ================================================ FILE: plugins/commands/cloud/search.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module Command class Search < Vagrant.plugin("2", :command) include Util def execute options = {quiet: true} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud search [options] query" o.separator "" o.separator "Search for boxes managed by a specific" o.separator "user/organization on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-a", "--architecture ARCH", "Filter search results to a single architecture. Defaults to all.") do |a| options[:architecture] = a end o.on("-j", "--json", "Formats results in JSON") do |j| options[:json] = j end o.on("-p", "--page PAGE", Integer, "The page to display Default: 1") do |j| options[:page] = j end o.on("-s", "--short", "Shows a simple list of box names") do |s| options[:short] = s end o.on("-o", "--order ORDER", String, "Order to display results ('desc' or 'asc') Default: 'desc'") do |o| options[:order] = o end o.on("-l", "--limit LIMIT", Integer, "Max number of search results Default: 25") do |l| options[:limit] = l end o.on("-p", "--provider PROVIDER", String, "Filter search results to a single provider. Defaults to all.") do |p| options[:provider] = p end o.on("--sort-by SORT", "Field to sort results on (created, downloads, updated) Default: downloads") do |s| options[:sort] = s end o.on("--[no-]auth", "Authenticate with Vagrant Cloud if required before searching") do |l| options[:quiet] = !l end end # Parse the options argv = parse_options(opts) return if !argv if argv.length != 1 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = client_login(@env, options.slice(:quiet)) query = argv.first options[:limit] = 25 if !(options[:limit].to_i < 1) && !options[:limit] search(query, @client&.token, options) end # Perform requested search and display results to user # # @param [String] query Search query string # @param [Hash] options # @option options [String] :provider Filter by provider # @option options [String] :sort Field to sort results # @option options [Integer] :limit Number of results to display # @option options [Integer] :page Page of results to display # @param [String] access_token User access token # @return [Integer] def search(query, access_token, options={}) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) params = {query: query}.merge(options.slice(:architecture, :provider, :sort, :order, :limit, :page)) result = account.searcher.search(**params) if result.boxes.empty? @env.ui.warn(I18n.t("cloud_command.search.no_results", query: query)) return 0 end format_search_results(result.boxes, options[:short], options[:json], @env) 0 rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.search.fail")) @env.ui.error(e.message) 1 end end end end end ================================================ FILE: plugins/commands/cloud/util.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CloudCommand module Util # @return [String] Vagrant Cloud server URL def api_server_url if Vagrant.server_url == Vagrant::DEFAULT_SERVER_URL return "#{Vagrant.server_url}/api/v1" end begin addr = URI.parse(Vagrant.server_url) if addr.path.empty? || addr.path.to_s == "/" addr.path = "/api/v1" end addr.to_s rescue URI::Error Vagrant.server_url end end # @param [Vagrant::Environment] env # @param [Hash] options # @option options [String] :login Username or email # @option options [String] :description Description of login usage for token # @option options [String] :code 2FA code for login # @option options [Boolean] :quiet Do not prompt user # @returns [VagrantPlugins::CloudCommand::Client, nil] def client_login(env, options={}) return @_client if defined?(@_client) @_client = Client.new(env) return @_client if @_client.logged_in? # If directed to be quiet, do not continue and # just return nil return if options[:quiet] # Let the user know what is going on. env.ui.output(I18n.t("cloud_command.command_header") + "\n") # If it is a private cloud installation, show that if Vagrant.server_url != Vagrant::DEFAULT_SERVER_URL env.ui.output("Vagrant Cloud URL: #{Vagrant.server_url}") end options = {} if !options # Ask for the username if options[:login] @_client.username_or_email = options[:login] env.ui.output("Vagrant Cloud username or email: #{@_client.username_or_email}") else @_client.username_or_email = env.ui.ask("Vagrant Cloud username or email: ") end @_client.password = env.ui.ask("Password (will be hidden): ", echo: false) description_default = "Vagrant login from #{Socket.gethostname}" if !options[:description] description = env.ui.ask("Token description (Defaults to #{description_default.inspect}): ") else description = options[:description] env.ui.output("Token description: #{description}") end description = description_default if description.empty? code = nil begin token = @_client.login(description: description, code: code) rescue Errors::TwoFactorRequired until code code = env.ui.ask("2FA code: ") if @_client.two_factor_delivery_methods.include?(code.downcase) delivery_method, code = code, nil @_client.request_code delivery_method end end retry end @_client.store_token(token) Vagrant::Util::CredentialScrubber.sensitive(token) env.ui.success(I18n.t("cloud_command.logged_in")) @_client end # Print search results from Vagrant Cloud to the console # # @param [Array] search_results Box search results from Vagrant Cloud # @param [Boolean] short Print short summary # @param [Boolean] json Print output in JSON format # @param [Vagrant::Environment] env Current Vagrant environment # @return [nil] def format_search_results(search_results, short, json, env) result = search_results.map do |b| { name: b.tag, version: b.current_version.version, downloads: format_downloads(b.downloads.to_s), providers: b.current_version.providers.map(&:name).uniq.join(", "), architectures: b.current_version.providers.map(&:architecture).join(", ") } end if short result.map { |b| env.ui.info(b[:name]) } elsif json env.ui.info(result.to_json) else column_labels = {} columns = result.first.keys columns.each do |c| column_labels[c] = c.to_s.upcase end print_search_table(env, column_labels, result, [:downloads]) end nil end # Output box details result from Vagrant Cloud # # @param [VagrantCloud::Box, VagrantCloud::Box::Version] box Box or box version to display # @param [Vagrant::Environment] env Current Vagrant environment # @return [nil] def format_box_results(box, env, options={}) if box.is_a?(VagrantCloud::Box) info = box_info(box, options) elsif box.is_a?(VagrantCloud::Box::Version) info = version_info(box) else info = provider_info(box) end width = info.keys.map(&:size).max info.each do |k, v| v.to_s.split("\n").each_with_index do |line, idx| whitespace = width - k.size + line.to_s.size if idx == 0 env.ui.info "#{k}: #{line.rjust(whitespace)}" else whitespace += k.size + 2 env.ui.info line.rjust(whitespace) end end end nil end # Load box and yield # # @param [VagrantCloud::Account] account Vagrant Cloud account # @param [String] org Organization name # @param [String] box Box name # @yieldparam [VagrantCloud::Box] box Requested Vagrant Cloud box # @yieldreturn [Integer] # @return [Integer] def with_box(account:, org:, box:) org = account.organization(name: org) b = org.boxes.detect { |b| b.name == box } if !b @env.ui.error(I18n.t("cloud_command.box.not_found", org: org.username, box_name: box)) return 1 end yield b end # Load box version and yield # # @param [VagrantCloud::Account] account Vagrant Cloud account # @param [String] org Organization name # @param [String] box Box name # @param [String] version Box version # @yieldparam [VagrantCloud::Box::Version] version Requested Vagrant Cloud box version # @yieldreturn [Integer] # @return [Integer] def with_version(account:, org:, box:, version:) with_box(account: account, org: org, box: box) do |b| v = b.versions.detect { |v| v.version == version } if !v @env.ui.error(I18n.t("cloud_command.version.not_found", box_name: box, org: org, version: version)) return 1 end yield v end end # Load box version and yield # # @param [VagrantCloud::Account] account Vagrant Cloud account # @param [String] org Organization name # @param [String] box Box name # @param [String] version Box version # @param [String] provider Box version provider name # @yieldparam [VagrantCloud::Box::Provider] provider Requested Vagrant Cloud box version provider # @yieldreturn [Integer] # @return [Integer] def with_provider(account:, org:, box:, version:, provider:, architecture:) with_version(account: account, org: org, box: box, version: version) do |v| p = v.providers.detect { |p| p.name == provider && p.architecture == architecture } if !p @env.ui.error(I18n.t("cloud_command.provider.not_found", org: org, box_name: box, version: version, provider_name: provider)) return 1 end yield p end end protected # Extract box information for display # # @param [VagrantCloud::Box] box Box for extracting information # @return [Hash] def box_info(box, options={}) current_version = box.current_version if current_version current_version = nil if !Array(options[:providers]).empty? && current_version.providers.none? { |p| options[:providers].include?(p.name) } current_version = nil if !Array(options[:architectures]).empty? && current_version.providers.none? { |p| options[:architectures].include?(p.architecture) } end versions = box.versions # Apply provider filter if defined versions = versions.find_all { |v| v.providers.any? { |p| options[:providers].include?(p.name) } } if !Array(options[:providers]).empty? # Apply architecture filter if defined versions = versions.find_all { |v| v.providers.any? { |p| options[:architectures].include?(p.architecture) } } if !Array(options[:architectures]).empty? Hash.new.tap do |i| i["Box"] = box.tag i["Description"] = box.description i["Private"] = box.private ? "yes" : "no" i["Created"] = box.created_at i["Updated"] = box.updated_at if !current_version.nil? i["Current Version"] = box.current_version.version else i["Current Version"] = "N/A" end i["Versions"] = versions.slice(0, 5).map(&:version).join(", ") if box.versions.size > 5 i["Versions"] += " ..." end i["Downloads"] = format_downloads(box.downloads) end end # Extract version information for display # # @param [VagrantCloud::Box::Version] version Box version for extracting information # @return [Hash] def version_info(version) provider_arches = version.providers.group_by(&:name).map { |provider_name, info| "#{provider_name} (#{info.map(&:architecture).sort.join(", ")})" }.sort.join("\n") Hash.new.tap do |i| i["Box"] = version.box.tag i["Version"] = version.version i["Description"] = version.description i["Status"] = version.status i["Providers"] = provider_arches i["Created"] = version.created_at i["Updated"] = version.updated_at end end # Extract provider information for display # # @param [VagrantCloud::Box::Provider] provider Box provider for extracting information # @return [Hash] def provider_info(provider) { "Box" => provider.version.box.tag, "Private" => provider.version.box.private ? "yes" : "no", "Version" => provider.version.version, "Provider" => provider.name, "Architecture" => provider.architecture, "Default Architecture" => provider.default_architecture ? "yes" : "no", } end # Print table results from search request # # @param [Vagrant::Environment] env Current Vagrant environment # @param [Hash] column_labels A hash of key/value pairs for table labels (i.e. {col1: "COL1"}) # @param [Array] results An array of hashes representing search resuls # @param [Array] to_jrust_keys - List of columns keys to right justify (left justify is defualt) # @return [nil] # @note Modified from https://stackoverflow.com/a/28685559 def print_search_table(env, column_labels, results, to_rjust_keys) columns = column_labels.each_with_object({}) do |(col,label),h| h[col] = { label: label, width: [results.map { |g| g[col].size }.max, label.size].max } end write_header(env, columns) write_divider(env, columns) results.each { |h| write_line(env, columns, h, to_rjust_keys) } write_divider(env, columns) end # Write the header for a table # # @param [Vagrant::Environment] env Current Vagrant environment # @param [Array] columns List of columns in Hash format with `:label` and `:width` keys # @return [nil] def write_header(env, columns) env.ui.info "| #{ columns.map { |_,g| g[:label].ljust(g[:width]) }.join(' | ') } |" nil end # Write a row divider for a table # # @param [Vagrant::Environment] env Current Vagrant environment # @param [Array] columns List of columns in Hash format with `:label` and `:width` keys # @return [nil] def write_divider(env, columns) env.ui.info "+-#{ columns.map { |_,g| "-"*g[:width] }.join("-+-") }-+" nil end # Write a line of content for a table # # @param [Vagrant::Environment] env Current Vagrant environment # @param [Array] columns List of columns in Hash format with `:label` and `:width` keys # @param [Hash] h Values to print in row # @param [Array] to_rjust_keys List of columns to right justify # @return [nil] def write_line(env, columns, h, to_rjust_keys) str = h.keys.map { |k| if to_rjust_keys.include?(k) h[k].rjust(columns[k][:width]) else h[k].ljust(columns[k][:width]) end }.join(" | ") env.ui.info "| #{str} |" nil end # Converts a string of numbers into a formatted number # # 1234 -> 1,234 # # @param [String] number Numer to format def format_downloads(number) number.to_s.chars.reverse.each_slice(3).map(&:join).join(",").reverse end end end end ================================================ FILE: plugins/commands/cloud/version/create.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module VersionCommand module Command class Create < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud version create [options] organization/box-name version" o.separator "" o.separator "Creates a version entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-d", "--description DESCRIPTION", String, "A description for this version") do |d| options[:description] = d end 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 @client = client_login(@env) org, box_name = argv.first.split('/', 2) version = argv[1] create_version(org, box_name, version, @client.token, options.slice(:description)) end # Create a new version of the box # # @param [String] org Organization box is within # @param [String] box_name Name of box # @param [String] box_version Version of box to create # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options # @option options [String] :description Description of box version # @return [Integer] def create_version(org, box_name, box_version, access_token, options={}) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_box(account: account, org: org, box: box_name) do |box| version = box.add_version(box_version) version.description = options[:description] if options.key?(:description) version.save @env.ui.success(I18n.t("cloud_command.version.create_success", version: box_version, org: org, box_name: box_name)) format_box_results(version, @env) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.version.create_fail", version: box_version, org: org, box_name: box_name)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/version/delete.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module VersionCommand module Command class Delete < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud version delete [options] organization/box-name version" o.separator "" o.separator "Deletes a version entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-f", "--[no-]force", "Force deletion without confirmation") do |f| options[:force] = f end end # Parse the options argv = parse_options(opts) return if !argv if argv.size != 2 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end org, box_name = argv.first.split('/', 2) version = argv[1] if !options[:force] @env.ui.warn(I18n.t("cloud_command.version.delete_warn", version: version, box: argv.first)) cont = @env.ui.ask(I18n.t("cloud_command.continue")) return 1 if cont.strip.downcase != "y" end @client = client_login(@env) delete_version(org, box_name, version, @client.token, options.slice) end # Delete the requested box version # # @param [String] org Box organization name # @param [String] box_name Name of the box # @param [String] box_version Version of the box # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options Current unsued def delete_version(org, box_name, box_version, access_token, options={}) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_version(account: account, org: org, box: box_name, version: box_version) do |version| version.delete @env.ui.success(I18n.t("cloud_command.version.delete_success", version: box_version, org: org, box_name: box_name)) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.version.delete_fail", version: box_version, org: org, box_name: box_name)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/version/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CloudCommand module VersionCommand class Plugin < Vagrant.plugin("2") name "vagrant cloud version" description <<-DESC Version life cycle commands for Vagrant Cloud DESC command(:version) do require_relative "root" Command::Root end end end end end ================================================ FILE: plugins/commands/cloud/version/release.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module VersionCommand module Command class Release < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud version release [options] organization/box-name version" o.separator "" o.separator "Releases a version entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-f", "--[no-]force", "Release without confirmation") do |f| options[:force] = f end end # Parse the options argv = parse_options(opts) return if !argv if argv.size != 2 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end if !options[:force] @env.ui.warn(I18n.t("cloud_command.version.release_warn", version: argv[1], box: argv.first)) cont = @env.ui.ask(I18n.t("cloud_command.continue")) return 1 if cont.strip.downcase != "y" end @client = client_login(@env) org, box_name = argv.first.split('/', 2) version = argv[1] release_version(org, box_name, version, @client.token, options) end # Release the box version # # @param [String] org Organization name # @param [String] box_name Box name # @param [String] version Version of the box # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options Currently unused # @return [Integer] def release_version(org, box_name, version, access_token, options={}) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_version(account: account, org: org, box: box_name, version: version) do |v| v.release @env.ui.success(I18n.t("cloud_command.version.release_success", version: version, org: org, box_name: box_name)) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.version.release_fail", version: version, org: org, box_name: box_name)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/version/revoke.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module VersionCommand module Command class Revoke < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud version revoke [options] organization/box-name version" o.separator "" o.separator "Revokes a version entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-f", "--[no-]force", "Force revocation without confirmation") do |f| options[:force] = f end end # Parse the options argv = parse_options(opts) return if !argv if argv.size != 2 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end if !options[:force] @env.ui.warn(I18n.t("cloud_command.version.revoke_warn", version: argv[1], box: argv.first)) cont = @env.ui.ask(I18n.t("cloud_command.continue")) return 1 if cont.strip.downcase != "y" end @client = client_login(@env) org, box_name = argv.first.split('/', 2) version = argv[1] revoke_version(org, box_name, version, @client.token, options) end # Revoke release of box version # # @param [String] org Organization name # @param [String] box_name Box name # @param [String] version Version of the box # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options Currently unused # @return [Integer] def revoke_version(org, box_name, box_version, access_token, options={}) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_version(account: account, org: org, box: box_name, version: box_version) do |version| version.revoke @env.ui.success(I18n.t("cloud_command.version.revoke_success", version: box_version, org: org, box_name: box_name)) format_box_results(version, @env) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.version.revoke_fail", version: box_version, org: org, box_name: box_name)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/cloud/version/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CloudCommand module VersionCommand module Command class Root < Vagrant.plugin("2", :command) def self.synopsis "Version commands" end def initialize(argv, env) super @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) @subcommands = Vagrant::Registry.new @subcommands.register(:create) do require File.expand_path("../create", __FILE__) Command::Create end @subcommands.register(:delete) do require File.expand_path("../delete", __FILE__) Command::Delete end @subcommands.register(:revoke) do require File.expand_path("../revoke", __FILE__) Command::Revoke end @subcommands.register(:release) do require File.expand_path("../release", __FILE__) Command::Release end @subcommands.register(:update) do require File.expand_path("../update", __FILE__) Command::Update end end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Print the help for all the version 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 cloud version []" opts.separator "" opts.separator "For taking various actions against a Vagrant box's version attribute on Vagrant Cloud" 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 cloud version -h`" end @env.ui.info(opts.help, prefix: false) end end end end end end ================================================ FILE: plugins/commands/cloud/version/update.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CloudCommand module VersionCommand module Command class Update < Vagrant.plugin("2", :command) include Util def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant cloud version update [options] organization/box-name version" o.separator "" o.separator "Updates a version entry on Vagrant Cloud" o.separator "" o.separator "Options:" o.separator "" o.on("-d", "--description DESCRIPTION", "A description for this version") do |d| options[:description] = d end end # Parse the options argv = parse_options(opts) return if !argv if argv.size != 2 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end @client = client_login(@env) org, box_name = argv.first.split('/', 2) version = argv[1] update_version(org, box_name, version, @client.token, options) end # Update the version of the box # @param [String] org Organization name # @param [String] box_name Box name # @param [String] version Version of the box # @param [String] access_token User Vagrant Cloud access token # @param [Hash] options # @options options [String] :description Description of box version # @return [Integer] def update_version(org, box_name, box_version, access_token, options) account = VagrantCloud::Account.new( custom_server: api_server_url, access_token: access_token ) with_version(account: account, org: org, box: box_name, version: box_version) do |version| version.description = options[:description] if options.key?(:description) version.save @env.ui.success(I18n.t("cloud_command.version.update_success", version: box_version, org: org, box_name: box_name)) format_box_results(version, @env) 0 end rescue VagrantCloud::Error => e @env.ui.error(I18n.t("cloud_command.errors.version.update_fail", version: box_version, org: org, box_name: box_name)) @env.ui.error(e.message) 1 end end end end end end ================================================ FILE: plugins/commands/destroy/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommandDestroy class Command < Vagrant.plugin("2", :command) def self.synopsis "stops and deletes all traces of the vagrant machine" end def execute options = {} options[:force] = false options[:force_halt] = true opts = OptionParser.new do |o| o.banner = "Usage: vagrant destroy [options] [name|id]" o.separator "" o.separator "Options:" o.separator "" o.on("-f", "--force", "Destroy without confirmation.") do |f| options[:force] = f end o.on("--[no-]parallel", "Enable or disable parallelism if provider supports it (automatically enables force)") do |p| options[:parallel] = p end o.on("-g", "--graceful", "Gracefully poweroff of VM") do |f| options[:force_halt] = false end end # Parse the options argv = parse_options(opts) return if !argv if options[:parallel] && !options[:force] @env.ui.warn(I18n.t("vagrant.commands.destroy.warning")) sleep(5) options[:force] = true end @logger.debug("'Destroy' each target VM...") machines = [] init_states = {} declined = 0 @env.batch(options[:parallel]) do |batch| with_target_vms(argv, reverse: true) do |vm| # gather states to be checked after destroy init_states[vm.name] = vm.state.id machines << vm batch.action(vm, :destroy, force_confirm_destroy: options[:force], force_halt: options[:force_halt]) end end machines.each do |m| if init_states[m.name] != :not_created && m.state.id == init_states[m.name] @logger.debug("state was not changed for '#{m.name}', marking as failed (state: #{m.state.id})") declined += 1 end end # Nothing was declined return 0 if declined == 0 # Everything was declined, state was not changed return 1 if declined == machines.length # Some was declined return 2 end end end end ================================================ FILE: plugins/commands/destroy/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandDestroy class Plugin < Vagrant.plugin("2") name "destroy command" description <<-DESC The `destroy` command deletes and removes the files and record of your virtual machines. All data is lost and a new VM will have to be created using `up` DESC command("destroy") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/global-status/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandGlobalStatus class Command < Vagrant.plugin("2", :command) def self.synopsis "outputs status Vagrant environments for this user" end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant global-status" o.separator "" o.on("--prune", "Prune invalid entries.") do |p| options[:prune] = true end end # Parse the options argv = parse_options(opts) return if !argv columns = [ ["id", :id], ["name", :name], ["provider", :provider], ["state", :state], ["directory", :vagrantfile_path], ] widths = {} widths[:id] = 8 widths[:name] = 6 widths[:provider] = 6 widths[:state] = 6 widths[:vagrantfile_path] = 35 entries = [] prune = [] @env.machine_index.each do |entry| # If we're pruning and this entry is invalid, skip it # and prune it later. if options[:prune] && !entry.valid?(@env.home_path) prune << entry next end entries << entry columns.each do |_, method| # Skip the id next if method == :id widths[method] ||= 0 cur = entry.send(method).to_s.length widths[method] = cur if cur > widths[method] end end # Prune all the entries to prune prune.each do |entry| deletable = @env.machine_index.get(entry.id) @env.machine_index.delete(deletable) if deletable end # Machine-readable (non-formatted) output @env.ui.machine("metadata", "machine-count", entries.length.to_s); entries.each do |entry| opts = { "target" => entry.name.to_s } @env.ui.machine("machine-id", entry.id.to_s[0...7], opts) @env.ui.machine("provider-name", entry.provider.to_s, opts) @env.ui.machine("machine-home", entry.vagrantfile_path.to_s, opts) @env.ui.machine("state", entry.state.to_s, opts) end # Human-readable (table formatted) output total_width = 0 columns.each do |header, method| header = header.ljust(widths[method]) if widths[method] @env.ui.info("#{header} ", new_line: false) total_width += header.length + 1 end @env.ui.info("") @env.ui.info("-" * total_width) if entries.empty? @env.ui.info(I18n.t("vagrant.global_status_none")) return 0 end entries.each do |entry| columns.each do |_, method| v = entry.send(method).to_s v = v[0...7] if method == :id v = v.ljust(widths[method]) if widths[method] @env.ui.info("#{v} ", new_line: false) end @env.ui.info("") end @env.ui.info(" \n" + I18n.t("vagrant.global_status_footer")) # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/global-status/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandGlobalStatus class Plugin < Vagrant.plugin("2") name "global-status command" description <<-DESC The `global-status` command shows what the running state (running/saved/..) is of all the Vagrant environments known to the system. DESC command("global-status") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/halt/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandHalt class Command < Vagrant.plugin("2", :command) def self.synopsis "stops the vagrant machine" end def execute options = {} options[:force] = false opts = OptionParser.new do |o| o.banner = "Usage: vagrant halt [options] [name|id]" o.separator "" o.separator "Options:" o.separator "" o.on("-f", "--force", "Force shut down (equivalent of pulling power)") do |f| options[:force] = f end end # Parse the options argv = parse_options(opts) return if !argv @logger.debug("Halt command: #{argv.inspect} #{options.inspect}") with_target_vms(argv, reverse: true) do |vm| vm.action(:halt, force_halt: options[:force]) end # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/halt/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandHalt class Plugin < Vagrant.plugin("2") name "halt command" description <<-DESC The `halt` command shuts your virtual machine down forcefully. The command `up` recreates it. DESC command("halt") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/help/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandHelp class Command < Vagrant.plugin("2", :command) def self.synopsis "shows the help for a subcommand" end def execute return @env.cli([]) if @argv.empty? @env.cli([@argv[0], "-h"]) end end end end ================================================ FILE: plugins/commands/help/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandHelp class Plugin < Vagrant.plugin("2") name "help command" description <<-DESC The `help` command shows help for the given command. DESC command("help") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/init/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require 'vagrant/util/template_renderer' module VagrantPlugins module CommandInit class Command < Vagrant.plugin("2", :command) def self.synopsis "initializes a new Vagrant environment by creating a Vagrantfile" end def execute options = { force: false, minimal: false, output: "Vagrantfile", template: ENV["VAGRANT_DEFAULT_TEMPLATE"] } opts = OptionParser.new do |o| o.banner = "Usage: vagrant init [options] [name [url]]" o.separator "" o.separator "Options:" o.separator "" o.on("--box-version VERSION", "Version of the box to add") do |f| options[:box_version] = f end o.on("-f", "--force", "Overwrite existing Vagrantfile") do |f| options[:force] = f end o.on("-m", "--minimal", "Use minimal Vagrantfile template (no help comments). Ignored with --template") do |m| options[:minimal] = m end o.on("--output FILE", String, "Output path for the box. '-' for stdout") do |output| options[:output] = output end o.on("--template FILE", String, "Path to custom Vagrantfile template") do |template| options[:template] = template end end # Parse the options argv = parse_options(opts) return if !argv save_path = nil if options[:output] != "-" save_path = Pathname.new(options[:output]).expand_path(@env.cwd) save_path.delete if save_path.exist? && options[:force] raise Vagrant::Errors::VagrantfileExistsError if save_path.exist? end # Determine the template and template root to use template_root = "" if options[:template].nil? options[:template] = "Vagrantfile" if options[:minimal] options[:template] = "Vagrantfile.min" end template_root = ::Vagrant.source_root.join("templates/commands/init") end # Strip the .erb extension off the template if the user passes it in options[:template] = options[:template].chomp(".erb") # Make sure the template actually exists full_template_path = Vagrant::Util::TemplateRenderer.new(options[:template], template_root: template_root).full_template_path if !File.file?(full_template_path) raise Vagrant::Errors::VagrantfileTemplateNotFoundError, path: full_template_path end contents = Vagrant::Util::TemplateRenderer.render(options[:template], box_name: argv[0] || "base", box_url: argv[1], box_version: options[:box_version], template_root: template_root ) if save_path # Write out the contents begin save_path.open("w+") do |f| f.write(contents) end rescue Errno::EACCES raise Vagrant::Errors::VagrantfileWriteError end @env.ui.info(I18n.t("vagrant.commands.init.success"), prefix: false) else @env.ui.info(contents, prefix: false) end # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/init/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandInit class Plugin < Vagrant.plugin("2") name "init command" description <<-DESC The `init` command sets up your working directory to be a Vagrant-managed environment. DESC command("init") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/list-commands/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "optparse" module VagrantPlugins module CommandListCommands class Command < Vagrant.plugin("2", :command) def self.synopsis "outputs all available Vagrant subcommands, even non-primary ones" end def execute opts = OptionParser.new do |o| o.banner = "Usage: vagrant list-commands" end argv = parse_options(opts) return if !argv # Add the available subcommands as separators in order to print them # out as well. commands = {} longest = 0 Vagrant.plugin("2").manager.commands.each do |key, data| key = key.to_s klass = data[0].call commands[key] = klass.synopsis longest = key.length if key.length > longest end command_output = [] commands.keys.sort.each do |key| command_output << "#{key.ljust(longest+2)} #{commands[key]}" @env.ui.machine("cli-command", key.dup) end @env.ui.info( I18n.t("vagrant.list_commands", list: command_output.join("\n"))) # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/list-commands/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandListCommands class Plugin < Vagrant.plugin("2") name "list-commands command" description <<-DESC The `list-commands` command will list all commands that Vagrant understands, even hidden ones. DESC command("list-commands", primary: false) do require_relative "command" Command end end end end ================================================ FILE: plugins/commands/login/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module LoginCommand class Plugin < Vagrant.plugin("2") name "vagrant-login" description <<-DESC Provides the login command and internal API access to Vagrant Cloud. DESC command(:login) do require File.expand_path("../../cloud/auth/login", __FILE__) init! VagrantPlugins::CloudCommand::AuthCommand::Command::Login end def self.init! return if defined?(@_init) I18n.load_path << File.expand_path("../../cloud/locales/en.yml", __FILE__) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/commands/package/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require 'securerandom' module VagrantPlugins module CommandPackage class Command < Vagrant.plugin("2", :command) def self.synopsis "packages a running vagrant environment into a box" end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant package [options] [name|id]" o.separator "" o.separator "Options:" o.separator "" o.on("--base NAME", "Name of a VM in VirtualBox to package as a base box (VirtualBox Only)") do |b| options[:base] = b end o.on("--output NAME", "Name of the file to output") do |output| options[:output] = output end o.on("--include FILE,FILE..", Array, "Comma separated additional files to package with the box") do |i| options[:include] = i end o.on("--info FILE", "Path to a custom info.json file containing additional box information") do |info| options[:info] = info end o.on("--vagrantfile FILE", "Vagrantfile to package with the box") do |v| options[:vagrantfile] = v end end # Parse the options argv = parse_options(opts) return if !argv @logger.debug("package options: #{options.inspect}") if options[:base] package_base(options) else package_target(argv[0], options) end # Success, exit status 0 0 end protected def package_base(options) # XXX: This whole thing is hardcoded and very temporary. The whole # `vagrant package --base` process is deprecated for something much # better in the future. We just hardcode this to keep VirtualBox working # for now. provider = Vagrant.plugin("2").manager.providers[:virtualbox] tmp_data_directory = File.join(@env.tmp_path, SecureRandom.uuid) FileUtils.mkdir_p(tmp_data_directory) begin vm = Vagrant::Machine.new( options[:base], :virtualbox, provider[0], nil, provider[1], @env.vagrantfile.config, Pathname.new(tmp_data_directory), nil, @env, @env.vagrantfile, true) @logger.debug("Packaging base VM: #{vm.name}") package_vm(vm, options) ensure FileUtils.rm_rf(tmp_data_directory) end end def package_target(name, options) with_target_vms(name, single_target: true) do |vm| @logger.debug("Packaging VM: #{vm.name}") package_vm(vm, options) end end def package_vm(vm, options) opts = options.inject({}) do |acc, data| k,v = data acc["package.#{k}"] = v acc end env = vm.action(:package, opts) temp_dir = env["export.temp_dir"] ensure FileUtils.rm_rf(temp_dir) if temp_dir end end end end ================================================ FILE: plugins/commands/package/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandPackage class Plugin < Vagrant.plugin("2") name "package command" description <<-DESC The `package` command will take a previously existing Vagrant environment and package it into a box file. DESC command("package") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/plugin/action/expunge_plugins.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/plugin/manager" module VagrantPlugins module CommandPlugin module Action # This middleware removes user installed plugins by # removing: # * ~/.vagrant.d/plugins.json # * ~/.vagrant.d/gems # Usage should be restricted to when a repair is # unsuccessful and the only reasonable option remaining # is to re-install all plugins class ExpungePlugins def initialize(app, env) @app = app end def call(env) if !env[:force] result = nil attempts = 0 while attempts < 5 && result.nil? attempts += 1 result = env[:ui].ask( I18n.t("vagrant.commands.plugin.expunge_confirm") + " [N]: " ) result = result.to_s.downcase.strip result = "n" if result.empty? if !["y", "yes", "n", "no"].include?(result) result = nil env[:ui].error("Please answer Y or N") else result = result[0,1] end end if result != 'y' abort_action = true end end if !abort_action files = [] dirs = [] # Do not include global paths if local only if !env[:env_local_only] || env[:global_only] files << Vagrant::Plugin::Manager.instance.user_file.path dirs << Vagrant::Bundler.instance.plugin_gem_path end # Add local paths if they exist if Vagrant::Plugin::Manager.instance.local_file && (env[:env_local_only] || !env[:global_only]) files << Vagrant::Plugin::Manager.instance.local_file.path dirs << Vagrant::Bundler.instance.env_plugin_gem_path end # Expunge files and directories files.find_all(&:exist?).map(&:delete) dirs.find_all(&:exist?).map(&:rmtree) env[:ui].info(I18n.t("vagrant.commands.plugin.expunge_complete")) @app.call(env) else env[:ui].info(I18n.t("vagrant.commands.plugin.expunge_aborted")) end end end end end end ================================================ FILE: plugins/commands/plugin/action/install_gem.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/plugin/manager" require "vagrant/util/platform" module VagrantPlugins module CommandPlugin module Action # This action takes the `:plugin_name` variable in the environment # and installs it using the RubyGems API. class InstallGem def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::plugins::plugincommand::installgem") end def call(env) entrypoint = env[:plugin_entry_point] plugin_name = env[:plugin_name] sources = env[:plugin_sources] version = env[:plugin_version] env_local = env[:plugin_env_local] # Install the gem plugin_name_label = plugin_name plugin_name_label += " --version '#{version}'" if version env[:ui].info(I18n.t("vagrant.commands.plugin.installing", name: plugin_name_label)) manager = Vagrant::Plugin::Manager.instance plugin_spec = manager.install_plugin( plugin_name, version: version, require: entrypoint, sources: sources, verbose: !!env[:plugin_verbose], env_local: env_local ) # Record it so we can uninstall if something goes wrong @installed_plugin_name = plugin_spec.name # Tell the user env[:ui].success(I18n.t("vagrant.commands.plugin.installed", name: plugin_spec.name, version: plugin_spec.version.to_s)) # If the plugin's spec includes a post-install message display it post_install_message = plugin_spec.post_install_message if post_install_message if post_install_message.is_a?(Array) post_install_message = post_install_message.join(" ") end env[:ui].info(I18n.t("vagrant.commands.plugin.post_install", name: plugin_spec.name, message: post_install_message.to_s)) end # Continue @app.call(env) end def recover(env) # If any error happens, we uninstall it and remove it from # the state file. We can only do this if we successfully installed # the gem in the first place. if @installed_plugin_name new_env = env.dup new_env.delete(:interrupted) new_env[:plugin_name] = @installed_plugin_name new_env[:action_runner].run(Action.action_uninstall, new_env) end end end end end end ================================================ FILE: plugins/commands/plugin/action/license_plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "pathname" require "rubygems" require "set" require "log4r" module VagrantPlugins module CommandPlugin module Action # This middleware licenses a plugin by copying the license file to # the proper place. class LicensePlugin def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::plugins::plugincommand::license") end def call(env) # Verify the license file exists license_file = Pathname.new(env[:plugin_license_path]) if !license_file.file? raise Vagrant::Errors::PluginInstallLicenseNotFound, name: env[:plugin_name], path: license_file.to_s end # Copy it in. final_path = env[:home_path].join("license-#{env[:plugin_name]}.lic") @logger.info("Copying license from: #{license_file}") @logger.info("Copying license to: #{final_path}") env[:ui].info(I18n.t("vagrant.commands.plugin.installing_license", name: env[:plugin_name])) FileUtils.cp(license_file, final_path) # Installed! env[:ui].success(I18n.t("vagrant.commands.plugin.installed_license", name: env[:plugin_name])) @app.call(env) end end end end end ================================================ FILE: plugins/commands/plugin/action/list_plugins.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/plugin/manager" module VagrantPlugins module CommandPlugin module Action # This middleware lists all the installed plugins. # # This is a bit more complicated than simply listing installed # gems or what is in the state file as installed. Instead, this # actually compares installed gems with what the state file claims # is installed, and outputs the appropriate truly installed # plugins. class ListPlugins def initialize(app, env) @app = app end def call(env) manager = Vagrant::Plugin::Manager.instance plugins = manager.installed_plugins specs = Hash[ manager.installed_specs.map do |spec| [spec.name, spec] end ] # Output! if specs.empty? env[:ui].info(I18n.t("vagrant.commands.plugin.no_plugins")) return @app.call(env) end plugins.each do |plugin_name, plugin| spec = specs[plugin_name] next if spec.nil? meta = ", global" if plugin meta = ", system" if plugin["system"] meta = ", local" if plugin["env_local"] end env[:ui].info "#{spec.name} (#{spec.version}#{meta})" env[:ui].machine("plugin-name", spec.name) env[:ui].machine( "plugin-version", "#{spec.version}#{meta}", target: spec.name) if plugin["gem_version"] && plugin["gem_version"] != "" env[:ui].info(I18n.t( "vagrant.commands.plugin.plugin_version", version: plugin["gem_version"])) env[:ui].machine( "plugin-version-constraint", plugin["gem_version"], target: spec.name) end if plugin["require"] && plugin["require"] != "" env[:ui].info(I18n.t( "vagrant.commands.plugin.plugin_require", require: plugin["require"])) env[:ui].machine( "plugin-custom-entrypoint", plugin["require"], target: spec.name) end end @app.call(env) end end end end end ================================================ FILE: plugins/commands/plugin/action/plugin_exists_check.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/plugin/manager" module VagrantPlugins module CommandPlugin module Action # This class checks to see if the plugin is installed already, and # if so, raises an exception/error to output to the user. class PluginExistsCheck def initialize(app, env) @app = app end def call(env) installed = Vagrant::Plugin::Manager.instance.installed_plugins if !installed.key?(env[:plugin_name]) raise Vagrant::Errors::PluginNotInstalled, name: env[:plugin_name] end @app.call(env) end end end end end ================================================ FILE: plugins/commands/plugin/action/repair_plugins.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/plugin/manager" module VagrantPlugins module CommandPlugin module Action # This middleware attempts to repair installed plugins. # # In general, if plugins are failing to properly load the # core issue will likely be one of two issues: # 1. manual modifications within ~/.vagrant.d/ # 2. vagrant upgrade # Running an install on configured plugin set will most # likely fix these issues, which is all this action does. class RepairPlugins def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::plugins::plugincommand::repair") end def call(env) env[:ui].info(I18n.t("vagrant.commands.plugin.repairing")) plugins = Vagrant::Plugin::Manager.instance.globalize! begin ENV["VAGRANT_DISABLE_PLUGIN_INIT"] = nil Vagrant::Bundler.instance.init!(plugins, :repair) ENV["VAGRANT_DISABLE_PLUGIN_INIT"] = "1" env[:ui].info(I18n.t("vagrant.commands.plugin.repair_complete")) rescue => e @logger.error("Failed to repair user installed plugins: #{e.class} - #{e}") e.backtrace.each do |backtrace_line| @logger.debug(backtrace_line) end env[:ui].error(I18n.t("vagrant.commands.plugin.repair_failed", message: e.message)) end # Continue @app.call(env) end end class RepairPluginsLocal def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::plugins::plugincommand::repair_local") end def call(env) env[:ui].info(I18n.t("vagrant.commands.plugin.repairing_local")) Vagrant::Plugin::Manager.instance.localize!(env[:env]).each_pair do |pname, pinfo| env[:env].action_runner.run(Action.action_install, plugin_name: pname, plugin_entry_point: pinfo["require"], plugin_sources: pinfo["sources"], plugin_version: pinfo["gem_version"], plugin_env_local: true ) end env[:ui].info(I18n.t("vagrant.commands.plugin.repair_local_complete")) # Continue @app.call(env) end end end end end ================================================ FILE: plugins/commands/plugin/action/uninstall_plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommandPlugin module Action # This middleware uninstalls a plugin by simply removing it from # the state file. Running a {PruneGems} after should properly remove # it from the gem index. class UninstallPlugin def initialize(app, env) @app = app end def call(env) # Remove it! env[:ui].info(I18n.t("vagrant.commands.plugin.uninstalling", name: env[:plugin_name])) manager = Vagrant::Plugin::Manager.instance manager.uninstall_plugin(env[:plugin_name], env_local: env[:env_local]) @app.call(env) end end end end end ================================================ FILE: plugins/commands/plugin/action/update_gems.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/plugin/manager" module VagrantPlugins module CommandPlugin module Action class UpdateGems def initialize(app, env) @app = app end def call(env) names = env[:plugin_name] || [] if names.empty? env[:ui].info(I18n.t("vagrant.commands.plugin.updating")) else env[:ui].info(I18n.t("vagrant.commands.plugin.updating_specific", names: names.join(", "))) end manager = Vagrant::Plugin::Manager.instance installed_plugins = manager.installed_plugins new_specs = manager.update_plugins(names) updated_plugins = manager.installed_plugins updated = {} installed_plugins.each do |name, info| update = updated_plugins[name] if update && update["installed_gem_version"] != info["installed_gem_version"] updated[name] = update["installed_gem_version"] end end if updated.empty? env[:ui].success(I18n.t("vagrant.commands.plugin.up_to_date")) end updated.each do |name, version| env[:ui].success(I18n.t("vagrant.commands.plugin.updated", name: name, version: version.to_s)) end # Continue @app.call(env) end end end end end ================================================ FILE: plugins/commands/plugin/action.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "vagrant/action/builder" module VagrantPlugins module CommandPlugin module Action # This middleware sequence will remove all plugins. def self.action_expunge Vagrant::Action::Builder.new.tap do |b| b.use ExpungePlugins end end # This middleware sequence will install a plugin. def self.action_install Vagrant::Action::Builder.new.tap do |b| b.use InstallGem end end # This middleware sequence licenses paid addons. def self.action_license Vagrant::Action::Builder.new.tap do |b| b.use PluginExistsCheck b.use LicensePlugin end end # This middleware sequence will list all installed plugins. def self.action_list Vagrant::Action::Builder.new.tap do |b| b.use ListPlugins end end # This middleware sequence will repair installed plugins. def self.action_repair Vagrant::Action::Builder.new.tap do |b| b.use RepairPlugins end end # This middleware sequence will repair installed local plugins. def self.action_repair_local Vagrant::Action::Builder.new.tap do |b| b.use RepairPluginsLocal end end # This middleware sequence will uninstall a plugin. def self.action_uninstall Vagrant::Action::Builder.new.tap do |b| b.use PluginExistsCheck b.use UninstallPlugin end end # This middleware sequence will update a plugin. def self.action_update Vagrant::Action::Builder.new.tap do |b| b.use UpdateGems end end # The autoload farm action_root = Pathname.new(File.expand_path("../action", __FILE__)) autoload :ExpungePlugins, action_root.join("expunge_plugins") autoload :InstallGem, action_root.join("install_gem") autoload :LicensePlugin, action_root.join("license_plugin") autoload :ListPlugins, action_root.join("list_plugins") autoload :PluginExistsCheck, action_root.join("plugin_exists_check") autoload :RepairPlugins, action_root.join("repair_plugins") autoload :RepairPluginsLocal, action_root.join("repair_plugins") autoload :UninstallPlugin, action_root.join("uninstall_plugin") autoload :UpdateGems, action_root.join("update_gems") end end end ================================================ FILE: plugins/commands/plugin/command/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/plugin/state_file" module VagrantPlugins module CommandPlugin module Command class Base < Vagrant.plugin("2", :command) # This is a helper for executing an action sequence with the proper # environment hash setup so that the plugin specific helpers are # in. # # @param [Object] callable the Middleware callable # @param [Hash] env Extra environment hash that is merged in. def action(callable, env=nil) @env.action_runner.run(callable, env) end end end end end ================================================ FILE: plugins/commands/plugin/command/expunge.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require_relative "base" module VagrantPlugins module CommandPlugin module Command class Expunge < Base def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin expunge [-h]" o.on("--force", "Do not prompt for confirmation") do |force| options[:force] = force end o.on("--local", "Include plugins from local project for expunge") do |l| options[:env_local] = l end o.on("--local-only", "Only expunge local project plugins") do |l| options[:env_local_only] = l end o.on("--global-only", "Only expunge global plugins") do |l| options[:global_only] = l end o.on("--reinstall", "Reinstall current plugins after expunge") do |reinstall| options[:reinstall] = reinstall end end # Parse the options argv = parse_options(opts) return if !argv raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length > 0 plugins = Vagrant::Plugin::Manager.instance.installed_plugins if !options[:reinstall] && !options[:force] && !plugins.empty? result = nil attempts = 0 while attempts < 5 && result.nil? attempts += 1 result = @env.ui.ask( I18n.t("vagrant.commands.plugin.expunge_request_reinstall") + " [N]: " ) result = result.to_s.downcase.strip result = "n" if result.empty? if !["y", "yes", "n", "no"].include?(result) result = nil @env.ui.error("Please answer Y or N") else result = result[0,1] end end options[:reinstall] = result == "y" end # Remove all installed user plugins action(Action.action_expunge, options) if options[:reinstall] @env.ui.info(I18n.t("vagrant.commands.plugin.expunge_reinstall")) plugins.each do |plugin_name, plugin_info| next if plugin_info["system"] # system plugins do not require re-install # Rebuild information hash to use symbols plugin_info = Hash[ plugin_info.map do |key, value| ["plugin_#{key}".to_sym, value] end ] action( Action.action_install, plugin_info.merge( plugin_name: plugin_name ) ) end end # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/plugin/command/install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require_relative "base" require_relative "mixin_install_opts" module VagrantPlugins module CommandPlugin module Command class Install < Base include MixinInstallOpts LOCAL_INSTALL_PAUSE = 3 def execute options = { verbose: false } opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin install ... [-h]" o.separator "" build_install_opts(o, options) o.on("--local", "Install plugin for local project only") do |l| options[:env_local] = l end o.on("--verbose", "Enable verbose output for plugin installation") do |v| options[:verbose] = v end end # Parse the options argv = parse_options(opts) return if !argv if argv.length < 1 raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if !options[:env_local] errors = @env.vagrantfile.config.vagrant.validate(nil) if !errors["vagrant"].empty? raise Errors::ConfigInvalid, errors: Util::TemplateRenderer.render( "config/validation_failed", errors: errors) end local_plugins = @env.vagrantfile.config.vagrant.plugins plugin_list = local_plugins.map do |name, info| "#{name} (#{info.fetch(:version, "> 0")})" end.join("\n") @env.ui.info(I18n.t("vagrant.plugins.local.install_all", plugins: plugin_list) + "\n") # Pause to allow user to cancel sleep(LOCAL_INSTALL_PAUSE) local_plugins.each do |name, info| action(Action.action_install, plugin_entry_point: info[:entry_point], plugin_version: info[:version], plugin_sources: info[:sources] || Vagrant::Bundler::DEFAULT_GEM_SOURCES.dup, plugin_name: name, plugin_env_local: true ) end else # Install the gem argv.each do |name| action(Action.action_install, plugin_entry_point: options[:entry_point], plugin_version: options[:plugin_version], plugin_sources: options[:plugin_sources], plugin_name: name, plugin_verbose: options[:verbose], plugin_env_local: options[:env_local] ) end end # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/plugin/command/license.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require_relative "base" module VagrantPlugins module CommandPlugin module Command class License < Base def execute opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin license [-h]" end # Parse the options argv = parse_options(opts) return if !argv raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length < 2 # License the plugin action(Action.action_license, { plugin_license_path: argv[1], plugin_name: argv[0] }) # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/plugin/command/list.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require_relative "base" module VagrantPlugins module CommandPlugin module Command class List < Base def execute opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin list [-h]" # Stub option to allow Vagrantfile loading o.on("--local", "Include local project plugins"){|_|} end # Parse the options argv = parse_options(opts) return if !argv raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length > 0 # List the installed plugins action(Action.action_list) # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/plugin/command/mixin_install_opts.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommandPlugin module Command module MixinInstallOpts def build_install_opts(o, options) options[:plugin_sources] = Vagrant::Bundler::DEFAULT_GEM_SOURCES.dup 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 end o.on("--plugin-clean-sources", "Remove all plugin sources defined so far (including defaults)") do |clean| options[:plugin_sources] = [] if clean end o.on("--plugin-source PLUGIN_SOURCE", String, "Add a RubyGems repository source") do |plugin_source| options[:plugin_sources] << plugin_source end o.on("--plugin-version PLUGIN_VERSION", String, "Install a specific version of the plugin") do |plugin_version| options[:plugin_version] = plugin_version end end end end end end ================================================ FILE: plugins/commands/plugin/command/repair.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require_relative "base" module VagrantPlugins module CommandPlugin module Command class Repair < Base def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin repair [-h]" o.on("--local", "Repair plugins in local project") do |l| options[:env_local] = l end end # Parse the options argv = parse_options(opts) return if !argv raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length > 0 if Vagrant::Plugin::Manager.instance.local_file action(Action.action_repair_local, env: @env) end # Attempt to repair installed plugins action(Action.action_repair, options) # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/plugin/command/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandPlugin module Command class Root < Vagrant.plugin("2", :command) def self.synopsis "manages plugins: install, uninstall, update, etc." end def initialize(argv, env) super @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) @subcommands = Vagrant::Registry.new @subcommands.register(:expunge) do require_relative "expunge" Expunge end @subcommands.register(:install) do require_relative "install" Install end @subcommands.register(:license) do require_relative "license" License end @subcommands.register(:list) do require_relative "list" List end @subcommands.register(:repair) do require_relative "repair" Repair end @subcommands.register(:update) do require_relative "update" Update end @subcommands.register(:uninstall) do require_relative "uninstall" Uninstall end end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Print the help for all the sub-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 |o| o.banner = "Usage: vagrant plugin []" o.separator "" o.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| o.separator " #{key}" end o.separator "" o.separator "For help on any individual command run `vagrant plugin COMMAND -h`" end @env.ui.info(opts.help, prefix: false) end end end end end ================================================ FILE: plugins/commands/plugin/command/uninstall.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require_relative "base" module VagrantPlugins module CommandPlugin module Command class Uninstall < Base def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin uninstall [ ...] [-h]" o.on("--local", "Remove plugin from local project") do |l| options[:env_local] = l end end # Parse the options argv = parse_options(opts) return if !argv raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length < 1 # Uninstall the gems argv.each do |gem| action(Action.action_uninstall, plugin_name: gem, env_local: options[:env_local]) end # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/plugin/command/update.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require_relative "base" require_relative "mixin_install_opts" module VagrantPlugins module CommandPlugin module Command class Update < Base include MixinInstallOpts def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant plugin update [names...] [-h]" o.separator "" o.on("--local", "Update plugin in local project") do |l| options[:env_local] = l end end # Parse the options argv = parse_options(opts) return if !argv # Update the gem action(Action.action_update, { plugin_name: argv, env_local: options[:env_local] }) # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/plugin/gem_helper.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "rubygems" require "rubygems/config_file" require "rubygems/gem_runner" require "log4r" module VagrantPlugins module CommandPlugin # This class provides methods to help with calling out to the # `gem` command but using the RubyGems API. class GemHelper def initialize(gem_home) @gem_home = gem_home.to_s @logger = Log4r::Logger.new("vagrant::plugins::plugincommand::gemhelper") end # This will yield the given block with the proper ENV setup so # that RubyGems only sees the gems in the Vagrant-managed gem # path. def with_environment old_gem_home = ENV["GEM_HOME"] old_gem_path = ENV["GEM_PATH"] ENV["GEM_HOME"] = @gem_home ENV["GEM_PATH"] = @gem_home @logger.debug("Set GEM_* to: #{ENV["GEM_HOME"]}") # Clear paths so that it reads the new GEM_HOME setting Gem.paths = ENV # Set a custom configuration to avoid loading ~/.gemrc loads and # /etc/gemrc and so on. old_config = nil begin old_config = Gem.configuration rescue Psych::SyntaxError # Just ignore this. This means that the ".gemrc" file has # an invalid syntax and can't be loaded. We don't care, because # when we set Gem.configuration to nil later, it'll force a reload # if it is needed. end Gem.configuration = NilGemConfig.new # Clear the sources so that installation uses custom sources old_sources = Gem.sources Gem.sources = Gem.default_sources Vagrant::Bundler::DEFAULT_GEM_SOURCES.each do |source| if !Gem.sources.include?(source) Gem.sources << source end end # Use a silent UI so that we have no output Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) do return yield end ensure # Restore the old GEM_* settings ENV["GEM_HOME"] = old_gem_home ENV["GEM_PATH"] = old_gem_path # Reset everything Gem.configuration = old_config Gem.paths = ENV Gem.sources = old_sources.to_a end # This is pretty hacky but it is a custom implementation of # Gem::ConfigFile so that we don't load any gemrc files. class NilGemConfig < Gem::ConfigFile def initialize # We _can not_ `super` here because that can really mess up # some other configuration state. We need to just set everything # directly. @api_keys = {} @args = [] @backtrace = false @bulk_threshold = 1000 @hash = {} @update_sources = true @verbose = true end end end end end ================================================ FILE: plugins/commands/plugin/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandPlugin class Plugin < Vagrant.plugin("2") name "plugin command" description <<-DESC This command helps manage and install plugins within the Vagrant environment. DESC command("plugin") do require File.expand_path("../command/root", __FILE__) Command::Root end end autoload :Action, File.expand_path("../action", __FILE__) end end ================================================ FILE: plugins/commands/port/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/presence" require "optparse" module VagrantPlugins module CommandPort class Command < Vagrant.plugin("2", :command) include Vagrant::Util::Presence def self.synopsis "displays information about guest port mappings" end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant port [options] [name|id]" o.separator "" o.separator "Options:" o.separator "" o.on("--guest PORT", "Output the host port that maps to the given guest port") do |port| options[:guest] = port end end # Parse the options argv = parse_options(opts) return if !argv with_target_vms(argv, single_target: true) do |vm| vm.action_raw(:config_validate, Vagrant::Action::Builtin::ConfigValidate) if !vm.provider.capability?(:forwarded_ports) @env.ui.error(I18n.t("port_command.missing_capability", provider: vm.provider_name, )) return 1 end ports = vm.provider.capability(:forwarded_ports) if !present?(ports) @env.ui.info(I18n.t("port_command.empty_ports")) return 0 end if present?(options[:guest]) return print_single(vm, ports, options[:guest]) else return print_all(vm, ports) end end end private # Print all the guest <=> host port mappings. # @return [0] the exit code def print_all(vm, ports) @env.ui.info(I18n.t("port_command.details")) @env.ui.info("") ports.each do |host, guest| @env.ui.info("#{guest.to_s.rjust(6)} (guest) => #{host} (host)") @env.ui.machine("forwarded_port", guest, host, target: vm.name.to_s) end return 0 end # Print the host mapping that matches the given guest target. # @return [0,1] the exit code def print_single(vm, ports, target) map = ports.find { |_, guest| "#{guest}" == "#{target}" } if !present?(map) @env.ui.error(I18n.t("port_command.no_matching_port", port: target, )) return 1 end @env.ui.info("#{map[0]}") return 0 end end end end ================================================ FILE: plugins/commands/port/locales/en.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: port_command: details: |- The forwarded ports for the machine are listed below. Please note that these values may differ from values configured in the Vagrantfile if the provider supports automatic port collision detection and resolution. empty_ports: |- The provider reported there are no forwarded ports for this virtual machine. This can be caused if there are no ports specified in the Vagrantfile or if the virtual machine is not currently running. Please check that the virtual machine is running and try again. missing_capability: |- The %{provider} provider does not support listing forwarded ports. This is most likely a limitation of the provider and not a bug in Vagrant. If you believe this is a bug in Vagrant, please search existing issues before opening a new one. no_matching_port: |- The guest is not currently mapping port %{port} to the host machine. Is the port configured in the Vagrantfile? You may need to run `vagrant reload` if changes were made to the port configuration in the Vagrantfile. ================================================ FILE: plugins/commands/port/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandPort class Plugin < Vagrant.plugin("2") name "port command" description <<-DESC The `port` command displays guest port mappings. DESC command("port") do require_relative "command" self.init! Command end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path("../locales/en.yml", __FILE__) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/commands/powershell/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "optparse" require "vagrant/util/powershell" 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 powershell [-- extra powershell args]" o.separator "" o.separator "Opens a PowerShell session on the host to the guest" o.separator "machine if both support powershell remoting." o.separator "" o.separator "Options:" o.separator "" o.on("-c", "--command COMMAND", "Execute a powershell command directly") do |c| options[:command] = c end o.on("-e", "--elevated", "Execute a powershell command with elevated permissions") do |c| options[:elevated] = true 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 # Elevated option enabled means we can only execute commands raise Errors::ElevatedNoCommand if !options[:command] && options[:elevated] # 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 options[:command] if machine.config.vm.communicator != :winrm raise VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady end out_code = machine.communicate.execute(options[:command].dup, elevated: options[:elevated]) do |type,data| machine.ui.detail(data) if type == :stdout end if out_code == 0 machine.ui.success("Command: #{options[:command]} executed successfully with output code #{out_code}.") end next end # Check if the host even supports ps remoting raise Errors::HostUnsupported if !@env.host.capability?(:ps_client) 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"] 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.resetting")) 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 ================================================ FILE: plugins/commands/powershell/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommandPS module Errors # A convenient superclass for all our errors. class PSCommandError < Vagrant::Errors::VagrantError error_namespace("vagrant_ps.errors") end class HostUnsupported < PSCommandError error_key(:host_unsupported) end class PSRemotingUndetected < PSCommandError error_key(:ps_remoting_undetected) end class PowerShellError < PSCommandError error_key(:powershell_error) end class ElevatedNoCommand < PSCommandError error_key(:elevated_no_command) end end end end ================================================ FILE: plugins/commands/powershell/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandPS autoload :Errors, File.expand_path("../errors", __FILE__) class Plugin < Vagrant.plugin("2") name "powershell command" description <<-DESC The powershell command opens a remote PowerShell session to the machine if it supports powershell remoting. DESC command("powershell") do require_relative "command" 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.load_path << File.expand_path("templates/locales/comm_winrm.yml", Vagrant.source_root) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/commands/powershell/scripts/enable_psremoting.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 account 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) ================================================ FILE: plugins/commands/powershell/scripts/reset_trustedhosts.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 ================================================ FILE: plugins/commands/provider/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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] @env.ui.output(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] key = "provider_install_#{machine.provider_name}".to_sym if !@env.host.capability?(key) raise Vagrant::Errors::ProviderCantInstall, provider: machine.provider_name.to_s end @env.host.capability(key) return end # No subtask, just output the provider name @env.ui.output(machine.provider_name.to_s) # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/provider/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 ================================================ FILE: plugins/commands/provision/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandProvision class Command < Vagrant.plugin("2", :command) def self.synopsis "provisions the vagrant machine" end def execute options = {} options[:provision_types] = nil opts = OptionParser.new do |o| o.banner = "Usage: vagrant provision [vm-name] [--provision-with x,y,z]" o.on("--provision-with x,y,z", Array, "Enable only certain provisioners, by type or by name.") do |list| options[:provision_types] = list.map { |type| type.to_sym } end end # Parse the options argv = parse_options(opts) return if !argv # Go over each VM and provision! @logger.debug("'provision' each target VM...") with_target_vms(argv) do |machine| machine.action(:provision, options) end # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/provision/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandProvision class Plugin < Vagrant.plugin("2") name "provision command" description <<-DESC The `provision` command provisions your virtual machine based on the configuration of the Vagrantfile. DESC command("provision") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/push/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandPush class Command < Vagrant.plugin("2", :command) def self.synopsis "deploys code in this environment to a configured destination" end # @todo support multiple strategies if requested by the community def execute opts = OptionParser.new do |o| o.banner = "Usage: vagrant push [strategy] [options]" end # Parse the options argv = parse_options(opts) return if !argv name = validate_pushes!(@env.pushes, argv[0]) # Validate the configuration @env.machine(@env.machine_names.first, @env.default_provider).action_raw( :config_validate, Vagrant::Action::Builtin::ConfigValidate) @logger.debug("'push' environment with strategy: `#{name}'") @env.push(name) 0 end # Validate that the given list of names corresponds to valid pushes. # # @raise Vagrant::Errors::PushesNotDefined # if there are no pushes defined # @raise Vagrant::Errors::PushStrategyNotProvided # if there are multiple push strategies defined and none were specified # @raise Vagrant::Errors::PushStrategyNotDefined # if the given push name do not correspond to a push strategy # # @param [Array] pushes # the list of pushes defined by the environment # @param [String] name # the name provided by the user on the command line # # @return [Symbol] # the compiled list of pushes # def validate_pushes!(pushes, name = nil) if pushes.nil? || pushes.empty? raise Vagrant::Errors::PushesNotDefined end if name.nil? if pushes.length == 1 return pushes.first.to_sym else raise Vagrant::Errors::PushStrategyNotProvided, pushes: pushes.map(&:to_s) end end name = name.to_sym if !pushes.include?(name) raise Vagrant::Errors::PushStrategyNotDefined, name: name.to_s, pushes: pushes.map(&:to_s) end return name end end end end ================================================ FILE: plugins/commands/push/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandPush class Plugin < Vagrant.plugin("2") name "push command" description <<-DESC The `push` command deploys code in this environment. DESC command("push") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/rdp/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "optparse" module VagrantPlugins module CommandRDP class Command < Vagrant.plugin("2", :command) def self.synopsis "connects to machine via RDP" end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant rdp [options] [name|id] [-- extra args]" end # Parse out the extra args to send to the RDP client, 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 RDP raise Errors::HostUnsupported if !@env.host.capability?(:rdp_client) # Execute RDP if we can with_target_vms(argv, single_target: true) do |machine| if !machine.communicate.ready? raise Vagrant::Errors::VMNotCreatedError end machine.ui.output(I18n.t("vagrant_rdp.detecting")) rdp_info = get_rdp_info(machine) raise Errors::RDPUndetected if !rdp_info # Extra arguments if we have any rdp_info[:extra_args] = options[:extra_args] machine.ui.detail( "Address: #{rdp_info[:host]}:#{rdp_info[:port]}") machine.ui.detail("Username: #{rdp_info[:username]}") machine.ui.success(I18n.t("vagrant_rdp.connecting")) @env.host.capability(:rdp_client, rdp_info) end end protected def get_rdp_info(machine) rdp_info = {} if machine.provider.capability?(:rdp_info) rdp_info = machine.provider.capability(:rdp_info) rdp_info ||= {} end ssh_info = machine.ssh_info if !rdp_info[:username] username = ssh_info[:username] if machine.config.vm.communicator == :winrm username = machine.config.winrm.username end rdp_info[:username] = username end if !rdp_info[:password] password = ssh_info[:password] if machine.config.vm.communicator == :winrm password = machine.config.winrm.password end rdp_info[:password] = password end rdp_info[:host] ||= ssh_info[:host] rdp_info[:port] ||= machine.config.rdp.port rdp_info[:username] ||= machine.config.rdp.username if rdp_info[:host] == "127.0.0.1" # We need to find a forwarded port... search_port = machine.config.rdp.search_port ports = nil if machine.provider.capability?(:forwarded_ports) ports = machine.provider.capability(:forwarded_ports) else ports = {}.tap do |result| machine.config.vm.networks.each do |type, netopts| next if type != :forwarded_port next if !netopts[:host] result[netopts[:host]] = netopts[:guest] end end end ports = ports.invert port = ports[search_port] rdp_info[:port] = port return nil if !port end return rdp_info end end end end ================================================ FILE: plugins/commands/rdp/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommandRDP class Config < Vagrant.plugin("2", :config) attr_accessor :port attr_accessor :search_port attr_accessor :username def initialize @port = UNSET_VALUE @search_port = UNSET_VALUE @username = UNSET_VALUE end def finalize! @port = 3389 if @port == UNSET_VALUE @search_port = 3389 if @search_port == UNSET_VALUE @username = nil if @username == UNSET_VALUE end def validate(machine) errors = _detected_errors { "RDP" => errors } end end end end ================================================ FILE: plugins/commands/rdp/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommandRDP module Errors # A convenient superclass for all our errors. class RDPError < Vagrant::Errors::VagrantError error_namespace("vagrant_rdp.errors") end class HostUnsupported < RDPError error_key(:host_unsupported) end class RDPUndetected < RDPError error_key(:rdp_undetected) end end end end ================================================ FILE: plugins/commands/rdp/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandRDP autoload :Errors, File.expand_path("../errors", __FILE__) class Plugin < Vagrant.plugin("2") name "rdp command" description <<-DESC The rdp command opens a remote desktop Window to the machine if it supports RDP. DESC command("rdp") do require File.expand_path("../command", __FILE__) init! Command end config("rdp") do require_relative "config" Config end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path( "templates/locales/command_rdp.yml", Vagrant.source_root) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/commands/reload/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require "vagrant" require Vagrant.source_root.join("plugins/commands/up/start_mixins") module VagrantPlugins module CommandReload class Command < Vagrant.plugin("2", :command) # We assume that the `up` plugin exists and that we'll have access # to this. include VagrantPlugins::CommandUp::StartMixins def self.synopsis "restarts vagrant machine, loads new Vagrantfile configuration" end def execute options = {} options[:provision_ignore_sentinel] = false opts = OptionParser.new do |o| o.banner = "Usage: vagrant reload [vm-name]" o.separator "" build_start_options(o, options) o.on("-f", "--force", "Force shut down (equivalent of pulling power)") do |f| options[:force_halt] = f end end # Parse the options argv = parse_options(opts) return if !argv # Validate the provisioners validate_provisioner_flags!(options, argv) @logger.debug("'reload' each target VM...") machines = [] with_target_vms(argv) do |machine| machines << machine machine.action(:reload, options) end # Output the post-up messages that we have, if any machines.each do |m| next if !m.config.vm.post_up_message next if m.config.vm.post_up_message == "" # Add a newline to separate things. @env.ui.info("", prefix: false) m.ui.success(I18n.t( "vagrant.post_up_message", name: m.name.to_s, message: m.config.vm.post_up_message)) end # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/reload/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandReload class Plugin < Vagrant.plugin("2") name "reload command" description <<-DESC The `reload` command will halt, reconfigure your machine based on the Vagrantfile, and bring it back up. DESC command("reload") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/resume/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require Vagrant.source_root.join("plugins/commands/up/start_mixins") module VagrantPlugins module CommandResume class Command < Vagrant.plugin("2", :command) # We assume that the `up` plugin exists and that we'll have access # to this. include VagrantPlugins::CommandUp::StartMixins def self.synopsis "resume a suspended vagrant machine" end def execute options = {} options[:provision_ignore_sentinel] = false opts = OptionParser.new do |o| o.banner = "Usage: vagrant resume [vm-name]" o.separator "" build_start_options(o, options) end # Parse the options argv = parse_options(opts) return if !argv # Validate the provisioners validate_provisioner_flags!(options, argv) @logger.debug("'resume' each target VM...") with_target_vms(argv) do |machine| machine.action(:resume, options) end # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/resume/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandResume class Plugin < Vagrant.plugin("2") name "resume command" description <<-DESC The `resume` command resumes a suspend virtual machine. DESC command("resume") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/snapshot/command/delete.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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| if !vm.provider.capability?(:snapshot_list) raise Vagrant::Errors::SnapshotNotSupported end snapshot_list = vm.provider.capability(:snapshot_list) if snapshot_list.include? name vm.action(:snapshot_delete, snapshot_name: name) else raise Vagrant::Errors::SnapshotNotFound, snapshot_name: name, machine: vm.name.to_s end end # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/snapshot/command/list.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandSnapshot module Command class List < Vagrant.plugin("2", :command) def execute 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| if !vm.id vm.ui.info(I18n.t("vagrant.commands.common.vm_not_created")) next end if !vm.provider.capability?(:snapshot_list) raise Vagrant::Errors::SnapshotNotSupported 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 vm.ui.output("", prefix: true) snapshots.each do |snapshot| vm.ui.detail(snapshot, prefix: false) end end # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/snapshot/command/pop.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'json' require 'optparse' require Vagrant.source_root.join("plugins/commands/up/start_mixins") require_relative "push_shared" module VagrantPlugins module CommandSnapshot module Command class Pop < Vagrant.plugin("2", :command) include PushShared include VagrantPlugins::CommandUp::StartMixins def execute options = {} options[:snapshot_delete] = true options[:provision_ignore_sentinel] = false options[:snapshot_start] = true opts = OptionParser.new do |o| o.banner = "Usage: vagrant snapshot pop [options] [vm-name]" o.separator "" o.separator "Restore state that was pushed onto the snapshot stack" o.separator "with `vagrant snapshot push`." o.separator "" build_start_options(o, options) o.on("--no-delete", "Don't delete the snapshot after the restore") do options[:snapshot_delete] = false end o.on("--no-start", "Don't start the snapshot after the restore") do options[:snapshot_start] = false end end # Parse the options argv = parse_options(opts) return if !argv # Validate the provisioners validate_provisioner_flags!(options, argv) return shared_exec(argv, method(:pop), options) end end end end end ================================================ FILE: plugins/commands/snapshot/command/push.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 ================================================ FILE: plugins/commands/snapshot/command/push_shared.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'json' module VagrantPlugins module CommandSnapshot module Command module PushShared def shared_exec(argv, m, opts={}) 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, opts) end end # Success, exit with 0 0 end def push(machine, opts={}) 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) end def pop(machine, opts={}) # 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 # 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, if required opts[:snapshot_name] = name machine.action(:snapshot_restore, opts) end end end end end ================================================ FILE: plugins/commands/snapshot/command/restore.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require Vagrant.source_root.join("plugins/commands/up/start_mixins") module VagrantPlugins module CommandSnapshot module Command class Restore < Vagrant.plugin("2", :command) include VagrantPlugins::CommandUp::StartMixins def execute options = {} options[:provision_ignore_sentinel] = false options[:snapshot_start] = true opts = OptionParser.new do |o| o.banner = "Usage: vagrant snapshot restore [options] [vm-name] " o.separator "" build_start_options(o, options) o.separator "Restore a snapshot taken previously with snapshot save." o.on("--no-start", "Don't start the snapshot after the restore") do options[:snapshot_start] = false end 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 # Validate the provisioners validate_provisioner_flags!(options, argv) name = argv.pop options[:snapshot_name] = name with_target_vms(argv) do |vm| if !vm.provider.capability?(:snapshot_list) raise Vagrant::Errors::SnapshotNotSupported end snapshot_list = vm.provider.capability(:snapshot_list) if snapshot_list.include? name vm.action(:snapshot_restore, options) else raise Vagrant::Errors::SnapshotNotFound, snapshot_name: name, machine: vm.name.to_s end end # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/snapshot/command/root.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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_relative "save" Save end @subcommands.register(:restore) do require_relative "restore" Restore end @subcommands.register(:delete) do require_relative "delete" Delete end @subcommands.register(:list) do require_relative "list" List end @subcommands.register(:push) do require_relative "push" Push end @subcommands.register(:pop) do require_relative "pop" Pop end end def execute if @main_args.include?("-h") || @main_args.include?("--help") # Print the help for all the commands. @env.ui.info(help, prefix: false) return 0 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 if !command_class || !@sub_command raise Vagrant::Errors::CLIInvalidUsage, help: help() end @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 opts.help end end end end end ================================================ FILE: plugins/commands/snapshot/command/save.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandSnapshot module Command class Save < Vagrant.plugin("2", :command) def execute options = {} options[:force] = false 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 "If no vm-name is given, Vagrant will take a snapshot of" o.separator "the entire environment with the same snapshot name." o.separator "" o.separator "Snapshots are useful for experimenting in a machine and being able" o.separator "to rollback quickly." o.on("-f", "--force", "Replace snapshot without confirmation") do |f| options[:force] = f end 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| if !vm.provider.capability?(:snapshot_list) raise Vagrant::Errors::SnapshotNotSupported end # In this case, no vm name was given, and we are iterating over the # entire environment. If a vm hasn't been created yet, we can't list # its snapshots if vm.id.nil? @env.ui.warn(I18n.t("vagrant.commands.snapshot.save.vm_not_created", name: vm.name)) next end snapshot_list = vm.provider.capability(:snapshot_list) if !snapshot_list.include? name vm.action(:snapshot_save, snapshot_name: name) elsif options[:force] # not a unique snapshot name vm.action(:snapshot_delete, snapshot_name: name) vm.action(:snapshot_save, snapshot_name: name) else raise Vagrant::Errors::SnapshotConflictFailed end end # Success, exit status 0 0 end end end end end ================================================ FILE: plugins/commands/snapshot/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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_relative "command/root" Command::Root end end end end ================================================ FILE: plugins/commands/ssh/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandSSH class Command < Vagrant.plugin("2", :command) def self.synopsis "connects to machine via SSH" end def execute options = {} options[:tty] = true opts = OptionParser.new do |o| o.banner = "Usage: vagrant ssh [options] [name|id] [-- extra ssh args]" o.separator "" o.separator "Options:" o.separator "" o.on("-c", "--command COMMAND", "Execute an SSH command directly") do |c| options[:command] = c end o.on("-p", "--plain", "Plain mode, leaves authentication up to user") do |p| options[:plain_mode] = p end o.on("-t", "--[no-]tty", "Enables tty when executing an ssh command (defaults to true)") do |t| options[:tty] = t end end # Parse out the extra args to send to SSH, which is everything # after the "--" split_index = @argv.index("--") if split_index options[:ssh_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 # Execute the actual SSH with_target_vms(argv, single_target: true) do |vm| ssh_opts = { plain_mode: options[:plain_mode], extra_args: options[:ssh_args] } if options[:command] @logger.debug("Executing single command on remote machine: #{options[:command]}") env = vm.action(:ssh_run, ssh_opts: ssh_opts, ssh_run_command: options[:command], tty: options[:tty],) # Exit with the exit status of the command or a 0 if we didn't # get one. exit_status = env[:ssh_run_exit_status] || 0 return exit_status else Vagrant::Bundler.instance.deinit @logger.debug("Invoking `ssh` action on machine") vm.action(:ssh, ssh_opts: ssh_opts) # We should never reach this point, since the point of `ssh` # is to exec into the proper SSH shell, but we'll just return # an exit status of 0 anyways. return 0 end end end end end end ================================================ FILE: plugins/commands/ssh/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandSSH class Plugin < Vagrant.plugin("2") name "ssh command" description <<-DESC The `ssh` command allows you to SSH in to your running virtual machine. DESC command("ssh") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/ssh_config/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require "vagrant/util/safe_puts" require "vagrant/util/platform" module VagrantPlugins module CommandSSHConfig class Command < Vagrant.plugin("2", :command) include Vagrant::Util::SafePuts def self.synopsis "outputs OpenSSH valid configuration to connect to the machine" end def convert_win_paths(paths) paths.map! { |path| Vagrant::Util::Platform.format_windows_path(path, :disable_unc) } end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant ssh-config [options] [name|id]" o.separator "" o.separator "Options:" o.separator "" o.on("--host NAME", "Name the host for the config") do |h| options[:host] = h end end argv = parse_options(opts) return if !argv with_target_vms(argv) do |machine| ssh_info = machine.ssh_info raise Vagrant::Errors::SSHNotReady if ssh_info.nil? if Vagrant::Util::Platform.windows? ssh_info[:private_key_path] = convert_win_paths(ssh_info[:private_key_path]) end variables = { host_key: options[:host] || machine.name || "vagrant", ssh_host: ssh_info[:host], ssh_port: ssh_info[:port], ssh_user: ssh_info[:username], keys_only: ssh_info[:keys_only], verify_host_key: ssh_info[:verify_host_key], private_key_path: ssh_info[:private_key_path], log_level: ssh_info[:log_level], forward_agent: ssh_info[:forward_agent], forward_x11: ssh_info[:forward_x11], proxy_command: ssh_info[:proxy_command], ssh_command: ssh_info[:ssh_command], forward_env: ssh_info[:forward_env], config: ssh_info[:config], disable_deprecated_algorithms: ssh_info[:disable_deprecated_algorithms] } # Render the template and output directly to STDOUT template = "commands/ssh_config/config" config = Vagrant::Util::TemplateRenderer.render(template, variables) machine.ui.machine("ssh-config", config) safe_puts(config) safe_puts end # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/ssh_config/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandSSHConfig class Plugin < Vagrant.plugin("2") name "ssh-config command" description <<-DESC The `ssh-config` command dumps an OpenSSH compatible configuration that can be used to quickly SSH into your virtual machine. DESC command("ssh-config") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/status/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandStatus class Command < Vagrant.plugin("2", :command) def self.synopsis "outputs status of the vagrant machine" end def execute opts = OptionParser.new do |o| o.banner = "Usage: vagrant status [name|id]" end # Parse the options argv = parse_options(opts) return if !argv max_name_length = 25 with_target_vms(argv) do |machine| max_name_length = machine.name.length if machine.name.length > max_name_length end state = nil results = [] with_target_vms(argv) do |machine| state = machine.state if !state current_state = machine.state results << "#{machine.name.to_s.ljust(max_name_length)} " + "#{current_state.short_description} (#{machine.provider_name})" opts = { target: machine.name.to_s } @env.ui.machine("provider-name", machine.provider_name, opts) @env.ui.machine("state", current_state.id, opts) @env.ui.machine("state-human-short", current_state.short_description, opts) @env.ui.machine("state-human-long", current_state.long_description, opts) end message = nil if results.length == 1 message = state.long_description else message = I18n.t("vagrant.commands.status.listing") end @env.ui.info(I18n.t("vagrant.commands.status.output", states: results.join("\n"), message: message), prefix: false) # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/status/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandStatus class Plugin < Vagrant.plugin("2") name "status command" description <<-DESC The `status` command shows what the running state (running/saved/..) is of all your virtual machines in this environment. DESC command("status") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/suspend/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandSuspend class Command < Vagrant.plugin("2", :command) def self.synopsis "suspends the machine" end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant suspend [options] [name|id]" o.separator "" o.on("-a", "--all-global", "Suspend all running vms globally.") do |p| options[:all] = true end end # Parse the options argv = parse_options(opts) return if !argv @logger.debug("'suspend' each target VM...") target = [] if options[:all] if argv.size > 0 raise Vagrant::Errors::CommandSuspendAllArgs end m = @env.machine_index.each { |m| m } target = m.keys else target = argv end with_target_vms(target) do |vm| vm.action(:suspend) end # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/suspend/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandSuspend class Plugin < Vagrant.plugin("2") name "suspend command" description <<-DESC The `suspend` command suspends execution and puts it to sleep. The command `resume` returns it to running status. DESC command("suspend") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/up/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require 'set' require File.expand_path("../start_mixins", __FILE__) module VagrantPlugins module CommandUp class Command < Vagrant.plugin("2", :command) include StartMixins def self.synopsis "starts and provisions the vagrant environment" end def execute options = {} options[:destroy_on_error] = true options[:install_provider] = true options[:parallel] = true options[:provision_ignore_sentinel] = false opts = OptionParser.new do |o| o.banner = "Usage: vagrant up [options] [name|id]" o.separator "" o.separator "Options:" o.separator "" build_start_options(o, options) o.on("--[no-]destroy-on-error", "Destroy machine if any fatal error happens (default to true)") do |destroy| options[:destroy_on_error] = destroy end o.on("--[no-]parallel", "Enable or disable parallelism if provider supports it") do |parallel| options[:parallel] = parallel end o.on("--provider PROVIDER", String, "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 argv = parse_options(opts) return if !argv # Validate the provisioners validate_provisioner_flags!(options, argv) # Go over each VM and bring it up @logger.debug("'Up' each target VM...") # 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 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 # To prevent vagrant from attempting to validate a global vms config # (which doesn't exist within the local dir) when attempting to # install a machines provider, this check below will disable the # install_providers function if a user gives us a machine id instead # of the machines name. machine_names = [] with_target_vms(names, provider: options[:provider]){|m| machine_names << m.name } options[:install_provider] = false if !(machine_names - names).empty? # 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, provider: options[:provider]) 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", name: machine.name, provider: machine.provider_name)) machines << machine batch.action(machine, :up, options) end end end if machines.empty? @env.ui.info(I18n.t("vagrant.up_no_machines")) return 0 end # Output the post-up messages that we have, if any machines.each do |m| next if !m.config.vm.post_up_message next if m.config.vm.post_up_message == "" # Add a newline to separate things. @env.ui.info("", prefix: false) m.ui.success(I18n.t( "vagrant.post_up_message", name: m.name.to_s, message: m.config.vm.post_up_message)) end # Success, exit status 0 0 end protected def install_providers(names, provider: nil) # 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 with_target_vms(names, provider: provider) do |machine| # Check if we have this machine in the index entry = @env.machine_index.get(machine.name.to_s) # Get the provider for this machine. This logic isn't completely # straightforward. If we have a forced provider, we always use # that no matter what. If we have an entry in the index (meaning # the machine may be created), we use that provider no matter # what since that will be used by the core. If we have none, then # we ask the Vagrant env what the default provider would be and use # that. # # Note that this logic is a bit redundant if we have "provider" # set but I think its probably cleaner to put this logic in one # place. p = provider p = entry.provider.to_sym if !p && entry p = @env.default_provider( machine: machine.name.to_sym, check_usable: false) if !p # Add it to the set providers.add(p) 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 ================================================ FILE: plugins/commands/up/middleware/store_box_metadata.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" module VagrantPlugins module CommandUp # Stores metadata information about the box used # for the current guest. This allows Vagrant to # determine the box currently in use when the # Vagrantfile is modified with a new box name or # version while the guest still exists. class StoreBoxMetadata def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::up::storeboxmetadata") end def call(env) if env[:machine].box box = env[:machine].box box_meta = { name: box.name, version: box.version, provider: box.provider, directory: box.directory.sub(Vagrant.user_data_path.to_s + "/", "") } meta_file = env[:machine].data_dir.join("box_meta") @logger.debug("Writing box metadata file to #{meta_file}") File.open(meta_file.to_s, "w+") do |file| file.write(JSON.dump(box_meta)) end else @logger.debug("No box data found for #{env[:machine].name} with provider #{env[:machine].provider_name}") end @app.call(env) end end end end ================================================ FILE: plugins/commands/up/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandUp class Plugin < Vagrant.plugin("2") name "up command" description <<-DESC The `up` command brings the virtual environment up and running. DESC command("up") do require File.expand_path("../command", __FILE__) Command end action_hook(:store_box_metadata, :machine_action_up) do |hook| require_relative "middleware/store_box_metadata" hook.append(StoreBoxMetadata) end end end end ================================================ FILE: plugins/commands/up/start_mixins.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "set" module VagrantPlugins module CommandUp module StartMixins # This adds the standard `start` command line flags to the given # OptionParser, storing the result in the `options` dictionary. # # @param [OptionParser] parser # @param [Hash] options def build_start_options(parser, options) # Setup the defaults options[:provision_types] = nil # Add the options parser.on("--[no-]provision", "Enable or disable provisioning") do |p| options[:provision_enabled] = p options[:provision_ignore_sentinel] = true end parser.on("--provision-with x,y,z", Array, "Enable only certain provisioners, by type or by name.") do |list| options[:provision_types] = list.map { |type| type.to_sym } options[:provision_enabled] = true options[:provision_ignore_sentinel] = true end end # This validates the provisioner flags and raises an exception # if there are invalid ones. def validate_provisioner_flags!(options, argv) if options[:provision_types].nil? return end provisioner_names = Set.new with_target_vms(argv) do |machine| machine.config.vm.provisioners.map(&:name).each do |name| provisioner_names.add(name) end end if (provisioner_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 end end ================================================ FILE: plugins/commands/upload/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require "rubygems/package" module VagrantPlugins module CommandUpload class Command < Vagrant.plugin("2", :command) VALID_COMPRESS_TYPES = [:tgz, :zip].freeze def self.synopsis "upload to machine via communicator" end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant upload [options] [destination] [name|id]" o.separator "" o.separator "Options:" o.separator "" o.on("-t", "--temporary", "Upload source to temporary directory") do |t| options[:temporary] = t end o.on("-c", "--compress", "Use gzip compression for upload") do |c| options[:compress] = c end o.on("-C", "--compression-type=TYPE", "Type of compression to use (#{VALID_COMPRESS_TYPES.join(", ")})") do |c| options[:compression_type] = c.to_sym options[:compress] = true end end argv = parse_options(opts) return if !argv case argv.size when 3 source, destination, guest = argv when 2, 1 source = argv[0] if @env.active_machines.map(&:first).map(&:to_s).include?(argv[1]) guest = argv[1] else destination = argv[1] end else raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp end # NOTE: We do this to handle paths on Windows like: "..\space dir\" # because the final separator acts to escape the quote and ends up # in the source value. source = source.sub(/["']$/, "") destination ||= File.basename(source) if File.file?(source) type = :file elsif File.directory?(source) type = :directory else raise Vagrant::Errors::UploadSourceMissing, source: source end with_target_vms(guest, single_target: true) do |machine| if options[:temporary] if !machine.guest.capability?(:create_tmp_path) raise Vagrant::Errors::UploadMissingTempCapability end extension = File.extname(source) if type == :file destination = machine.guest.capability(:create_tmp_path, type: type, extension: extension) end if options[:compress] compression_setup!(machine, options) @env.ui.info(I18n.t("vagrant.commands.upload.compress", source: source, type: options[:compression_type] )) destination_decompressed = destination destination = machine.guest.capability(:create_tmp_path, type: :file, extension: ".#{options[:compression_type]}") source_display = source source = options[:compression_type] == :zip ? compress_source_zip(source) : compress_source_tgz(source) end @env.ui.info(I18n.t("vagrant.commands.upload.start", source: source, destination: destination )) # If the source is a directory, attach a `/.` to the end so we # upload the contents to the destination instead of within a # folder at the destination if File.directory?(source) && !source.end_with?(".") upload_source = File.join(source, ".") else upload_source = source end machine.communicate.upload(upload_source, destination) if options[:compress] @env.ui.info(I18n.t("vagrant.commands.upload.decompress", destination: destination_decompressed, type: options[:compression_type] )) machine.guest.capability(options[:decompression_method], destination, destination_decompressed, type: type) destination = destination_decompressed FileUtils.rm(source) source = source_display end end @env.ui.info(I18n.t("vagrant.commands.upload.complete", source: source, destination: destination )) # Success, exit status 0 0 end # Setup compression options and validate host and guest have capability # to handle compression # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [Hash] options Command options def compression_setup!(machine, options) if !options[:compression_type] if machine.guest.capability_host_chain.first[0] == :windows options[:compression_type] = :zip else options[:compression_type] = :tgz end end if !VALID_COMPRESS_TYPES.include?(options[:compression_type]) raise Vagrant::Errors::UploadInvalidCompressionType, type: options[:compression_type], valid_types: VALID_COMPRESS_TYPES.join(", ") end options[:decompression_method] = "decompress_#{options[:compression_type]}".to_sym if !machine.guest.capability?(options[:decompression_method]) raise Vagrant::Errors::UploadMissingExtractCapability, type: options[:compression_type] end end # Compress path using zip into temporary file # # @param [String] path Path to compress # @return [String] path to compressed file def compress_source_zip(path) require "zip" zipfile = Tempfile.create(["vagrant", ".zip"]) zipfile.close if File.file?(path) source_items = [path] else source_items = Dir.glob(File.join(path, "**", "**", "*")) end c_dir = nil Zip::File.open(zipfile.path, Zip::File::CREATE) do |zip| source_items.each do |source_item| next if File.directory?(source_item) trim_item = source_item.sub(path, "").sub(%r{^[/\\]}, "") dirname = File.dirname(trim_item) zip.mkdir dirname if c_dir != dirname c_dir = dirname zip.get_output_stream(trim_item) do |f| source_file = File.open(source_item, "rb") while data = source_file.read(2048) f.write(data) end end end end zipfile.path end # Compress path using tar and gzip into temporary file # # @param [String] path Path to compress # @return [String] path to compressed file def compress_source_tgz(path) tarfile = Tempfile.create(["vagrant", ".tar"]) tarfile.close tarfile = File.open(tarfile.path, "wb+") tgzfile = Tempfile.create(["vagrant", ".tgz"]) tgzfile.close tgzfile = File.open(tgzfile.path, "wb") tar = Gem::Package::TarWriter.new(tarfile) tgz = Zlib::GzipWriter.new(tgzfile) if File.file?(path) tar.add_file(File.basename(path), File.stat(path).mode) do |io| File.open(path, "rb") do |file| while bytes = file.read(4096) io.write(bytes) end end end else Dir.glob(File.join(path, "**/**/*")).each do |item| rel_path = item.sub(path, "") item_mode = File.stat(item).mode if File.directory?(item) tar.mkdir(rel_path, item_mode) else tar.add_file(rel_path, item_mode) do |io| File.open(item, "rb") do |file| while bytes = file.read(4096) io.write(bytes) end end end end end end tar.close tarfile.rewind while bytes = tarfile.read(4096) tgz.write bytes end tgz.close tgzfile.close tarfile.close File.delete(tarfile.path) tgzfile.path end end end end ================================================ FILE: plugins/commands/upload/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandUpload class Plugin < Vagrant.plugin("2") name "upload command" description <<-DESC The `upload` command uploads files to guest via communicator DESC command("upload") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/validate/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' module VagrantPlugins module CommandValidate class Command < Vagrant.plugin("2", :command) def self.synopsis "validates the Vagrantfile" end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant validate [options]" o.separator "" o.separator "Validates a Vagrantfile config" o.separator "" o.separator "Options:" o.separator "" o.on("-p", "--ignore-provider", "Ignores provider config options") do |p| options[:ignore_provider] = p end end # Parse the options argv = parse_options(opts) return if !argv action_env = {} if options[:ignore_provider] action_env[:ignore_provider] = true tmp_data_dir = mockup_providers! end # Validate the configuration of all machines with_target_vms() do |machine| machine.action_raw(:config_validate, Vagrant::Action::Builtin::ConfigValidate, action_env) end @env.ui.info(I18n.t("vagrant.commands.validate.success")) # Success, exit status 0 0 ensure FileUtils.remove_entry tmp_data_dir if tmp_data_dir end protected # This method is required to bypass some of the provider checks that would # otherwise raise exceptions before Vagrant could load and validate a config. # It essentially ignores that there are no installed or usable prodivers so # that Vagrant can go along and validate the rest of the Vagrantfile and ignore # any provider blocks. # # return [String] tmp_data_dir - Temporary dir used to store guest metadata during validation def mockup_providers! require 'log4r' logger = Log4r::Logger.new("vagrant::validate") logger.debug("Overriding all registered provider classes for validate") # Without setting up a tmp Environment, Vagrant will completely # erase the local data dotfile and you can lose state after the # validate command completes. tmp_data_dir = Dir.mktmpdir("vagrant-validate-") @env = Vagrant::Environment.new( cwd: @env.cwd, home_path: @env.home_path, ui_class: @env.ui_class, vagrantfile_name: @env.vagrantfile_name, local_data_path: tmp_data_dir, data_dir: tmp_data_dir ) Vagrant.plugin("2").manager.providers.each do |key, data| data[0].class_eval do def initialize(machine) end def machine_id_changed end def self.installed? true end def self.usable?(raise_error=false) true end def state state_id = Vagrant::MachineState::NOT_CREATED_ID short = :not_created long = :not_created Vagrant::MachineState.new(state_id, short, long) end end end tmp_data_dir end end end end ================================================ FILE: plugins/commands/validate/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandValidate class Plugin < Vagrant.plugin("2") name "validate command" description <<-DESC The `validate` command validates the Vagrantfile. DESC command("validate") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/version/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "optparse" module VagrantPlugins module CommandVersion class Command < Vagrant.plugin("2", :command) def self.synopsis "prints current and latest Vagrant version" end def execute opts = OptionParser.new do |o| o.banner = "Usage: vagrant version" end # Parse the options argv = parse_options(opts) return if !argv # Output the currently installed version instantly. @env.ui.output(I18n.t( "vagrant.version_current", version: Vagrant::VERSION)) @env.ui.machine("version-installed", Vagrant::VERSION) # Load the latest information cp = Vagrant::Util::CheckpointClient.instance.result if !cp @env.ui.output("\n"+I18n.t( "vagrant.version_no_checkpoint")) return 0 end latest = cp["current_version"] # Output latest version @env.ui.output(I18n.t( "vagrant.version_latest", version: latest)) @env.ui.machine("version-latest", latest) # Determine if it's a new version, and if so, output some more # information. current = Gem::Version.new(Vagrant::VERSION) latest = Gem::Version.new(latest) if current >= latest @env.ui.success(" \n" + I18n.t( "vagrant.version_latest_installed")) return 0 end # Out of date! Let the user know how to upgrade. @env.ui.output(" \n" + I18n.t( "vagrant.version_upgrade_howto", version: latest.to_s)) 0 end end end end ================================================ FILE: plugins/commands/version/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandVersion class Plugin < Vagrant.plugin("2") name "version command" description <<-DESC The `version` command prints the currently installed version as well as the latest available version. DESC command("version") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/winrm/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require "vagrant/util/safe_puts" module VagrantPlugins module CommandWinRM class Command < Vagrant.plugin("2", :command) include Vagrant::Util::SafePuts def self.synopsis "executes commands on a machine via WinRM" end def execute options = { command: [], shell: :powershell } opts = OptionParser.new do |o| o.banner = "Usage: vagrant winrm [options] [name|id]" o.separator "" o.separator "Options:" o.separator "" o.on("-c", "--command COMMAND", "Execute a WinRM command directly") do |c| options[:command] << c end o.on("-e", "--elevated", "Run with elevated credentials") do |e| options[:elevated] = true end o.on("-s", "--shell SHELL", [:powershell, :cmd], "Use specified shell (powershell, cmd)") do |s| options[:shell] = s end end argv = parse_options(opts) return if !argv with_target_vms(argv, single_target: true) do |machine| if machine.config.vm.communicator != :winrm raise Vagrant::Errors::WinRMInvalidCommunicator end opts = { shell: options[:shell], elevated: !!options[:elevated] } options[:command].each do |cmd| begin machine.communicate.execute(cmd, opts) do |type, data| io = type == :stderr ? $stderr : $stdout safe_puts(data, io: io, printer: :print) end rescue VagrantPlugins::CommunicatorWinRM::Errors::WinRMBadExitStatus return 1 end end end # Success, exit status 0 0 end end end end ================================================ FILE: plugins/commands/winrm/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandWinRM class Plugin < Vagrant.plugin("2") name "winrm command" description <<-DESC The `winrm` command executes commands on a machine via WinRM DESC command("winrm") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/commands/winrm_config/command.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require "vagrant/util/safe_puts" require_relative "../../communicators/winrm/helper" module VagrantPlugins module CommandWinRMConfig class Command < Vagrant.plugin("2", :command) include Vagrant::Util::SafePuts def self.synopsis "outputs WinRM configuration to connect to the machine" end def convert_win_paths(paths) paths.map! { |path| Vagrant::Util::Platform.format_windows_path(path, :disable_unc) } end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant winrm-config [options] [name|id]" o.separator "" o.separator "Options:" o.separator "" o.on("--host NAME", "Name the host for the config") do |h| options[:host] = h end end argv = parse_options(opts) return if !argv with_target_vms(argv) do |machine| winrm_info = CommunicatorWinRM::Helper.winrm_info(machine) raise Vagrant::Errors::WinRMNotRead if winrm_info.nil? rdp_info = get_rdp_info(machine) || {} variables = { host_key: options[:host] || machine.name || "vagrant", rdp_host: rdp_info[:host] || winrm_info[:host], rdp_port: rdp_info[:port], rdp_user: rdp_info[:username], rdp_pass: rdp_info[:password], winrm_host: winrm_info[:host], winrm_port: winrm_info[:port], winrm_user: machine.config.winrm.username, winrm_password: machine.config.winrm.password } template = "commands/winrm_config/config" config = Vagrant::Util::TemplateRenderer.render(template, variables) machine.ui.machine("winrm-config", config) safe_puts(config) safe_puts end # Success, exit status 0 0 end protected # Generate RDP information for machine # # @param [Vagrant::Machine] machine Guest machine # @return [Hash, nil] def get_rdp_info(machine) rdp_info = {} if machine.provider.capability?(:rdp_info) rdp_info = machine.provider.capability(:rdp_info) rdp_info ||= {} end ssh_info = machine.ssh_info || {} if !rdp_info[:username] username = ssh_info[:username] if machine.config.vm.communicator == :winrm username = machine.config.winrm.username end rdp_info[:username] = username end if !rdp_info[:password] password = ssh_info[:password] if machine.config.vm.communicator == :winrm password = machine.config.winrm.password end rdp_info[:password] = password end rdp_info[:host] ||= ssh_info[:host] rdp_info[:port] ||= machine.config.rdp.port rdp_info[:username] ||= machine.config.rdp.username if rdp_info[:host] == "127.0.0.1" # We need to find a forwarded port... search_port = machine.config.rdp.search_port ports = nil if machine.provider.capability?(:forwarded_ports) ports = machine.provider.capability(:forwarded_ports) else ports = {}.tap do |result| machine.config.vm.networks.each do |type, netopts| next if type != :forwarded_port next if !netopts[:host] result[netopts[:host]] = netopts[:guest] end end end ports = ports.invert port = ports[search_port] rdp_info[:port] = port return nil if !port end rdp_info end end end end ================================================ FILE: plugins/commands/winrm_config/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommandWinRMConfig class Plugin < Vagrant.plugin("2") name "winrm-config command" description <<-DESC The `winrm-config` command dumps WinRM configuration information DESC command("winrm-config") do require File.expand_path("../command", __FILE__) Command end end end end ================================================ FILE: plugins/communicators/none/communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant" module VagrantPlugins module CommunicatorNone # This class provides no communication with the VM. # It allows Vagrant to manage a machine lifecycle # while not actually connecting to it. The communicator # stubs out all methods to be successful allowing # Vagrant to proceed "as normal" without actually # doing anything. class Communicator < Vagrant.plugin("2", :communicator) def self.match?(_) # Any machine can be not communicated with true end def initialize(_) @logger = Log4r::Logger.new(self.class.name.downcase) end def ready? @logger.debug("#ready? stub called on none") true end def execute(*_) @logger.debug("#execute stub called on none") 0 end def sudo(*_) @logger.debug("#sudo stub called on none") 0 end def test(*_) @logger.debug("#test stub called on none") true end end end end ================================================ FILE: plugins/communicators/none/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins Vagrant::Util::Experimental.guard_with(:none_communicator) do module CommunicatorNone class Plugin < Vagrant.plugin("2") name "none communicator" description <<-DESC This plugin provides no communication to remote machines. It allows Vagrant to manage remote machines without the ability to connect to them for configuration/provisioning. Any calls to methods provided by this communicator will always be successful. DESC communicator("none") do require File.expand_path("../communicator", __FILE__) Communicator end end end end end ================================================ FILE: plugins/communicators/ssh/communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'etc' require 'logger' require 'pathname' require 'stringio' require 'thread' require 'timeout' require 'log4r' require 'net/ssh' require 'net/ssh/proxy/command' require 'net/scp' require 'vagrant/util/ansi_escape_code_remover' require 'vagrant/util/file_mode' require 'vagrant/util/keypair' require 'vagrant/util/platform' require 'vagrant/util/retryable' module VagrantPlugins module CommunicatorSSH # This class provides communication with the VM via SSH. class Communicator < Vagrant.plugin("2", :communicator) READY_COMMAND="" # Marker for start of PTY enabled command output PTY_DELIM_START = "bccbb768c119429488cfd109aacea6b5-pty" # Marker for end of PTY enabled command output PTY_DELIM_END = "bccbb768c119429488cfd109aacea6b5-pty" # Marker for start of regular command output CMD_GARBAGE_MARKER = "41e57d38-b4f7-4e46-9c38-13873d338b86-vagrant-ssh" # These are the exceptions that we retry because they represent # errors that are generally fixed from a retry and don't # necessarily represent immediate failure cases. SSH_RETRY_EXCEPTIONS = [ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH, Errno::EPIPE, Net::SSH::Disconnect, Timeout::Error ] include Vagrant::Util::ANSIEscapeCodeRemover include Vagrant::Util::Retryable def self.match?(machine) # All machines are currently expected to have SSH. true end def initialize(machine) @lock = Mutex.new @machine = machine @logger = Log4r::Logger.new("vagrant::communication::ssh") @connection = nil @inserted_key = false end def wait_for_ready(timeout) Timeout.timeout(timeout) do # Wait for ssh_info to be ready ssh_info = nil while true ssh_info = @machine.ssh_info break if ssh_info sleep(0.5) end # Got it! Let the user know what we're connecting to. if !@ssh_info_notification @machine.ui.detail("SSH address: #{ssh_info[:host]}:#{ssh_info[:port]}") @machine.ui.detail("SSH username: #{ssh_info[:username]}") ssh_auth_type = "private key" ssh_auth_type = "password" if ssh_info[:password] @machine.ui.detail("SSH auth method: #{ssh_auth_type}") @ssh_info_notification = true end previous_messages = {} while true message = nil begin begin connect(retries: 1) return true if ready? rescue Vagrant::Errors::VagrantError => e @logger.info("SSH not ready: #{e.inspect}") raise end rescue Vagrant::Errors::SSHConnectionTimeout message = "Connection timeout." rescue Vagrant::Errors::SSHAuthenticationFailed message = "Authentication failure." rescue Vagrant::Errors::SSHDisconnected message = "Remote connection disconnect." rescue Vagrant::Errors::SSHConnectionRefused message = "Connection refused." rescue Vagrant::Errors::SSHConnectionReset message = "Connection reset." rescue Vagrant::Errors::SSHConnectionAborted message = "Connection aborted." rescue Vagrant::Errors::SSHHostDown message = "Host appears down." rescue Vagrant::Errors::SSHNoRoute message = "Host unreachable." rescue Vagrant::Errors::SSHInvalidShell raise rescue Vagrant::Errors::SSHKeyTypeNotSupported raise rescue Vagrant::Errors::SSHKeyTypeNotSupportedByServer raise rescue Vagrant::Errors::SSHKeyBadOwner raise rescue Vagrant::Errors::SSHKeyBadPermissions raise rescue Vagrant::Errors::SSHInsertKeyUnsupported raise rescue Vagrant::Errors::VagrantError => e # Ignore it, SSH is not ready, some other error. end # If we have a message to show, then show it. We don't show # repeated messages unless they've been repeating longer than # 10 seconds. if message message_at = Time.now.to_f show_message = true if previous_messages[message] show_message = (message_at - previous_messages[message]) > 10.0 end if show_message @machine.ui.detail("Warning: #{message} Retrying...") previous_messages[message] = message_at end end end end rescue Timeout::Error return false end def ready? @logger.debug("Checking whether SSH is ready...") # Attempt to connect. This will raise an exception if it fails. begin connect @logger.info("SSH is ready!") rescue Vagrant::Errors::VagrantError => e # We catch a `VagrantError` which would signal that something went # wrong expectedly in the `connect`, which means we didn't connect. @logger.info("SSH not up: #{e.inspect}") return false end # Verify the shell is valid if execute(self.class.const_get(:READY_COMMAND), error_check: false) != 0 raise Vagrant::Errors::SSHInvalidShell end # If we're already attempting to switch out the SSH key, then # just return that we're ready (for Machine#guest). @lock.synchronize do return true if @inserted_key || !machine_config_ssh.insert_key @inserted_key = true end # If we used a password, then insert the insecure key ssh_info = @machine.ssh_info return if ssh_info.nil? insert = ssh_info[:password] && ssh_info[:private_key_path].empty? ssh_info[:private_key_path].each do |pk| if insecure_key?(pk) insert = true @machine.ui.detail("\n"+I18n.t("vagrant.inserting_insecure_detected")) break end end if insert # If we don't have the power to insert/remove keys, then its an error cap = @machine.guest.capability?(:insert_public_key) && @machine.guest.capability?(:remove_public_key) raise Vagrant::Errors::SSHInsertKeyUnsupported if !cap key_type = machine_config_ssh.key_type begin # If the key type is set to `:auto` check for supported type. Otherwise # ensure that the key type is supported by the guest if key_type == :auto key_type = catch(:key_type) do begin Vagrant::Util::Keypair::PREFER_KEY_TYPES.each do |type_name, type| throw :key_type, type if supports_key_type?(type_name) end nil rescue => err @logger.warn("Failed to check key types server supports: #{err}") nil end end @logger.debug("Detected key type for new private key: #{key_type}") # If no key type was discovered, default to rsa if key_type.nil? @logger.debug("Failed to detect supported key type in: #{supported_key_types.join(", ")}") available_types = supported_key_types.map { |t| next if !Vagrant::Util::Keypair::PREFER_KEY_TYPES.key?(t) "#{t} (#{Vagrant::Util::Keypair::PREFER_KEY_TYPES[t]})" }.compact.join(", ") raise Vagrant::Errors::SSHKeyTypeNotSupportedByServer, requested_key_type: ":auto", available_key_types: available_types end else type_name = Vagrant::Util::Keypair::PREFER_KEY_TYPES.key(key_type) if !supports_key_type?(type_name) available_types = supported_key_types.map { |t| next if !Vagrant::Util::Keypair::PREFER_KEY_TYPES.key?(t) "#{t} (#{Vagrant::Util::Keypair::PREFER_KEY_TYPES[t]})" }.compact.join(", ") raise Vagrant::Errors::SSHKeyTypeNotSupportedByServer, requested_key_type: "#{type_name} (#{key_type})", available_key_types: available_types end end rescue ServerDataError @logger.warn("failed to load server data for key type check") if key_type.nil? || key_type == :auto @logger.warn("defaulting key type to :rsa due to failed server data loading") key_type = :rsa end end @logger.info("Creating new ssh keypair (type: #{key_type.inspect})") _pub, priv, openssh = Vagrant::Util::Keypair.create(type: key_type) @logger.info("Inserting key to avoid password: #{openssh}") @machine.ui.detail("\n"+I18n.t("vagrant.inserting_random_key")) @machine.guest.capability(:insert_public_key, openssh) # Write out the private key in the data dir so that the # machine automatically picks it up. @machine.data_dir.join("private_key").open("wb+") do |f| f.write(priv) end # Adjust private key file permissions if host provides capability if @machine.env.host.capability?(:set_ssh_key_permissions) @machine.env.host.capability(:set_ssh_key_permissions, @machine.data_dir.join("private_key")) end # Remove the old key if it exists @machine.ui.detail(I18n.t("vagrant.inserting_remove_key")) @machine.guest.capability( :remove_public_key, Vagrant.source_root.join("keys", "vagrant.pub").read.chomp) # Done, restart. @machine.ui.detail(I18n.t("vagrant.inserted_key")) @connection.close @connection = nil return ready? end # If we reached this point then we successfully connected true end def execute(command, opts=nil, &block) opts = { error_check: true, error_class: Vagrant::Errors::VagrantError, error_key: :ssh_bad_exit_status, good_exit: 0, command: command, shell: nil, sudo: false, force_raw: false }.merge(opts || {}) opts[:good_exit] = Array(opts[:good_exit]) # Connect via SSH and execute the command in the shell. stdout = "" stderr = "" exit_status = connect do |connection| shell_opts = { sudo: opts[:sudo], shell: opts[:shell], force_raw: opts[:force_raw] } shell_execute(connection, command, **shell_opts) do |type, data| if type == :stdout stdout += data elsif type == :stderr stderr += data end block.call(type, data) if block end end # Check for any errors if opts[:error_check] && !opts[:good_exit].include?(exit_status) # The error classes expect the translation key to be _key, # but that makes for an ugly configuration parameter, so we # set it here from `error_key` error_opts = opts.merge( _key: opts[:error_key], stdout: stdout, stderr: stderr ) raise opts[:error_class], error_opts end # Return the exit status exit_status end def sudo(command, opts=nil, &block) # Run `execute` but with the `sudo` option. opts = { sudo: true }.merge(opts || {}) execute(command, opts, &block) end def download(from, to=nil) @logger.debug("Downloading: #{from} to #{to}") scp_connect do |scp| scp.download!(from, to) end end def test(command, opts=nil) opts = { error_check: false }.merge(opts || {}) execute(command, opts) == 0 end def upload(from, to) @logger.debug("Uploading: #{from} to #{to}") if File.directory?(from) if from.end_with?(".") @logger.debug("Uploading directory contents of: #{from}") from = from.sub(/\.$/, "") else @logger.debug("Uploading full directory container of: #{from}") to = File.join(to, File.basename(File.expand_path(from))) end end scp_connect do |scp| uploader = lambda do |path, remote_dest=nil| if File.directory?(path) dest = File.join(to, path.sub(/^#{Regexp.escape(from)}/, "")) create_remote_directory(dest) Dir.new(path).each do |entry| next if entry == "." || entry == ".." full_path = File.join(path, entry) create_remote_directory(dest) uploader.call(full_path, dest) end else if remote_dest dest = File.join(remote_dest, File.basename(path)) else dest = to if to.end_with?(File::SEPARATOR) dest = File.join(to, File.basename(path)) end end @logger.debug("Ensuring remote directory exists for destination upload") create_remote_directory(File.dirname(dest)) @logger.debug("Uploading file #{path} to remote #{dest}") upload_file = File.open(path, "rb") begin scp.upload!(upload_file, dest) ensure upload_file.close end end end uploader.call(from) end rescue RuntimeError => e # Net::SCP raises a runtime error for this so the only way we have # to really catch this exception is to check the message to see if # it is something we care about. If it isn't, we re-raise. raise if e.message !~ /Permission denied/ # Otherwise, it is a permission denied, so let's raise a proper # exception raise Vagrant::Errors::SCPPermissionDenied, from: from.to_s, to: to.to_s end def reset! if @connection @connection.close @connection = nil end @ssh_info_notification = true # suppress ssh info output wait_for_ready(5) end def generate_environment_export(env_key, env_value) template = machine_config_ssh.export_command_template template.sub("%ENV_KEY%", env_key).sub("%ENV_VALUE%", env_value) + "\n" end protected # Opens an SSH connection and yields it to a block. def connect(**opts) if @connection && !@connection.closed? # There is a chance that the socket is closed despite us checking # 'closed?' above. To test this we need to send data through the # socket. # # We wrap the check itself in a 5 second timeout because there # are some cases where this will just hang. begin Timeout.timeout(5) do @connection.exec!("") end rescue Exception => e @logger.info("Connection errored, not re-using. Will reconnect.") @logger.debug(e.inspect) @connection = nil end # If the @connection is still around, then it is valid, # and we use it. if @connection @logger.debug("Re-using SSH connection.") return yield @connection if block_given? return end end # Get the SSH info for the machine, raise an exception if the # provider is saying that SSH is not ready. ssh_info = @machine.ssh_info raise Vagrant::Errors::SSHNotReady if ssh_info.nil? # Default some options opts[:retries] = ssh_info[:connect_retries] if !opts.key?(:retries) opts[:retry_delay] = ssh_info[:connect_retry_delay] if !opts.key?(:retry_delay) # Set some valid auth methods. We disable the auth methods that # we're not using if we don't have the right auth info. auth_methods = ["none", "hostbased"] auth_methods << "publickey" if ssh_info[:private_key_path] auth_methods << "password" if ssh_info[:password] # Build the options we'll use to initiate the connection via Net::SSH common_connect_opts = { auth_methods: auth_methods, config: false, forward_agent: ssh_info[:forward_agent], send_env: ssh_info[:forward_env], keys_only: ssh_info[:keys_only], verify_host_key: ssh_info[:verify_host_key], password: ssh_info[:password], port: ssh_info[:port], timeout: ssh_info[:connect_timeout], user_known_hosts_file: [], verbose: :debug } # Connect to SSH, giving it a few tries connection = nil begin timeout = 60 @logger.info("Attempting SSH connection...") connection = retryable(tries: opts[:retries], on: SSH_RETRY_EXCEPTIONS, sleep: opts[:retry_delay]) do Timeout.timeout(timeout) do begin # This logger will get the Net-SSH log data for us. ssh_logger_io = StringIO.new ssh_logger = Logger.new(ssh_logger_io) # Setup logging for connections connect_opts = common_connect_opts.dup connect_opts[:logger] = ssh_logger if ssh_info[:private_key_path] connect_opts[:keys] = ssh_info[:private_key_path] end if ssh_info[:proxy_command] connect_opts[:proxy] = Net::SSH::Proxy::Command.new(ssh_info[:proxy_command]) end if ssh_info[:config] connect_opts[:config] = ssh_info[:config] end if ssh_info[:remote_user] connect_opts[:remote_user] = ssh_info[:remote_user] end if @machine.config.ssh.keep_alive connect_opts[:keepalive] = true connect_opts[:keepalive_interval] = 5 end @logger.info("Attempting to connect to SSH...") @logger.info(" - Host: #{ssh_info[:host]}") @logger.info(" - Port: #{ssh_info[:port]}") @logger.info(" - Username: #{ssh_info[:username]}") @logger.info(" - Password? #{!!ssh_info[:password]}") @logger.info(" - Key Path: #{ssh_info[:private_key_path]}") @logger.debug(" - connect_opts: #{connect_opts}") Net::SSH.start(ssh_info[:host], ssh_info[:username], **connect_opts) ensure # Make sure we output the connection log @logger.debug("== Net-SSH connection debug-level log START ==") @logger.debug(ssh_logger_io.string) @logger.debug("== Net-SSH connection debug-level log END ==") end end end rescue Errno::EACCES # This happens on connect() for unknown reasons yet... raise Vagrant::Errors::SSHConnectEACCES rescue Errno::ETIMEDOUT, Timeout::Error, IO::TimeoutError # This happens if we continued to timeout when attempting to connect. raise Vagrant::Errors::SSHConnectionTimeout rescue Net::SSH::AuthenticationFailed # This happens if authentication failed. We wrap the error in our # own exception. raise Vagrant::Errors::SSHAuthenticationFailed rescue Net::SSH::Disconnect # This happens if the remote server unexpectedly closes the # connection. This is usually raised when SSH is running on the # other side but can't properly setup a connection. This is # usually a server-side issue. raise Vagrant::Errors::SSHDisconnected rescue Errno::ECONNREFUSED # This is raised if we failed to connect the max amount of times raise Vagrant::Errors::SSHConnectionRefused rescue Errno::ECONNRESET # This is raised if we failed to connect the max number of times # due to an ECONNRESET. raise Vagrant::Errors::SSHConnectionReset rescue Errno::ECONNABORTED # This is raised if we failed to connect the max number of times # due to an ECONNABORTED raise Vagrant::Errors::SSHConnectionAborted rescue Errno::EHOSTDOWN # This is raised if we get an ICMP DestinationUnknown error. raise Vagrant::Errors::SSHHostDown 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 # This is an internal error in Net::SSH raise Vagrant::Errors::NetSSHException, message: e.message rescue NotImplementedError # This is raised if a private key type that Net-SSH doesn't support # is used. Show a nicer error. raise Vagrant::Errors::SSHKeyTypeNotSupported end @connection = connection @connection_ssh_info = ssh_info # Yield the connection that is ready to be used and # return the value of the block return yield connection if block_given? end # The shell wrapper command used in shell_execute defined by # the sudo and shell options. def shell_cmd(opts) sudo = opts[:sudo] shell = opts[:shell] # Determine the shell to execute. Prefer the explicitly passed in shell # over the default configured shell. If we are using `sudo` then we # need to wrap the shell in a `sudo` call. cmd = machine_config_ssh.shell cmd = shell if shell cmd = machine_config_ssh.sudo_command.gsub("%c", cmd) if sudo cmd end # Executes the command on an SSH connection within a login shell. def shell_execute(connection, command, **opts) opts = { sudo: false, shell: nil }.merge(opts) sudo = opts[:sudo] @logger.info("Execute: #{command} (sudo=#{sudo.inspect})") exit_status = nil # These variables are used to scrub PTY output if we're in a PTY pty = false pty_stdout = "" # Open the channel so we can execute or command channel = connection.open_channel do |ch| if machine_config_ssh.pty ch.request_pty do |ch2, success| pty = success && command != "" if success @logger.debug("pty obtained for connection") else @logger.warn("failed to obtain pty, will try to continue anyways") end end end marker_found = false data_buffer = '' stderr_marker_found = false stderr_data_buffer = '' ch.exec(shell_cmd(opts)) do |ch2, _| # Setup the channel callbacks so we can get data and exit status ch2.on_data do |ch3, data| # Filter out the clear screen command data = remove_ansi_escape_codes(data) if pty pty_stdout << data else if !marker_found data_buffer << data marker_index = data_buffer.index(CMD_GARBAGE_MARKER) if marker_index marker_found = true data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size) data.replace(data_buffer) data_buffer = nil end end if block_given? && marker_found && !data.empty? yield :stdout, data end end end ch2.on_extended_data do |ch3, type, data| # Filter out the clear screen command data = remove_ansi_escape_codes(data) @logger.debug("stderr: #{data}") if !stderr_marker_found stderr_data_buffer << data marker_index = stderr_data_buffer.index(CMD_GARBAGE_MARKER) if marker_index stderr_marker_found = true stderr_data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size) data.replace(stderr_data_buffer) stderr_data_buffer = nil end end if block_given? && stderr_marker_found && !data.empty? yield :stderr, data end end ch2.on_request("exit-status") do |ch3, data| exit_status = data.read_long @logger.debug("Exit status: #{exit_status}") # Close the channel, since after the exit status we're # probably done. This fixes up issues with hanging. ch.close end # Set the terminal ch2.send_data(generate_environment_export("TERM", "vt100")) # Set SSH_AUTH_SOCK if we are in sudo and forwarding agent. # This is to work around often misconfigured boxes where # the SSH_AUTH_SOCK env var is not preserved. if @connection_ssh_info[:forward_agent] && sudo auth_socket = "" execute("echo; printf $SSH_AUTH_SOCK") do |type, data| if type == :stdout auth_socket += data end end if auth_socket != "" # Make sure we only read the last line which should be # the $SSH_AUTH_SOCK env var we printed. auth_socket = auth_socket.split("\n").last.to_s.chomp end if auth_socket == "" @logger.warn("No SSH_AUTH_SOCK found despite forward_agent being set.") else @logger.info("Setting SSH_AUTH_SOCK remotely: #{auth_socket}") ch2.send_data(generate_environment_export("SSH_AUTH_SOCK", auth_socket)) end end # Output the command. If we're using a pty we have to do # a little dance to make sure we get all the output properly # without the cruft added from pty mode. if pty data = "stty raw -echo\n" data += generate_environment_export("PS1", "") data += generate_environment_export("PS2", "") data += generate_environment_export("PROMPT_COMMAND", "") data += "printf #{PTY_DELIM_START}\n" data += "#{command}\n" data += "exitcode=$?\n" data += "printf #{PTY_DELIM_END}\n" data += "exit $exitcode\n" data = data.force_encoding('ASCII-8BIT') ch2.send_data(data) else ch2.send_data("printf '#{CMD_GARBAGE_MARKER}'\n(>&2 printf '#{CMD_GARBAGE_MARKER}')\n#{command}\n".force_encoding('ASCII-8BIT')) # Remember to exit or this channel will hang open ch2.send_data("exit\n") end # Send eof to let server know we're done ch2.eof! end end begin # Wait for the channel to complete begin channel.wait rescue Errno::ECONNRESET, IOError @logger.info( "SSH connection unexpected closed. Assuming reboot or something.") exit_status = 0 pty = false rescue Net::SSH::ChannelOpenFailed raise Vagrant::Errors::SSHChannelOpenFail rescue Net::SSH::Disconnect raise Vagrant::Errors::SSHDisconnected end end # If we're in a PTY, we now finally parse the output if pty @logger.debug("PTY stdout: #{pty_stdout}") if !pty_stdout.include?(PTY_DELIM_START) || !pty_stdout.include?(PTY_DELIM_END) @logger.error("PTY stdout doesn't include delims") raise Vagrant::Errors::SSHInvalidShell.new end data = pty_stdout[/.*#{PTY_DELIM_START}(.*?)#{PTY_DELIM_END}/m, 1] data ||= "" @logger.debug("PTY stdout parsed: #{data}") yield :stdout, data if block_given? end if !exit_status @logger.debug("Exit status: #{exit_status.inspect}") raise Vagrant::Errors::SSHNoExitStatus end # Return the final exit status return exit_status end # Opens an SCP connection and yields it so that you can download # and upload files. def scp_connect # Connect to SCP and yield the SCP object connect do |connection| scp = Net::SCP.new(connection) return yield scp end rescue Net::SCP::Error => e # If we get the exit code of 127, then this means SCP is unavailable. raise Vagrant::Errors::SCPUnavailable if e.message =~ /\(127\)/ # Otherwise, just raise the error up raise end # This will test whether path is the Vagrant insecure private key. # # @param [String] path def insecure_key?(path) return false if !path return false if !File.file?(path) Dir.glob(Vagrant.source_root.join("keys", "vagrant.key.*")).any? do |source_path| File.read(path).chomp == File.read(source_path).chomp end end def create_remote_directory(dir) execute("mkdir -p \"#{dir}\"") end def machine_config_ssh @machine.config.ssh end protected class ServerDataError < StandardError; end # Check if server supports given key type # # @param [String, Symbol] type Key type # @return [Boolean] # @note This does not use a stable API and may be subject # to unexpected breakage on net-ssh updates def supports_key_type?(type) if @connection.nil? raise Vagrant::Errors::SSHNotReady end supported_key_types.include?(type.to_s) end def supported_key_types return @supported_key_types if @supported_key_types if @connection.nil? raise Vagrant::Errors::SSHNotReady end list = "" result = sudo("sshd -T | grep key", {error_check: false}) do |type, data| list << data end # If the command failed, attempt to extract some supported # key information from within net-ssh if result != 0 server_data = @connection. transport&. algorithms&. instance_variable_get(:@server_data) if server_data.nil? @logger.warn("No server data available for key type support check") raise ServerDataError, "no data available" end if !server_data.is_a?(Hash) @logger.warn("Server data is not expected type (expecting Hash, got #{server_data.class})") raise ServerDataError, "unexpected type encountered (expecting Hash, got #{server_data.class})" end @logger.debug("server supported key type list (extracted from connection server info using host key): #{server_data[:host_key]}") return @supported_key_types = server_data[:host_key] end # Convert the options into a Hash for easy access opts = Hash[*list.split("\n").map{|line| line.split(" ", 2)}.flatten] # Define the option names to check for in preferred order # NOTE: pubkeyacceptedkeytypes has been renamed to pubkeyacceptedalgorithms # ref: https://github.com/openssh/openssh-portable/commit/ee9c0da8035b3168e8e57c1dedc2d1b0daf00eec ["pubkeyacceptedalgorithms", "pubkeyacceptedkeytypes", "hostkeyalgorithms"].each do |opt_name| next if !opts.key?(opt_name) @supported_key_types = opts[opt_name].split(",") @logger.debug("server supported key type list (using #{opt_name}): #{@supported_key_types}") return @supported_key_types end # Still here means unable to determine key types # so log what information was returned and toss # and error @logger.warn("failed to determine supported key types from remote inspection") @logger.debug("data returned for supported key types remote inspection: #{list.inspect}") raise ServerDataError, "no data available" end end end end ================================================ FILE: plugins/communicators/ssh/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommunicatorSSH class Plugin < Vagrant.plugin("2") name "ssh communicator" description <<-DESC This plugin allows Vagrant to communicate with remote machines using SSH as the underlying protocol, powered internally by Ruby's net-ssh library. DESC communicator("ssh") do require File.expand_path("../communicator", __FILE__) Communicator end end end end ================================================ FILE: plugins/communicators/winrm/command_filter.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommunicatorWinRM # Handles loading and applying all available WinRM command filters class CommandFilter @@cmd_filters = [ "cat", "chmod", "chown", "grep", "rm", "test", "uname", "which", "mkdir", ] # Filter the given Vagrant command to ensure compatibility with Windows # # @param [String] The Vagrant shell command # @returns [String] Windows runnable command or empty string def filter(command) command_filters.each { |c| command = c.filter(command) if c.accept?(command) } command end # All the available Linux command filters # # @returns [Array] All Linux command filter instances def command_filters @command_filters ||= create_command_filters() end private def create_command_filters [].tap do |filters| @@cmd_filters.each do |cmd| require_relative "command_filters/#{cmd}" class_name = "VagrantPlugins::CommunicatorWinRM::CommandFilters::#{cmd.capitalize}" filters << Module.const_get(class_name).new end end end end end end ================================================ FILE: plugins/communicators/winrm/command_filters/cat.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommunicatorWinRM module CommandFilters # Handles the special case of determining the guest OS using cat class Cat def filter(command) # cat /etc/release | grep -i OmniOS # cat /etc/redhat-release # cat /etc/issue | grep 'Core Linux' # cat /etc/release | grep -i SmartOS '' end def accept?(command) # cat works in PowerShell, however we don't want to run Guest # OS detection as this will fail on Windows because the lack of the # grep command command.start_with?('cat /etc/') end end end end end ================================================ FILE: plugins/communicators/winrm/command_filters/chmod.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommunicatorWinRM module CommandFilters # Converts a *nix 'chmod' command to a PowerShell equivalent (none) class Chmod def filter(command) # Not supported on Windows, the communicator should skip this command '' end def accept?(command) command.start_with?('chmod ') end end end end end ================================================ FILE: plugins/communicators/winrm/command_filters/chown.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommunicatorWinRM module CommandFilters # Converts a *nix 'chown' command to a PowerShell equivalent (none) class Chown def filter(command) # Not supported on Windows, the communicator should skip this command '' end def accept?(command) command.start_with?('chown ') end end end end end ================================================ FILE: plugins/communicators/winrm/command_filters/grep.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommunicatorWinRM module CommandFilters # Converts a *nix 'grep' command to a PowerShell equivalent (none) class Grep def filter(command) # grep 'Fedora release [12][67890]' /etc/redhat-release # grep Funtoo /etc/gentoo-release # grep Gentoo /etc/gentoo-release # grep is often used to detect the guest type in Vagrant, so don't bother running # to speed up OS detection '' end def accept?(command) command.start_with?('grep ') end end end end end ================================================ FILE: plugins/communicators/winrm/command_filters/mkdir.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "shellwords" module VagrantPlugins module CommunicatorWinRM module CommandFilters # Converts a *nix 'mkdir' command to a PowerShell equivalent class Mkdir def filter(command) # mkdir -p /some/dir # mkdir /some/dir cmd_parts = Shellwords.split(command.strip) dir = cmd_parts.pop while !dir.nil? && dir.start_with?('-') dir = cmd_parts.pop end # This will ignore any -p switches, which are redundant in PowerShell, # and ambiguous in PowerShell 4+ return "mkdir \"#{dir}\" -force" end def accept?(command) command.start_with?('mkdir ') end end end end end ================================================ FILE: plugins/communicators/winrm/command_filters/rm.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "shellwords" module VagrantPlugins module CommunicatorWinRM module CommandFilters # Converts a *nix 'rm' command to a PowerShell equivalent class Rm def filter(command) # rm -Rf /some/dir # rm -R /some/dir # rm -R -f /some/dir # rm -f /some/dir # rm /some/dir cmd_parts = Shellwords.split(command.strip) # Figure out if we need to do this recursively recurse = false cmd_parts.each do |k| argument = k.downcase if argument == '-r' || argument == '-rf' || argument == '-fr' recurse = true break end end # Figure out which argument is the path dir = cmd_parts.pop while !dir.nil? && dir.start_with?('-') dir = cmd_parts.pop end ret_cmd = '' if recurse ret_cmd = "if (Test-Path \"#{dir}\") {Remove-Item \"#{dir}\" -force -recurse}" else ret_cmd = "if (Test-Path \"#{dir}\") {Remove-Item \"#{dir}\" -force}" end return ret_cmd end def accept?(command) command.start_with?('rm ') end end end end end ================================================ FILE: plugins/communicators/winrm/command_filters/test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "shellwords" module VagrantPlugins module CommunicatorWinRM module CommandFilters # Converts a *nix 'test' command to a PowerShell equivalent class Test def filter(command) # test -d /tmp/dir # test -f /tmp/afile # test -L /somelink # test -x /tmp/some.exe cmd_parts = Shellwords.split(command.strip) flag = cmd_parts[1] path = cmd_parts[2] if flag == '-d' check_for_directory(path) elsif flag == '-f' || flag == '-x' check_for_file(path) else check_exists(path) end end def accept?(command) command.start_with?("test ") end private def check_for_directory(path) <<-EOH $p = "#{path}" if ((Test-Path $p) -and (get-item $p).PSIsContainer) { exit 0 } exit 1 EOH end def check_for_file(path) <<-EOH $p = "#{path}" if ((Test-Path $p) -and (!(get-item $p).PSIsContainer)) { exit 0 } exit 1 EOH end def check_exists(path) <<-EOH $p = "#{path}" if (Test-Path $p) { exit 0 } exit 1 EOH end end end end end ================================================ FILE: plugins/communicators/winrm/command_filters/uname.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommunicatorWinRM module CommandFilters # Converts a *nix 'uname' command to a PowerShell equivalent (none) class Uname def filter(command) # uname -s | grep 'Darwin' # uname -s | grep VMkernel # uname -s | grep 'FreeBSD' # uname -s | grep 'Linux' # uname -s | grep NetBSD # uname -s | grep 'OpenBSD' # uname -sr | grep SunOS | grep -v 5.11 # uname -sr | grep 'SunOS 5.11' # uname is used to detect the guest type in Vagrant, so don't bother running # to speed up OS detection '' end def accept?(command) command.start_with?('uname ') end end end end end ================================================ FILE: plugins/communicators/winrm/command_filters/which.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "shellwords" module VagrantPlugins module CommunicatorWinRM module CommandFilters # Converts a *nix 'which' command to a PowerShell equivalent class Which def filter(command) executable = Shellwords.split(command.strip)[1] return <<-EOH $command = [Array](Get-Command "#{executable}" -errorAction SilentlyContinue) if ($null -eq $command) { exit 1 } write-host $command[0].Definition exit 0 EOH end def accept?(command) command.start_with?('which ') end end end end end ================================================ FILE: plugins/communicators/winrm/communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "tempfile" require "timeout" require_relative "helper" require_relative "shell" require_relative "command_filter" module VagrantPlugins module CommunicatorWinRM # Provides communication channel for Vagrant commands via WinRM. class Communicator < Vagrant.plugin("2", :communicator) include Vagrant::Util def self.match?(machine) # This is useless, and will likely be removed in the future (this # whole method). true end def initialize(machine) @cmd_filter = CommandFilter.new() @logger = Log4r::Logger.new("vagrant::communication::winrm") @machine = machine @shell = nil @logger.info("Initializing WinRMCommunicator") end def wait_for_ready(timeout) Timeout.timeout(timeout) do # Wait for winrm_info to be ready winrm_info = nil while true 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 # 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 last_message_repeat_at = 0 while true message = nil begin begin return true if ready? rescue Vagrant::Errors::VagrantError => e @logger.info("WinRM not ready: #{e.inspect}") raise end rescue Errors::ConnectionTimeout message = "Connection timeout." rescue Errors::AuthenticationFailed message = "Authentication failure." rescue Errors::Disconnected message = "Remote connection disconnect." rescue Errors::ConnectionRefused message = "Connection refused." rescue Errors::ConnectionReset message = "Connection reset." rescue Errors::HostDown message = "Host appears down." rescue Errors::NoRoute message = "Host unreachable." rescue Errors::TransientError => e # Any other retryable errors message = e.message end # If we have a message to show, then show it. We don't show # repeated messages unless they've been repeating longer than # 10 seconds. if message message_at = Time.now.to_f show_message = true if last_message == message show_message = (message_at - last_message_repeat_at) > 10.0 end if show_message @machine.ui.detail("Warning: #{message} Retrying...") last_message = message last_message_repeat_at = message_at end end end end rescue Timeout::Error return false end def ready? @logger.info("Checking whether WinRM is ready...") result = Timeout.timeout(@machine.config.winrm.timeout) do shell(true).cmd("hostname") end @logger.info("WinRM is ready!") return true rescue Errors::TransientError, VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady => e # We catch a `TransientError` which would signal that something went # that might work if we wait and retry. @logger.info("WinRM not up: #{e.inspect}") # We reset the shell to trigger calling of winrm_finder again. # This resolves a problem when using vSphere where the winrm_info was not refreshing # thus never getting the correct hostname. @shell = nil return false end def reset! shell(true) end def shell(reload=false) @shell = nil if reload @shell ||= create_shell end def execute(command, opts={}, &block) # If this is a *nix command with no Windows equivalent, don't run it command = @cmd_filter.filter(command) return 0 if command.empty? opts = { command: command, elevated: false, error_check: true, error_class: Errors::WinRMBadExitStatus, error_key: nil, # use the error_class message key good_exit: 0, shell: :powershell, interactive: false, }.merge(opts || {}) opts[:shell] = :elevated if opts[:elevated] opts[:good_exit] = Array(opts[:good_exit]) @logger.debug("#{opts[:shell]} executing:\n#{command}") output = shell.send(opts[:shell], command, opts, &block) execution_output(output, opts) end alias_method :sudo, :execute def test(command, opts=nil) # If this is a *nix command (which we know about) with no Windows # equivalent, assume failure command = @cmd_filter.filter(command) return false if command.empty? opts = { command: command, elevated: false, error_check: false, }.merge(opts || {}) # If we're passed a *nix command which PS can't parse we get exit code # 0, but output in stderr. We need to check both exit code and stderr. output = shell.send(:powershell, command) return output.exitcode == 0 && output.stderr.length == 0 end def upload(from, to) @logger.info("Uploading: #{from} to #{to}") shell.upload(from, to) end def download(from, to) @logger.info("Downloading: #{from} to #{to}") shell.download(from, to) end protected # This creates a new WinRMShell based on the information we know # about this machine. def create_shell winrm_info = Helper.winrm_info(@machine) WinRMShell.new( winrm_info[:host], winrm_info[:port], @machine.config.winrm ) end # Handles the raw WinRM shell result and converts it to a # standard Vagrant communicator result def execution_output(output, opts) if opts[:shell] == :wql return output elsif opts[:error_check] && \ !opts[:good_exit].include?(output.exitcode) raise_execution_error(output, opts) end output.exitcode end def raise_execution_error(output, opts) # WinRM can return multiple stderr and stdout entries error_opts = opts.merge( stdout: output.stdout, stderr: output.stderr ) # Use a different error message key if the caller gave us one, # otherwise use the error's default message error_opts[:_key] = opts[:error_key] if opts[:error_key] # Raise the error, use the type the caller gave us or the comm default raise opts[:error_class], error_opts end end #WinRM class end end ================================================ FILE: plugins/communicators/winrm/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommunicatorWinRM class Config < Vagrant.plugin("2", :config) attr_accessor :username attr_accessor :password attr_accessor :host attr_accessor :port attr_accessor :guest_port attr_accessor :max_tries attr_accessor :retry_delay attr_accessor :timeout attr_accessor :transport attr_accessor :ssl_peer_verification attr_accessor :execution_time_limit attr_accessor :basic_auth_only attr_accessor :codepage def initialize @username = UNSET_VALUE @password = UNSET_VALUE @host = UNSET_VALUE @port = UNSET_VALUE @guest_port = UNSET_VALUE @max_tries = UNSET_VALUE @retry_delay = UNSET_VALUE @timeout = UNSET_VALUE @transport = UNSET_VALUE @ssl_peer_verification = UNSET_VALUE @execution_time_limit = UNSET_VALUE @basic_auth_only = UNSET_VALUE @codepage = UNSET_VALUE end def finalize! @username = "vagrant" if @username == UNSET_VALUE @password = "vagrant" if @password == UNSET_VALUE @transport = :negotiate if @transport == UNSET_VALUE @host = nil if @host == UNSET_VALUE is_ssl = @transport == :ssl @port = (is_ssl ? 5986 : 5985) if @port == UNSET_VALUE @guest_port = (is_ssl ? 5986 : 5985) if @guest_port == UNSET_VALUE @max_tries = 20 if @max_tries == UNSET_VALUE @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 @basic_auth_only = false if @basic_auth_only == UNSET_VALUE @codepage = nil if @codepage == UNSET_VALUE end def validate(machine) errors = [] errors << "winrm.username cannot be nil." if @username.nil? errors << "winrm.password cannot be nil." if @password.nil? errors << "winrm.port cannot be nil." if @port.nil? errors << "winrm.guest_port cannot be nil." if @guest_port.nil? errors << "winrm.max_tries cannot be nil." if @max_tries.nil? errors << "winrm.retry_delay cannot be nil." if @retry_delay.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 unless @basic_auth_only == true || @basic_auth_only == false errors << "winrm.basic_auth_only must be a boolean." end { "WinRM" => errors } end end end end ================================================ FILE: plugins/communicators/winrm/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommunicatorWinRM module Errors # A convenient superclass for all our errors. class WinRMError < Vagrant::Errors::VagrantError error_namespace("vagrant_winrm.errors") end class TransientError < WinRMError end class AuthenticationFailed < WinRMError error_key(:authentication_failed) end class ExecutionError < WinRMError error_key(:execution_error) end class InvalidShell < WinRMError error_key(:invalid_shell) end class WinRMBadExitStatus < WinRMError error_key(:winrm_bad_exit_status) end class WinRMNotReady < WinRMError error_key(:winrm_not_ready) end class WinRMFileTransferError < WinRMError error_key(:winrm_file_transfer_error) end class InvalidTransport < WinRMError error_key(:invalid_transport) end class SSLError < WinRMError error_key(:ssl_error) end class ConnectionTimeout < TransientError error_key(:connection_timeout) end class Disconnected < TransientError error_key(:disconnected) end class ConnectionRefused < TransientError error_key(:connection_refused) end class ConnectionReset < TransientError error_key(:connection_reset) end class HostDown < TransientError error_key(:host_down) end class NoRoute < TransientError error_key(:no_route) end end end end ================================================ FILE: plugins/communicators/winrm/helper.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommunicatorWinRM # This is a helper module that provides some functions to the # communicator. This is extracted into a module so that we can # easily unit test these methods. module Helper # Returns the host and port to access WinRM. # # This asks the provider via the `winrm_info` capability if it # exists, otherwise defaulting to its own heuristics. # # @param [Vagrant::Machine] machine # @return [Hash] def self.winrm_info(machine) info = {} if machine.provider.capability?(:winrm_info) info = machine.provider.capability(:winrm_info) raise Errors::WinRMNotReady if !info end info[:host] ||= winrm_address(machine) info[:port] ||= winrm_port(machine, info[:host] == "127.0.0.1") return info end # Returns the address to access WinRM. This does not contain # the port. # # @param [Vagrant::Machine] machine # @return [String] def self.winrm_address(machine) addr = machine.config.winrm.host return addr if addr ssh_info = machine.ssh_info raise Errors::WinRMNotReady if winrm_info_invalid?(ssh_info) return ssh_info[:host] end def self.winrm_info_invalid?(ssh_info) invalid = (!ssh_info || ssh_info[:host].to_s.empty? || ssh_info[:host].start_with?("169.254")) return invalid end # Returns the port to access WinRM. # # @param [Vagrant::Machine] machine # @return [Integer] def self.winrm_port(machine, local=true) host_port = machine.config.winrm.port if machine.config.winrm.guest_port # If we're not requesting a local port, return # the guest port directly. return machine.config.winrm.guest_port if !local # Search by guest port if we can. We use a provider capability # if we have it. Otherwise, we just scan the Vagrantfile defined # ports. port = nil if machine.provider.capability?(:forwarded_ports) Array(machine.provider.capability(:forwarded_ports)).each do |host, guest| if guest == machine.config.winrm.guest_port port = host break end end end if !port machine.config.vm.networks.each do |type, netopts| next if type != :forwarded_port next if !netopts[:host] if netopts[:guest] == machine.config.winrm.guest_port port = netopts[:host] break end end end # Set it if we found it host_port = port if port end host_port end end end end ================================================ FILE: plugins/communicators/winrm/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommunicatorWinRM autoload :Errors, File.expand_path("../errors", __FILE__) class Plugin < Vagrant.plugin("2") name "winrm communicator" description <<-DESC This plugin allows Vagrant to communicate with remote machines using WinRM. DESC communicator("winrm") do require File.expand_path("../communicator", __FILE__) init! Communicator end config("winrm") do require_relative "config" Config end protected def self.init! return if defined?(@_init) @_init = true # Setup the I18n I18n.load_path << File.expand_path( "templates/locales/comm_winrm.yml", Vagrant.source_root) I18n.reload! # Check if vagrant-winrm plugin is installed and # output warning to user if found if !ENV["VAGRANT_IGNORE_WINRM_PLUGIN"] && Vagrant::Plugin::Manager.instance.installed_plugins.keys.include?("vagrant-winrm") $stderr.puts <<-EOF WARNING: Vagrant has detected the `vagrant-winrm` plugin. Vagrant ships with WinRM support builtin and no longer requires the `vagrant-winrm` plugin. To prevent unexpected errors please uninstall the `vagrant-winrm` plugin using the command shown below: vagrant plugin uninstall vagrant-winrm To disable this warning, set the environment variable `VAGRANT_IGNORE_WINRM_PLUGIN` EOF end # Load the WinRM gem require "vagrant/util/silence_warnings" Vagrant::Util::SilenceWarnings.silence! do require "winrm" end end # @private # Reset the cached init value. This is not considered a public # API and should only be used for testing. def self.reset! send(:remove_instance_variable, :@_init) end end end end ================================================ FILE: plugins/communicators/winrm/shell.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "timeout" require "log4r" require "vagrant/util/retryable" require "vagrant/util/silence_warnings" Vagrant::Util::SilenceWarnings.silence! do require "winrm" end require "winrm-elevated" require "winrm-fs" module VagrantPlugins module CommunicatorWinRM class WinRMShell include Vagrant::Util::Retryable # Exit code generated when user is invalid. Can occur # after a hostname update INVALID_USERID_EXITCODE = -2147024809 # These are the exceptions that we retry because they represent # errors that are generally fixed from a retry and don't # necessarily represent immediate failure cases. @@exceptions_to_retry_on = [ HTTPClient::KeepAliveDisconnected, WinRM::WinRMHTTPTransportError, WinRM::WinRMAuthorizationError, WinRM::WinRMWSManFault, Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH, Timeout::Error ] attr_reader :logger attr_reader :host attr_reader :port attr_reader :username attr_reader :password attr_reader :execution_time_limit attr_reader :config def initialize(host, port, config) @logger = Log4r::Logger.new("vagrant::communication::winrmshell") @logger.debug("initializing WinRMShell") @host = host @port = port @username = config.username @password = config.password @execution_time_limit = config.execution_time_limit @config = config end def powershell(command, opts = {}, &block) connection.shell(:powershell) do |shell| execute_with_rescue(shell, command, &block) end end def cmd(command, opts = {}, &block) shell_opts = {} shell_opts[:codepage] = @config.codepage if @config.codepage connection.shell(:cmd, shell_opts) do |shell| execute_with_rescue(shell, command, &block) end end def elevated(command, opts = {}, &block) connection.shell(:elevated) do |shell| shell.interactive_logon = opts[:interactive] || false result = execute_with_rescue(shell, command, &block) if result.exitcode == INVALID_USERID_EXITCODE && result.stderr.include?(":UserId:") uname = shell.username ename = elevated_username if uname != ename @logger.warn("elevated command failed due to username error") @logger.warn("retrying command using machine prefixed username - #{ename}") begin shell.username = ename result = execute_with_rescue(shell, command, &block) ensure shell.username = uname end end end result end end def wql(query, opts = {}, &block) retryable(tries: @config.max_tries, on: @@exceptions_to_retry_on, sleep: @config.retry_delay) do connection.run_wql(query) end rescue => e raise_winrm_exception(e, "run_wql", query) end # @param from [Array, String] a single path or folder, or an # array of paths and folders to upload to the guest # @param to [String] a path or folder on the guest to upload to # @return [FixNum] Total size transfered from host to guest def upload(from, to) file_manager = WinRM::FS::FileManager.new(connection) if from.is_a?(String) && File.directory?(from) if from.end_with?(".") from = from[0, from.length - 1] else to = File.join(to, File.basename(File.expand_path(from))) end end if from.is_a?(Array) # Preserve return FixNum of bytes transfered return_bytes = 0 from.each do |file| return_bytes += file_manager.upload(file, to) end return return_bytes else file_manager.upload(from, to) end end def download(from, to) file_manager = WinRM::FS::FileManager.new(connection) file_manager.download(from, to) end protected def execute_with_rescue(shell, command, &block) handle_output(shell, command, &block) rescue => e raise_winrm_exception(e, shell.class.name.split("::").last, command) end def handle_output(shell, command, &block) output = shell.run(command) do |out, err| block.call(:stdout, out) if block_given? && out block.call(:stderr, err) if block_given? && err end @logger.debug("Output: #{output.inspect}") # Verify that we didn't get a parser error, and if so we should # set the exit code to 1. Parse errors return exit code 0 so we # need to do this. if output.exitcode == 0 if output.stderr.include?("ParserError") @logger.warn("Detected ParserError, setting exit code to 1") output.exitcode = 1 end end return output end def raise_winrm_exception(exception, shell = nil, command = nil) case exception when WinRM::WinRMAuthorizationError raise Errors::AuthenticationFailed, user: @config.username, password: @config.password, endpoint: endpoint, message: exception.message when WinRM::WinRMHTTPTransportError raise Errors::ExecutionError, shell: shell, command: command, message: exception.message when OpenSSL::SSL::SSLError raise Errors::SSLError, message: exception.message when HTTPClient::TimeoutError raise Errors::ConnectionTimeout, message: exception.message when IO::TimeoutError raise Errors::ConnectionTimeout when Errno::ETIMEDOUT raise Errors::ConnectionTimeout # This is raised if the connection timed out when Errno::ECONNREFUSED # This is raised if we failed to connect the max amount of times raise Errors::ConnectionRefused when Errno::ECONNRESET # This is raised if we failed to connect the max number of times # due to an ECONNRESET. raise Errors::ConnectionReset when Errno::EHOSTDOWN # This is raised if we get an ICMP DestinationUnknown error. raise Errors::HostDown when Errno::EHOSTUNREACH # This is raised if we can't work out how to route traffic. raise Errors::NoRoute else raise Errors::ExecutionError, shell: shell, command: command, message: exception.message end end def new_connection @logger.info("Attempting to connect to WinRM...") @logger.info(" - Host: #{@host}") @logger.info(" - Port: #{@port}") @logger.info(" - Username: #{@config.username}") @logger.info(" - Transport: #{@config.transport}") client = ::WinRM::Connection.new(endpoint_options) client.logger = @logger client end def connection @connection ||= new_connection end def endpoint case @config.transport.to_sym when :ssl "https://#{@host}:#{@port}/wsman" when :plaintext, :negotiate "http://#{@host}:#{@port}/wsman" else raise Errors::WinRMInvalidTransport, transport: @config.transport end end def endpoint_options { endpoint: endpoint, transport: @config.transport, operation_timeout: @config.timeout, user: @username, password: @password, host: @host, port: @port, basic_auth_only: @config.basic_auth_only, no_ssl_peer_verification: !@config.ssl_peer_verification, retry_delay: @config.retry_delay, retry_limit: @config.max_tries } end def elevated_username if username.include?("\\") return username end computername = "" powershell("Write-Output $env:computername") do |type, data| computername << data if type == :stdout end computername.strip! if computername.empty? return username end "#{computername}\\#{username}" end end #WinShell class end end ================================================ FILE: plugins/communicators/winssh/communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../ssh/communicator", __FILE__) require "net/sftp" module VagrantPlugins module CommunicatorWinSSH # This class provides communication with a Windows VM running # the Windows native port of OpenSSH class Communicator < VagrantPlugins::CommunicatorSSH::Communicator # Command to run when checking if connection is ready and working READY_COMMAND="dir" def initialize(machine) super @logger = Log4r::Logger.new("vagrant::communication::winssh") end # Wrap the shell if required. By default we are using powershell # which requires no modification. If cmd is defined as shell, add # prefix to start within cmd.exe def shell_cmd(opts) case opts[:shell].to_s when "cmd" "cmd.exe /c '#{opts[:command]}'" else opts[:command] end end # Executes the command on an SSH connection within a login shell. def shell_execute(connection, command, **opts) opts[:shell] ||= machine_config_ssh.shell command = shell_cmd(opts.merge(command: command)) @logger.info("Execute: #{command} - opts: #{opts}") exit_status = nil # Open the channel so we can execute or command channel = connection.open_channel do |ch| marker_found = false data_buffer = '' stderr_marker_found = false stderr_data_buffer = '' @logger.debug("Base SSH exec command: #{command}") command = "$ProgressPreference = 'SilentlyContinue';Write-Output #{CMD_GARBAGE_MARKER};[Console]::Error.WriteLine('#{CMD_GARBAGE_MARKER}');#{command}" ch.exec(command) do |ch2, _| # Setup the channel callbacks so we can get data and exit status ch2.on_data do |ch3, data| # Filter out the clear screen command data = remove_ansi_escape_codes(data) if !marker_found data_buffer << data marker_index = data_buffer.index(CMD_GARBAGE_MARKER) if marker_index marker_found = true data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size) data.replace(data_buffer) data_buffer = nil end end if block_given? && marker_found yield :stdout, data end end ch2.on_extended_data do |ch3, type, data| # Filter out the clear screen command data = remove_ansi_escape_codes(data) @logger.debug("stderr: #{data}") if !stderr_marker_found stderr_data_buffer << data marker_index = stderr_data_buffer.index(CMD_GARBAGE_MARKER) if marker_index stderr_marker_found = true stderr_data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size) data.replace(stderr_data_buffer.lstrip) data_buffer = nil end end if block_given? && stderr_marker_found && !data.empty? yield :stderr, data end end ch2.on_request("exit-status") do |ch3, data| exit_status = data.read_long @logger.debug("Exit status: #{exit_status}") # Close the channel, since after the exit status we're # probably done. This fixes up issues with hanging. ch.close end end end begin keep_alive = nil if @machine.config.ssh.keep_alive # Begin sending keep-alive packets while we wait for the script # to complete. This avoids connections closing on long-running # scripts. keep_alive = Thread.new do loop do sleep 5 @logger.debug("Sending SSH keep-alive...") connection.send_global_request("keep-alive@openssh.com") end end end # Wait for the channel to complete begin channel.wait rescue Errno::ECONNRESET, IOError @logger.info( "SSH connection unexpected closed. Assuming reboot or something.") exit_status = 0 pty = false rescue Net::SSH::ChannelOpenFailed raise Vagrant::Errors::SSHChannelOpenFail rescue Net::SSH::Disconnect raise Vagrant::Errors::SSHDisconnected end ensure # Kill the keep-alive thread keep_alive.kill if keep_alive end # Return the final exit status return exit_status end def machine_config_ssh @machine.config.winssh end def download(from, to=nil) @logger.debug("Downloading: #{from} to #{to}") sftp_connect do |sftp| sftp.download!(from, to) end end # Note: I could not get Net::SFTP to throw a permissions denied error, # even when uploading to a directory where I did not have write # privileges. I believe this is because Windows SSH sessions are started # in an elevated process. def upload(from, to) to = Vagrant::Util::Platform.unix_windows_path(to) @logger.debug("Uploading: #{from} to #{to}") if File.directory?(from) if from.end_with?(".") @logger.debug("Uploading directory contents of: #{from}") from = from.sub(/\.$/, "") else @logger.debug("Uploading full directory container of: #{from}") to = File.join(to, File.basename(File.expand_path(from))) end end sftp_connect do |sftp| uploader = lambda do |path, remote_dest=nil| if File.directory?(path) Dir.new(path).each do |entry| next if entry == "." || entry == ".." full_path = File.join(path, entry) dest = File.join(to, path.sub(/^#{Regexp.escape(from)}/, "")) sftp.mkdir(dest) uploader.call(full_path, dest) end else if remote_dest dest = File.join(remote_dest, File.basename(path)) else dest = to if to.end_with?(File::SEPARATOR) dest = File.join(to, File.basename(path)) end end @logger.debug("Ensuring remote directory exists for destination upload") sftp.mkdir(File.dirname(dest)) @logger.debug("Uploading file #{path} to remote #{dest}") upload_file = File.open(path, "rb") begin sftp.upload!(upload_file, dest) ensure upload_file.close end end end uploader.call(from) end end # Opens an SFTP connection and yields it so that you can download and # upload files. SFTP works more reliably than SCP on Windows due to # issues with shell quoting and escaping. def sftp_connect # Connect to SFTP and yield the SFTP object connect do |connection| return yield connection.sftp end end protected # The WinSSH communicator connection provides isolated modification # to the generated connection instances. This modification forces # all provided commands to run within powershell def connect(**opts) connection = nil super { |c| connection = c } if !connection.instance_variable_get(:@winssh_patched) open_chan = connection.method(:open_channel) connection.define_singleton_method(:open_channel) do |*args, &chan_block| open_chan.call(*args) do |ch| exec = ch.method(:exec) ch.define_singleton_method(:exec) do |command, &block| command = Base64.strict_encode64(command.encode("UTF-16LE", "UTF-8")) command = "powershell -NoLogo -NonInteractive -ExecutionPolicy Bypass " \ "-NoProfile -EncodedCommand #{command}" exec.call(command, &block) end chan_block.call(ch) end end connection.instance_variable_set(:@winssh_patched, true) end if block_given? yield connection else connection end end end end end ================================================ FILE: plugins/communicators/winssh/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../kernel_v2/config/ssh", __FILE__) module VagrantPlugins module CommunicatorWinSSH class Config < VagrantPlugins::Kernel_V2::SSHConfig attr_accessor :upload_directory def initialize super @upload_directory = UNSET_VALUE end def finalize! @shell = "powershell" if @shell == UNSET_VALUE @sudo_command = "%c" if @sudo_command == UNSET_VALUE @upload_directory = "C:/Windows/Temp" if @upload_directory == UNSET_VALUE if @export_command_template == UNSET_VALUE @export_command_template = '$env:%ENV_KEY%="%ENV_VALUE%"' end super end def to_s "WINSSH" end # Remove configuration options from regular SSH that are # not used within this communicator undef :forward_x11 undef :pty end end end ================================================ FILE: plugins/communicators/winssh/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CommunicatorWinSSH class Plugin < Vagrant.plugin("2") name "windows ssh communicator" description <<-DESC DESC communicator("winssh") do require File.expand_path("../communicator", __FILE__) Communicator end config("winssh") do require_relative "config" Config end end end end ================================================ FILE: plugins/guests/alma/cap/flavor.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestAlma module Cap class Flavor def self.flavor(machine) # Read the version file version = "" machine.communicate.sudo("source /etc/os-release && printf $VERSION_ID") do |type, data| if type == :stdout version = data.split(".").first.to_i end end if version.nil? || version < 1 :alma else "alma_#{version}".to_sym end end end end end end ================================================ FILE: plugins/guests/alma/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../linux/guest" module VagrantPlugins module GuestAlma class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "almalinux".freeze end end end ================================================ FILE: plugins/guests/alma/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestAlma class Plugin < Vagrant.plugin("2") name "Alma guest" description "Alma guest support." guest(:alma, :redhat) do require_relative "guest" Guest end guest_capability(:alma, :flavor) do require_relative "cap/flavor" Cap::Flavor end end end end ================================================ FILE: plugins/guests/alpine/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/guest_hosts' module VagrantPlugins module GuestAlpine module Cap class ChangeHostName include Vagrant::Util::GuestHosts::Linux def self.change_host_name(machine, name) new(machine, name).change! end attr_reader :machine, :new_hostname def initialize(machine, new_hostname) @machine = machine @new_hostname = new_hostname end def change! update_etc_hosts return unless should_change? update_etc_hostname refresh_hostname_service update_mailname renew_dhcp end def should_change? new_hostname != current_hostname end def current_hostname @current_hostname ||= fetch_current_hostname end def fetch_current_hostname hostname = '' machine.communicate.sudo 'hostname -f' do |type, data| hostname = data.chomp if type == :stdout && hostname.empty? end hostname end def update_etc_hostname machine.communicate.sudo("echo '#{short_hostname}' > /etc/hostname") end # /etc/hosts should resemble: # 127.0.0.1 localhost # 127.0.1.1 host.fqdn.com host.fqdn host def update_etc_hosts comm = machine.communicate network_with_hostname = machine.config.vm.networks.map {|_, c| c if c[:hostname] }.compact[0] if network_with_hostname replace_host(comm, new_hostname, network_with_hostname[:ip]) else add_hostname_to_loopback_interface(comm, new_hostname) end end def refresh_hostname_service machine.communicate.sudo('hostname -F /etc/hostname') end def update_mailname machine.communicate.sudo('hostname -f > /etc/mailname') end def renew_dhcp machine.communicate.sudo('ifdown -a; ifup -a; ifup eth0') end def short_hostname new_hostname.split('.').first end end end end end ================================================ FILE: plugins/guests/alpine/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/AbcSize # rubocop:disable Style/BracesAroundHashParameters # # FIXME: address disabled warnings # require 'set' require 'tempfile' require 'pathname' require 'vagrant/util/template_renderer' module VagrantPlugins module GuestAlpine module Cap class ConfigureNetworks include Vagrant::Util def self.configure_networks(machine, networks) machine.communicate.tap do |comm| # First, remove any previous network modifications # from the interface file. comm.sudo("sed -e '/^#VAGRANT-BEGIN/,$ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces.pre") comm.sudo("sed -ne '/^#VAGRANT-END/,$ p' /etc/network/interfaces | tail -n +2 > /tmp/vagrant-network-interfaces.post") # Accumulate the configurations to add to the interfaces file as # well as what interfaces we're actually configuring since we use that # later. interfaces = Set.new entries = [] networks.each do |network| interfaces.add(network[:interface]) entry = TemplateRenderer.render("guests/alpine/network_#{network[:type]}", { options: network }) entries << entry end # Perform the careful dance necessary to reconfigure # the network interfaces temp = Tempfile.new('vagrant') temp.binmode temp.write(entries.join("\n")) temp.close comm.upload(temp.path, '/tmp/vagrant-network-entry') # Bring down all the interfaces we're reconfiguring. By bringing down # each specifically, we avoid reconfiguring eth0 (the NAT interface) so # SSH never dies. interfaces.each do |interface| comm.sudo("if [[ $(/sbin/ip a show eth#{interface} | grep UP) ]]; then /sbin/ifdown eth#{interface} 2> /dev/null; fi") comm.sudo("/sbin/ip addr flush dev eth#{interface} 2> /dev/null") end comm.sudo('cat /tmp/vagrant-network-interfaces.pre /tmp/vagrant-network-entry /tmp/vagrant-network-interfaces.post > /etc/network/interfaces') comm.sudo('rm -f /tmp/vagrant-network-interfaces.pre /tmp/vagrant-network-entry /tmp/vagrant-network-interfaces.post') # Bring back up each network interface, reconfigured interfaces.each do |interface| comm.sudo("/sbin/ifup eth#{interface}") end end end end end end end ================================================ FILE: plugins/guests/alpine/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # rubocop:disable Style/RedundantBegin # rubocop:disable Lint/HandleExceptions # # FIXME: address disabled warnings # module VagrantPlugins module GuestAlpine module Cap class Halt def self.halt(machine) begin machine.communicate.sudo('poweroff') rescue Net::SSH::Disconnect, IOError # Ignore, this probably means connection closed because it # shut down and SSHd was stopped. end end end end end end ================================================ FILE: plugins/guests/alpine/cap/nfs_client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestAlpine module Cap class NFSClient def self.nfs_client_install(machine) machine.communicate.sudo('apk update') machine.communicate.sudo('apk add --upgrade nfs-utils') machine.communicate.sudo('rc-update add rpc.statd') machine.communicate.sudo('rc-service rpc.statd start') end end end end end ================================================ FILE: plugins/guests/alpine/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestAlpine module Cap class RSync def self.rsync_installed(machine) machine.communicate.test('test -f /usr/bin/rsync') end def self.rsync_install(machine) machine.communicate.tap do |comm| comm.sudo('apk add --update-cache rsync') end end end end end end ================================================ FILE: plugins/guests/alpine/cap/smb.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestAlpine module Cap class SMB def self.smb_install(machine) machine.communicate.tap do |comm| comm.sudo('apk add cifs-utils') end end end end end end ================================================ FILE: plugins/guests/alpine/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant' module VagrantPlugins module GuestAlpine class Guest < Vagrant.plugin('2', :guest) def detect?(machine) machine.communicate.test('cat /etc/alpine-release') end end end end ================================================ FILE: plugins/guests/alpine/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant' module VagrantPlugins module GuestAlpine class Plugin < Vagrant.plugin('2') name 'Alpine guest' description 'Alpine Linux guest support.' guest(:alpine, :linux) do require File.expand_path('../guest', __FILE__) Guest end guest_capability(:alpine, :configure_networks) do require_relative 'cap/configure_networks' Cap::ConfigureNetworks end guest_capability(:alpine, :halt) do require_relative 'cap/halt' Cap::Halt end guest_capability(:alpine, :change_host_name) do require_relative 'cap/change_host_name' Cap::ChangeHostName end guest_capability(:alpine, :nfs_client_install) do require_relative 'cap/nfs_client' Cap::NFSClient end guest_capability(:alpine, :rsync_installed) do require_relative 'cap/rsync' Cap::RSync end guest_capability(:alpine, :rsync_install) do require_relative 'cap/rsync' Cap::RSync end guest_capability(:alpine, :smb_install) do require_relative 'cap/smb' Cap::SMB end def self.check_community_plugin plugins = Vagrant::Plugin::Manager.instance.installed_plugins if plugins.keys.include?("vagrant-alpine") $stderr.puts <<-EOF WARNING: Vagrant has detected the `vagrant-alpine` plugin. This plugin's functionality has been merged into the main Vagrant project and should be considered deprecated. To uninstall the plugin, run the command shown below: vagrant plugin uninstall vagrant-alpine EOF end end self.check_community_plugin end end end ================================================ FILE: plugins/guests/alt/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../../linux/cap/change_host_name' module VagrantPlugins module GuestALT module Cap class ChangeHostName extend VagrantPlugins::GuestLinux::Cap::ChangeHostName def self.change_name_command(name) basename = name.split(".", 2)[0] return <<-EOH.gsub(/^ {14}/, '') # Save current hostname saved in /etc/hosts CURRENT_HOSTNAME_FULL="$(hostname -f)" CURRENT_HOSTNAME_SHORT="$(hostname -s)" # Set the hostname - use hostnamectl if available if command -v hostnamectl; then hostnamectl set-hostname --static '#{name}' hostnamectl set-hostname --transient '#{name}' else hostname '#{name}' fi # Persist hostname change across reboots if [ -f /etc/sysconfig/network ]; then sed -i 's/\\(HOSTNAME=\\).*/\\1#{name}/' /etc/sysconfig/network elif [ -f /etc/hostname ]; then sed -i 's/.*/#{name}/' /etc/hostname else echo 'Unrecognized system. Hostname change may not persist across reboots.' exit 0 fi # Restart the network if we find a recognized SYS V init script if command -v service; then if [ -f /etc/init.d/network ]; then service network restart elif [ -f /etc/init.d/networking ]; then service networking restart elif [ -f /etc/init.d/NetworkManager ]; then service NetworkManager restart else echo 'Unrecognized system. Networking was not restarted following hostname change.' exit 0 fi fi EOH end end end end end ================================================ FILE: plugins/guests/alt/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestALT module Cap class ConfigureNetworks include Vagrant::Util extend Vagrant::Util::GuestInspection::Linux def self.configure_networks(machine, networks) comm = machine.communicate network_scripts_dir = machine.guest.capability(:network_scripts_dir) commands = {:start => [], :middle => [], :end => []} interfaces = machine.guest.capability(:network_interfaces) # Check if NetworkManager is installed on the system nmcli_installed = nmcli?(comm) net_configs = machine.config.vm.networks.map do |type, opts| opts if type.to_s.end_with?("_network") end.compact networks.each.with_index do |network, i| network[:device] = interfaces[network[:interface]] extra_opts = net_configs[i] ? net_configs[i].dup : {} if nmcli_installed # Now check if the device is actively being managed by NetworkManager nm_controlled = nm_controlled?(comm, network[:device]) end if !extra_opts.key?(:nm_controlled) extra_opts[:nm_controlled] = !!nm_controlled end extra_opts[:nm_controlled] = case extra_opts[:nm_controlled] when true "yes" when false, nil "no" else extra_opts[:nm_controlled].to_s end if extra_opts[:nm_controlled] == "yes" && !nmcli_installed raise Vagrant::Errors::NetworkManagerNotInstalled, device: network[:device] end # Render a new configuration template_options = extra_opts.merge(network) # ALT expects netmasks to be in the CIDR notation, but users may # specify IPV4 netmasks like "255.255.255.0". This magic converts # the netmask to the proper value. if template_options[:netmask] && template_options[:netmask].to_s.include?(".") template_options[:netmask] = (32-Math.log2((IPAddr.new(template_options[:netmask], Socket::AF_INET).to_i^0xffffffff)+1)).to_i end options_entry = TemplateRenderer.render("guests/alt/network_#{network[:type]}", options: template_options) # Upload the new configuration options_remote_path = "/tmp/vagrant-network-entry-#{network[:device]}-#{Time.now.to_i}-#{i}" ipv4_address_remote_path = "/tmp/vagrant-network-ipv4-address-entry-#{network[:device]}-#{Time.now.to_i}-#{i}" ipv4_route_remote_path = "/tmp/vagrant-network-ipv4-route-entry-#{network[:device]}-#{Time.now.to_i}-#{i}" Tempfile.open("vagrant-alt-configure-networks") do |f| f.binmode f.write(options_entry) f.fsync f.close machine.communicate.upload(f.path, options_remote_path) end # Add the new interface and bring it back up iface_path = "#{network_scripts_dir}/ifaces/#{network[:device]}" if network[:type].to_sym == :static ipv4_address_entry = TemplateRenderer.render("guests/alt/network_ipv4address", options: template_options) # Upload the new ipv4address configuration Tempfile.open("vagrant-alt-configure-ipv4-address") do |f| f.binmode f.write(ipv4_address_entry) f.fsync f.close machine.communicate.upload(f.path, ipv4_address_remote_path) end ipv4_route_entry = TemplateRenderer.render("guests/alt/network_ipv4route", options: template_options) # Upload the new ipv4route configuration Tempfile.open("vagrant-alt-configure-ipv4-route") do |f| f.binmode f.write(ipv4_route_entry) f.fsync f.close machine.communicate.upload(f.path, ipv4_route_remote_path) end end if nm_controlled and extra_opts[:nm_controlled] == "yes" commands[:start] << "nmcli d disconnect iface '#{network[:device]}'" else commands[:start] << "/sbin/ifdown '#{network[:device]}'" end commands[:middle] << "mkdir -p '#{iface_path}'" commands[:middle] << "mv -f '#{options_remote_path}' '#{iface_path}/options'" if network[:type].to_sym == :static commands[:middle] << "mv -f '#{ipv4_address_remote_path}' '#{iface_path}/ipv4address'" commands[:middle] << "mv -f '#{ipv4_route_remote_path}' '#{iface_path}/ipv4route'" end if extra_opts[:nm_controlled] == "no" commands[:end] << "/sbin/ifup '#{network[:device]}'" end end if nmcli_installed commands[:middle] << "((systemctl | grep NetworkManager.service) && systemctl restart NetworkManager) || " \ "(test -f /etc/init.d/NetworkManager && /etc/init.d/NetworkManager restart)" end commands = commands[:start] + commands[:middle] + commands[:end] comm.sudo(commands.join("\n")) comm.wait_for_ready(5) end end end end end ================================================ FILE: plugins/guests/alt/cap/flavor.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestALT module Cap class Flavor def self.flavor(machine) comm = machine.communicate # Read the version file if comm.test("test -f /etc/os-release") name = nil comm.sudo("grep NAME /etc/os-release") do |type, data| if type == :stdout name = data.split("=")[1].gsub!(/\A"|"\Z/, '') end end if !name.nil? and name == "Sisyphus" return :alt end version = nil comm.sudo("grep VERSION_ID /etc/os-release") do |type, data| if type == :stdout verstr = data.split("=")[1] if verstr == "p8" version = 8 elsif verstr =~ /^[[\d]]/ version = verstr.chomp.to_i subversion = verstr.chomp.split(".")[1].to_i if subversion > 90 version += 1 end end end end if version.nil? or version == 0 return :alt else return :"alt_#{version}" end else output = "" comm.sudo("cat /etc/altlinux-release") do |_, data| output = data end # Detect various flavors we care about if output =~ /(ALT SP|ALT Education|ALT Workstation|ALT Workstation K|ALT Linux starter kit)\s*8(\.[1-9])?( .+)?/i return :alt_8 elsif output =~ /ALT\s+8(\.[1-9])?( .+)?\s.+/i return :alt_8 elsif output =~ /ALT Linux p8( .+)?/i return :alt_8 else return :alt end end end end end end end ================================================ FILE: plugins/guests/alt/cap/network_scripts_dir.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestALT module Cap class NetworkScriptsDir def self.network_scripts_dir(machine) "/etc/net" end end end end end ================================================ FILE: plugins/guests/alt/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestALT module Cap class RSync def self.rsync_install(machine) machine.communicate.sudo <<-EOH.gsub(/^ {12}/, '') apt-get install -y -qq install rsync EOH end end end end end ================================================ FILE: plugins/guests/alt/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestALT class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("cat /etc/altlinux-release") end end end end ================================================ FILE: plugins/guests/alt/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestALT class Plugin < Vagrant.plugin("2") name "ALT Platform guest" description "ALT Platform guest support." guest(:alt, :redhat) do require_relative "guest" Guest end guest_capability(:alt, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:alt, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:alt, :flavor) do require_relative "cap/flavor" Cap::Flavor end guest_capability(:alt, :network_scripts_dir) do require_relative "cap/network_scripts_dir" Cap::NetworkScriptsDir end guest_capability(:alt, :rsync_install) do require_relative "cap/rsync" Cap::RSync end end end end ================================================ FILE: plugins/guests/amazon/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../debian/cap/configure_networks" require_relative "../../redhat/cap/configure_networks" module VagrantPlugins module GuestAmazon module Cap class ConfigureNetworks extend Vagrant::Util::GuestInspection::Linux def self.configure_networks(machine, networks) # If the guest is using networkd, call the debian capability # as it will handle networkd. Otherwise, fallback to using # the RHEL capability. if systemd_networkd?(machine.communicate) VagrantPlugins::GuestDebian::Cap::ConfigureNetworks.configure_networks(machine, networks) else VagrantPlugins::GuestRedHat::Cap::ConfigureNetworks.configure_networks(machine, networks) end end end end end end ================================================ FILE: plugins/guests/amazon/cap/flavor.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestAmazon module Cap class Flavor def self.flavor(machine) # Amazon AMI is a frankenstien RHEL, mainly based on 6 # Maybe in the future if they incorporate RHEL 7 elements # this should be extended to read /etc/os-release or similar return :rhel end end end end end ================================================ FILE: plugins/guests/amazon/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestAmazon class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("grep 'Amazon Linux' /etc/os-release") end end end end ================================================ FILE: plugins/guests/amazon/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestAmazon class Plugin < Vagrant.plugin("2") name "Amazon Linux guest" description "Amazon linux guest support." guest(:amazon, :redhat) do require_relative "guest" Guest end guest_capability(:amazon, :flavor) do require_relative "cap/flavor" Cap::Flavor end guest_capability(:amazon, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end end end end ================================================ FILE: plugins/guests/arch/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../../linux/cap/change_host_name' module VagrantPlugins module GuestArch module Cap class ChangeHostName extend VagrantPlugins::GuestLinux::Cap::ChangeHostName def self.change_name_command(name) "hostnamectl set-hostname '#{name.split(".", 2).first}'" end end end end end ================================================ FILE: plugins/guests/arch/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ipaddr" require "socket" require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestArch module Cap class ConfigureNetworks include Vagrant::Util extend Vagrant::Util::GuestInspection::Linux extend Vagrant::Util::GuestNetworks::Linux def self.configure_networks(machine, networks) comm = machine.communicate return configure_network_manager(machine, networks) if systemd_network_manager?(comm) commands = [] uses_systemd_networkd = systemd_networkd?(comm) interfaces = machine.guest.capability(:network_interfaces) networks.each.with_index do |network, i| network[:device] = interfaces[network[:interface]] # Arch expects netmasks to be in the "24" or "64", but users may # specify IPV4 netmasks like "255.255.255.0". This magic converts # the netmask to the proper value. if network[:netmask] && network[:netmask].to_s.include?(".") network[:netmask] = (32-Math.log2((IPAddr.new(network[:netmask], Socket::AF_INET).to_i^0xffffffff)+1)).to_i end if uses_systemd_networkd entry = TemplateRenderer.render("guests/arch/systemd_networkd/network_#{network[:type]}", options: network, ) else entry = TemplateRenderer.render("guests/arch/default_network/network_#{network[:type]}", options: network, ) end remote_path = "/tmp/vagrant-network-#{network[:device]}-#{Time.now.to_i}-#{i}" Tempfile.open("vagrant-arch-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close comm.upload(f.path, remote_path) end if uses_systemd_networkd commands << <<-EOH.gsub(/^ {16}/, '').rstrip # Configure #{network[:device]} chmod 0644 '#{remote_path}' && mv '#{remote_path}' '/etc/systemd/network/#{network[:device]}.network' && networkctl reload EOH else commands << <<-EOH.gsub(/^ {16}/, '').rstrip # Configure #{network[:device]} mv '#{remote_path}' '/etc/netctl/#{network[:device]}' && ip link set '#{network[:device]}' down && netctl restart '#{network[:device]}' && netctl enable '#{network[:device]}' EOH end end # Run all the network modification commands in one communicator call. comm.sudo(commands.join(" && \n")) end end end end end ================================================ FILE: plugins/guests/arch/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestArch module Cap class NFS def self.nfs_client_installed(machine) machine.communicate.test("pacman -Q nfs-utils") end def self.nfs_pre(machine) comm = machine.communicate # There is a bug in NFS where the rpcbind functionality is not started # and it's not a dependency of nfs-utils. Read more here: # # https://bbs.archlinux.org/viewtopic.php?id=193410 # comm.sudo <<-EOH.gsub(/^ {12}/, "") systemctl enable rpcbind && systemctl start rpcbind EOH end def self.nfs_client_install(machine) comm = machine.communicate comm.sudo <<-EOH.gsub(/^ {12}/, "") pacman --noconfirm -Syy && pacman --noconfirm -S nfs-utils ntp EOH end end end end end ================================================ FILE: plugins/guests/arch/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestArch module Cap class RSync def self.rsync_install(machine) comm = machine.communicate comm.sudo <<-EOH.gsub(/^ {12}/, '') pacman -Sy --noconfirm pacman -S --noconfirm rsync exit $? EOH end end end end end ================================================ FILE: plugins/guests/arch/cap/smb.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestArch module Cap class SMB def self.smb_install(machine) comm = machine.communicate if !comm.test("test -f /usr/bin/mount.cifs") comm.sudo <<-EOH.gsub(/^ {14}/, '') pacman -Sy --noconfirm pacman -S --noconfirm smbclient cifs-utils EOH end end end end end end ================================================ FILE: plugins/guests/arch/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestArch class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("cat /etc/arch-release") end end end end ================================================ FILE: plugins/guests/arch/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestArch class Plugin < Vagrant.plugin("2") name "Arch guest" description "Arch guest support." guest(:arch, :linux) do require_relative "guest" Guest end guest_capability(:arch, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:arch, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:arch, :nfs_client_install) do require_relative "cap/nfs" Cap::NFS end guest_capability(:arch, :nfs_client_installed) do require_relative "cap/nfs" Cap::NFS end guest_capability(:arch, :nfs_pre) do require_relative "cap/nfs" Cap::NFS end guest_capability(:arch, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:arch, :smb_install) do require_relative "cap/smb" Cap::SMB end end end end ================================================ FILE: plugins/guests/atomic/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../../linux/cap/change_host_name' module VagrantPlugins module GuestAtomic module Cap class ChangeHostName extend VagrantPlugins::GuestLinux::Cap::ChangeHostName def self.change_name_command(name) "hostnamectl set-hostname '#{name.split(".", 2).first}'" end end end end end ================================================ FILE: plugins/guests/atomic/cap/docker.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestAtomic module Cap module Docker def self.docker_daemon_running(machine) machine.communicate.test("test -S /run/docker.sock") end end end end end ================================================ FILE: plugins/guests/atomic/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestAtomic class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("grep 'ostree=.*atomic' /proc/cmdline") end end end end ================================================ FILE: plugins/guests/atomic/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestAtomic class Plugin < Vagrant.plugin("2") name "Atomic Host guest" description "Atomic Host guest support." guest(:atomic, :fedora) do require_relative "guest" Guest end guest_capability(:atomic, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:atomic, :docker_daemon_running) do require_relative "cap/docker" Cap::Docker end end end end ================================================ FILE: plugins/guests/bsd/cap/file_system.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestBSD module Cap class FileSystem # Create a temporary file or directory on the guest # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [Hash] opts Path options # @return [String] path to temporary file or directory def self.create_tmp_path(machine, opts) template = "vagrant" cmd = ["mktemp"] if opts[:type] == :directory cmd << "-d" end cmd << "-t" cmd << template tmp_path = "" machine.communicate.execute(cmd.join(" ")) do |type, data| if type == :stdout tmp_path << data end end tmp_path.strip end # Decompress tgz file on guest to given location # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [String] compressed_file Path to compressed file on guest # @param [String] destination Path for decompressed files on guest def self.decompress_tgz(machine, compressed_file, destination, opts={}) comm = machine.communicate extract_dir = create_tmp_path(machine, type: :directory) cmds = [] if opts[:type] == :directory cmds << "mkdir -p '#{destination}'" else cmds << "mkdir -p '#{File.dirname(destination)}'" end cmds += [ "tar -C '#{extract_dir}' -xzf '#{compressed_file}'", "mv '#{extract_dir}'/* '#{destination}'", "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] cmds.each{ |cmd| comm.execute(cmd) } true end # Decompress zip file on guest to given location # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [String] compressed_file Path to compressed file on guest # @param [String] destination Path for decompressed files on guest def self.decompress_zip(machine, compressed_file, destination, opts={}) comm = machine.communicate extract_dir = create_tmp_path(machine, type: :directory) cmds = [] if opts[:type] == :directory cmds << "mkdir -p '#{destination}'" else cmds << "mkdir -p '#{File.dirname(destination)}'" end cmds += [ "unzip '#{compressed_file}' -d '#{extract_dir}'", "mv '#{extract_dir}'/* '#{destination}'", "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] cmds.each{ |cmd| comm.execute(cmd) } true end end end end end ================================================ FILE: plugins/guests/bsd/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestBSD module Cap class Halt def self.halt(machine) begin machine.communicate.sudo("/sbin/shutdown -p now", shell: "sh") rescue IOError, Vagrant::Errors::SSHDisconnected # Do nothing, because it probably means the machine shut down # and SSH connection was lost. end end end end end end ================================================ FILE: plugins/guests/bsd/cap/mount_virtualbox_shared_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestBSD module Cap class MountVirtualBoxSharedFolder # BSD-based guests do not currently support VirtualBox synced folders. # Instead of raising an error about a missing capability, this defines # the capability and then provides a more detailed error message, # linking to sources on the Internet where the problem is # better-described. def self.mount_virtualbox_shared_folder(machine, name, guestpath, options) raise Vagrant::Errors::VirtualBoxMountNotSupportedBSD end end end end end ================================================ FILE: plugins/guests/bsd/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "shellwords" require "vagrant/util/retryable" module VagrantPlugins module GuestBSD module Cap class NFS extend Vagrant::Util::Retryable # Mount the given NFS folder. def self.mount_nfs_folder(machine, ip, folders) comm = machine.communicate # Mount each folder separately so we can retry. folders.each do |name, opts| # Shellescape the paths in case they do not have special characters. guest_path = Shellwords.escape(opts[:guestpath]) host_path = Shellwords.escape(opts[:hostpath]) # Build the list of mount options. mount_opts = [] mount_opts << "nfsv#{opts[:nfs_version]}" if opts[:nfs_version] mount_opts << "mntudp" if opts[:nfs_udp] if opts[:mount_options] mount_opts = mount_opts + opts[:mount_options].dup end mount_opts = mount_opts.join(",") # Make the directory on the guest. machine.communicate.sudo("mkdir -p #{guest_path}") # Perform the mount operation. command = "/sbin/mount -t nfs -o '#{mount_opts}' #{ip}:#{host_path} #{guest_path}" # Run the command, raising a specific error. retryable(on: Vagrant::Errors::NFSMountFailed, tries: 3, sleep: 5) do machine.communicate.sudo(command, error_class: Vagrant::Errors::NFSMountFailed, shell: "sh", ) end end end end end end end ================================================ FILE: plugins/guests/bsd/cap/public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "vagrant/util/shell_quote" module VagrantPlugins module GuestBSD module Cap class PublicKey def self.insert_public_key(machine, contents) comm = machine.communicate contents = contents.strip << "\n" remote_path = "/tmp/vagrant-insert-pubkey-#{Time.now.to_i}" Tempfile.open("vagrant-bsd-insert-public-key") do |f| f.binmode f.write(contents) f.fsync f.close comm.upload(f.path, remote_path) end # Use execute (not sudo) because we want to execute this as the SSH # user (which is "vagrant" by default). comm.execute <<-EOH.gsub(/^ {12}/, "") mkdir -p ~/.ssh chmod 0700 ~/.ssh && cat '#{remote_path}' >> ~/.ssh/authorized_keys && chmod 0600 ~/.ssh/authorized_keys result=$? rm -f '#{remote_path}' exit $result EOH end def self.remove_public_key(machine, contents) comm = machine.communicate contents = contents.strip << "\n" remote_path = "/tmp/vagrant-remove-pubkey-#{Time.now.to_i}" Tempfile.open("vagrant-bsd-remove-public-key") do |f| f.binmode f.write(contents) f.fsync f.close comm.upload(f.path, remote_path) end # Use execute (not sudo) because we want to execute this as the SSH # user (which is "vagrant" by default). comm.execute <<-EOH.sub(/^ {12}/, "") result=0 if test -f ~/.ssh/authorized_keys; then grep -v -x -f '#{remote_path}' ~/.ssh/authorized_keys > ~/.ssh/authorized_keys.tmp && mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys && chmod 0600 ~/.ssh/authorized_keys result=$? fi rm -f '#{remote_path}' exit $result EOH end end end end end ================================================ FILE: plugins/guests/bsd/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestBSD class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("uname -s | grep -i 'BSD'") end end end end ================================================ FILE: plugins/guests/bsd/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestBSD class Plugin < Vagrant.plugin("2") name "BSD-based guest" description "BSD-based guest support." guest(:bsd) do require_relative "guest" Guest end guest_capability(:bsd, :create_tmp_path) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:bsd, :decompress_tgz) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:bsd, :decompress_zip) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:bsd, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:bsd, :insert_public_key) do require_relative "cap/public_key" Cap::PublicKey end guest_capability(:bsd, :mount_nfs_folder) do require_relative "cap/nfs" Cap::NFS end guest_capability(:bsd, :mount_virtualbox_shared_folder) do require_relative "cap/mount_virtualbox_shared_folder" Cap::MountVirtualBoxSharedFolder end guest_capability(:bsd, :remove_public_key) do require_relative "cap/public_key" Cap::PublicKey end end end end ================================================ FILE: plugins/guests/centos/cap/flavor.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestCentos module Cap class Flavor def self.flavor(machine) # Pick up version info from `/etc/os-release`. This file started to exist # in CentOS 7. For versions before that (i.e. CentOS 6) just plain `:centos` # should do. version = nil if machine.communicate.test("test -f /etc/os-release") begin machine.communicate.execute("source /etc/os-release && printf $VERSION_ID") do |type, data| if type == :stdout version = data.split(".").first.to_i end end rescue end end if version.nil? || version < 1 return :centos else return "centos_#{version}".to_sym end end end end end end ================================================ FILE: plugins/guests/centos/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" require_relative '../linux/guest' module VagrantPlugins module GuestCentos class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "centos".freeze end end end ================================================ FILE: plugins/guests/centos/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestCentos class Plugin < Vagrant.plugin("2") name "CentOS guest" description "CentOS guest support." guest(:centos, :redhat) do require_relative "guest" Guest end guest_capability(:centos, :flavor) do require_relative "cap/flavor" Cap::Flavor end end end end ================================================ FILE: plugins/guests/coreos/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "yaml" module VagrantPlugins module GuestCoreOS module Cap class ChangeHostName extend Vagrant::Util::GuestInspection::Linux def self.change_host_name(machine, name) comm = machine.communicate if systemd_unit_file?(comm, "system-cloudinit*") file = Tempfile.new("vagrant-coreos-hostname") file.puts("#cloud-config\n") file.puts({"hostname" => name}.to_yaml) file.close dst = "/var/tmp/hostname.yml" svc_path = dst.tr("/", "-")[1..-1] comm.upload(file.path, dst) comm.sudo("systemctl start system-cloudinit@#{svc_path}.service") else if !comm.test("hostname -f | grep '^#{name}$'", sudo: false) basename = name.split(".", 2)[0] comm.sudo("hostname '#{basename}'") # Note that when working with CoreOS, we explicitly do not add the # entry to /etc/hosts because this file does not exist on CoreOS. # We could create it, but the recommended approach on CoreOS is to # use Fleet to manage /etc/hosts files. end end end end end end end ================================================ FILE: plugins/guests/coreos/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "yaml" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestCoreOS module Cap class ConfigureNetworks extend Vagrant::Util::GuestInspection::Linux NETWORK_MANAGER_CONN_DIR = "/etc/NetworkManager/system-connections".freeze DEFAULT_ENVIRONMENT_IP = "127.0.0.1".freeze def self.configure_networks(machine, networks) comm = machine.communicate return configure_networks_cloud_init(machine, networks) if comm.test("command -v cloud-init") interfaces = machine.guest.capability(:network_interfaces) nm_dev = {} comm.execute("nmcli -t c show") do |type, data| if type == :stdout _, id, _, dev = data.strip.split(":") nm_dev[dev] = id end end comm.sudo("rm #{File.join(NETWORK_MANAGER_CONN_DIR, 'vagrant-*.conf')}", error_check: false) networks.each_with_index do |network, i| network[:device] = interfaces[network[:interface]] addr = IPAddr.new(network[:ip]) mask = addr.mask(network[:netmask]) if !network[:mac_address] comm.execute("cat /sys/class/net/#{network[:device]}/address") do |type, data| if type == :stdout network[:mac_address] = data end end end f = Tempfile.new("vagrant-coreos-network") { connection: { type: "ethernet", id: network[:device], "interface-name": network[:device] }, ethernet: { "mac-address": network[:mac_address] }, ipv4: { method: "manual", addresses: "#{network[:ip]}/#{mask.prefix}", gateway: network.fetch(:gateway, mask.to_range.first.succ), }, }.each_pair do |section, content| f.puts "[#{section}]" content.each_pair do |key, value| f.puts "#{key}=#{value}" end end f.close comm.sudo("nmcli d disconnect '#{network[:device]}'", error_check: false) comm.sudo("nmcli c delete '#{nm_dev[network[:device]]}'", error_check: false) dst = File.join("/var/tmp", "vagrant-#{network[:device]}.conf") final = File.join(NETWORK_MANAGER_CONN_DIR, "vagrant-#{network[:device]}.conf") comm.upload(f.path, dst) comm.sudo("chown root:root '#{dst}'") comm.sudo("chmod 0600 '#{dst}'") comm.sudo("mv '#{dst}' '#{final}'") comm.sudo("nmcli c load '#{final}'") comm.sudo("nmcli d connect '#{network[:device]}'") f.delete end end def self.configure_networks_cloud_init(machine, networks) cloud_config = {} # Locate configured IP addresses to drop in /etc/environment # for export. If no addresses found, fall back to default public_ip = catch(:public_ip) do machine.config.vm.networks.each do |type, opts| next if type != :public_network throw(:public_ip, opts[:ip]) if opts[:ip] end DEFAULT_ENVIRONMENT_IP end private_ip = catch(:private_ip) do machine.config.vm.networks.each do |type, opts| next if type != :private_network throw(:private_ip, opts[:ip]) if opts[:ip] end public_ip end cloud_config["write_files"] = [ {"path" => "/etc/environment", "content" => "COREOS_PUBLIC_IPV4=#{public_ip}\nCOREOS_PRIVATE_IPV4=#{private_ip}"} ] # Generate configuration for any static network interfaces # which have been defined interfaces = machine.guest.capability(:network_interfaces) units = networks.map do |network| iface = network[:interface].to_i unit_name = "50-vagrant#{iface}.network" device = interfaces[iface] if network[:type].to_s == "dhcp" network_content = "DHCP=yes" else prefix = IPAddr.new("255.255.255.255/#{network[:netmask]}").to_i.to_s(2).count("1") address = "#{network[:ip]}/#{prefix}" network_content = "Address=#{address}" end {"name" => unit_name, "runtime" => "no", "content" => "[Match]\nName=#{device}\n[Network]\n#{network_content}"} end cloud_config["coreos"] = {"units" => units.compact} # Upload configuration and apply file = Tempfile.new("vagrant-coreos-networks") file.puts("#cloud-config\n") file.puts(cloud_config.to_yaml) file.close dst = "/var/tmp/networks.yml" svc_path = dst.tr("/", "-")[1..-1] machine.communicate.upload(file.path, dst) machine.communicate.sudo("systemctl start system-cloudinit@#{svc_path}.service") end end end end end ================================================ FILE: plugins/guests/coreos/cap/docker.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestCoreOS module Cap module Docker def self.docker_daemon_running(machine) machine.communicate.test("test -S /run/docker.sock") end end end end end ================================================ FILE: plugins/guests/coreos/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestCoreOS class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("(cat /etc/os-release | grep ID=coreos) || (cat /etc/os-release | grep -E 'ID_LIKE=.*coreos.*')") end end end end ================================================ FILE: plugins/guests/coreos/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestCoreOS class Plugin < Vagrant.plugin("2") name "CoreOS guest" description "CoreOS guest support." guest(:coreos, :linux) do require_relative "guest" Guest end guest_capability(:coreos, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:coreos, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:coreos, :docker_daemon_running) do require_relative "cap/docker" Cap::Docker end end end end ================================================ FILE: plugins/guests/darwin/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/guest_hosts' module VagrantPlugins module GuestDarwin module Cap class ChangeHostName extend Vagrant::Util::GuestHosts::BSD def self.change_host_name(machine, name) comm = machine.communicate if !comm.test("hostname -f | grep '^#{name}$'", sudo: false) basename = name.split(".", 2)[0] # LocalHostName should not contain dots - it is used by Bonjour and # visible through file sharing services. comm.sudo <<-EOH.gsub(/^ */, '') # Set hostname scutil --set ComputerName '#{name}' && scutil --set HostName '#{name}' && scutil --set LocalHostName '#{basename}' result=$? if [ $result -ne 0 ]; then exit $result fi hostname '#{name}' EOH end network_with_hostname = machine.config.vm.networks.map {|_, c| c if c[:hostname] }.compact[0] if network_with_hostname replace_host(comm, name, network_with_hostname[:ip]) else add_hostname_to_loopback_interface(comm, name) end end end end end end ================================================ FILE: plugins/guests/darwin/cap/choose_addressable_ip_addr.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestDarwin module Cap module ChooseAddressableIPAddr def self.choose_addressable_ip_addr(machine, possible) comm = machine.communicate possible.each do |ip| if comm.test("ping -c1 -t1 #{ip}") return ip end end # If we got this far, there are no addressable IPs return nil end end end end end ================================================ FILE: plugins/guests/darwin/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "vagrant/util/template_renderer" 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) if !machine.provider.capability?(:nic_mac_addresses) raise Vagrant::Errors::CantReadMACAddresses, provider: machine.provider_name.to_s end nic_mac_addresses = machine.provider.capability(:nic_mac_addresses) @@logger.debug("mac addresses: #{nic_mac_addresses.inspect}") 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 case network_type.to_sym when :static command = "networksetup -setmanual \"#{service_name}\" #{network[:ip]} #{network[:netmask]} #{network[:router]}" when :static6 command = "networksetup -setv6manual \"#{service_name}\" #{network[:ip]} #{network[:netmask]} #{network[:router]}" when :dhcp command = "networksetup -setdhcp \"#{service_name}\"" when :dhcp6 # This is not actually possible yet in Vagrant, but when we do # enable IPv6 across the board, Darwin will already have support. command = "networksetup -setv6automatic \"#{service_name}\"" else raise Vagrant::Errors::NetworkTypeNotSupported, type: network_type 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/) && 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) service = devicearry[1] interface = devicearry[2] # Should map interface to service { "en1" => "Thunderbolt Ethernet" } interface_map[interface] = service end end File.delete(tmp_ints) 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 end File.delete(tmp_hw) mac_service_map end end end end end ================================================ FILE: plugins/guests/darwin/cap/darwin_version.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestDarwin module Cap class DarwinVersion VERSION_REGEX = /\d+.\d+.?\d*/.freeze # Get the darwin version # # @param [Machine] # @return [String] version of drawin def self.darwin_version(machine) output = "" machine.communicate.sudo("sysctl kern.osrelease") do |_, data| output = data end output.scan(VERSION_REGEX).first end # Get the darwin major version # # @param [Machine] # @return [int] major version of drawin (nil if version is not detected) def self.darwin_major_version(machine) output = "" machine.communicate.sudo("sysctl kern.osrelease") do |_, data| output = data end version_string = output.scan(VERSION_REGEX).first if version_string major_version = version_string.split(".").first.to_i else major_version = nil end major_version end end end end end ================================================ FILE: plugins/guests/darwin/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestDarwin module Cap class Halt def self.halt(machine) begin # Darwin does not support the `-p` option like the rest of the # BSD-based guests, so it needs its own cap. machine.communicate.sudo("/sbin/shutdown -h now") rescue IOError, Vagrant::Errors::SSHDisconnected # Do nothing because SSH connection closed and it probably # means the VM just shut down really fast. end end end end end end ================================================ FILE: plugins/guests/darwin/cap/mount_smb_shared_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/retryable" require "shellwords" module VagrantPlugins module GuestDarwin module Cap class MountSMBSharedFolder extend Vagrant::Util::Retryable def self.mount_smb_shared_folder(machine, name, guestpath, options) expanded_guest_path = machine.guest.capability(:shell_expand_guest_path, guestpath) mount_point_owner = options[:owner]; if mount_point_owner machine.communicate.sudo("mkdir -p #{expanded_guest_path}") machine.communicate.sudo("chown #{mount_point_owner} #{expanded_guest_path}") else # fallback to assumption that user has permission # to create the specified mountpoint machine.communicate.execute("mkdir -p #{expanded_guest_path}") end smb_password = Shellwords.shellescape(options[:smb_password]) # Ensure password is scrubbed Vagrant::Util::CredentialScrubber.sensitive(smb_password) mount_options = options[:mount_options]; mount_command = "mount -t smbfs " + (mount_options ? "-o '#{mount_options.join(",")}' " : "") + "//#{options[:smb_username]}:#{smb_password}@#{options[:smb_host]}/#{name} " + "#{expanded_guest_path}" retryable(on: Vagrant::Errors::DarwinMountFailed, tries: 10, sleep: 2) do result = machine.communicate.execute(mount_command) if result.exit_code != 0 raise Vagrant::Errors::DarwinMountFailed, command: mount_command, output: result.stderr end end end end end end end ================================================ FILE: plugins/guests/darwin/cap/mount_vmware_shared_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "securerandom" module VagrantPlugins module GuestDarwin module Cap class MountVmwareSharedFolder MACOS_BIGSUR_DARWIN_VERSION = 20.freeze # Entry point for hook to called delayed actions # which finalizing the synced folders setup on # the guest def self.write_apfs_firmlinks(env) if env && env[:machine] && delayed = apfs_firmlinks_delayed.delete(env[:machine].id) delayed.call end end # @return [Hash] storage location for delayed actions def self.apfs_firmlinks_delayed if !@_apfs_firmlinks @_apfs_firmlinks = {} end @_apfs_firmlinks end # we seem to be unable to ask 'mount -t vmhgfs' to mount the roots # of specific shares, so instead we symlink from what is already # mounted by the guest tools # (ie. the behaviour of the VMware_fusion provider prior to 0.8.x) def self.mount_vmware_shared_folder(machine, name, guestpath, options) # Use this variable to determine which machines # have been registered with after hook @apply_firmlinks ||= Hash.new{ |h, k| h[k] = {bootstrap: false, content: []} } machine.communicate.tap do |comm| # check if we are dealing with an APFS root container if comm.test("test -d /System/Volumes/Data") parts = Pathname.new(guestpath).descend.to_a firmlink = parts[1].to_s firmlink.slice!(0, 1) if firmlink.start_with?("/") if parts.size > 2 guestpath = File.join("/System/Volumes/Data", guestpath) else guestpath = nil end end # Remove existing symlink or directory if defined if guestpath if comm.test("test -L \"#{guestpath}\"") comm.sudo("rm -f \"#{guestpath}\"") elsif comm.test("test -d \"#{guestpath}\"") comm.sudo("rm -Rf \"#{guestpath}\"") end # create intermediate directories if needed intermediate_dir = File.dirname(guestpath) if intermediate_dir != "/" comm.sudo("mkdir -p \"#{intermediate_dir}\"") end comm.sudo("ln -s \"/Volumes/VMware Shared Folders/#{name}\" \"#{guestpath}\"") end if firmlink && !system_firmlink?(firmlink) if guestpath.nil? guestpath = "/Volumes/VMware Shared Folders/#{name}" else guestpath = File.join("/System/Volumes/Data", firmlink) end share_line = "#{firmlink}\t#{guestpath}" # Check if the line is already defined. If so, bail since we are done if !comm.test("[[ \"$( /etc/synthetic.conf") if @apply_firmlinks[machine.id][:bootstrap] if machine.guest.capability("darwin_major_version").to_i < MACOS_BIGSUR_DARWIN_VERSION apfs_bootstrap_flag = "-B" expected_rc = 0 else apfs_bootstrap_flag = "-t" expected_rc = 253 end # Re-bootstrap the root container to pick up firmlink updates comm.sudo("/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util #{apfs_bootstrap_flag}", good_exit: [expected_rc]) end end end @apply_firmlinks[machine.id][:content] << share_line end end end # Check if firmlink is provided by the system # # @param [String] firmlink Firmlink path # @return [Boolean] def self.system_firmlink?(firmlink) if !@_firmlinks if File.exist?("/usr/share/firmlinks") @_firmlinks = File.readlines("/usr/share/firmlinks").map do |line| line.split.first end else @_firmlinks = [] end end firmlink = "/#{firmlink}" if !firmlink.start_with?("/") @_firmlinks.include?(firmlink) end # @private # Reset the cached values for capability. This is not considered a public # API and should only be used for testing. def self.reset! instance_variables.each(&method(:remove_instance_variable)) end end end end end ================================================ FILE: plugins/guests/darwin/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/rsync/default_unix_cap" module VagrantPlugins module GuestDarwin module Cap class RSync extend VagrantPlugins::SyncedFolderRSync::DefaultUnixCap end end end end ================================================ FILE: plugins/guests/darwin/cap/shell_expand_guest_path.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestDarwin module Cap class ShellExpandGuestPath def self.shell_expand_guest_path(machine, path) real_path = nil path = path.gsub(/ /, '\ ') machine.communicate.execute("printf #{path}") do |type, data| if type == :stdout real_path ||= "" real_path += data end end if !real_path # If no real guest path was detected, this is really strange # and we raise an exception because this is a bug. raise Vagrant::Errors::ShellExpandFailed end # Chomp the string so that any trailing newlines are killed return real_path.chomp end end end end end ================================================ FILE: plugins/guests/darwin/cap/verify_vmware_hgfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestDarwin module Cap class VerifyVmwareHgfs def self.verify_vmware_hgfs(machine) kext_bundle_id = "com.vmware.kext.vmhgfs" machine.communicate.test("kextstat -b #{kext_bundle_id} -l | grep #{kext_bundle_id}") end end end end end ================================================ FILE: plugins/guests/darwin/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestDarwin # A general Vagrant system implementation for OS X (ie. "Darwin"). # # Contributed by: - Brian Johnson # - Tim Sutton class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("uname -s | grep 'Darwin'") end end end end ================================================ FILE: plugins/guests/darwin/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestDarwin class Plugin < Vagrant.plugin("2") name "Darwin guest" description "Darwin guest support." action_hook(:apfs_firmlinks, :synced_folders) do |hook| require_relative "cap/mount_vmware_shared_folder" hook.prepend(Vagrant::Action::Builtin::Delayed, Cap::MountVmwareSharedFolder.method(:write_apfs_firmlinks)) end guest(:darwin, :bsd) do require_relative "guest" Guest end guest_capability(:darwin, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:darwin, :choose_addressable_ip_addr) do require_relative "cap/choose_addressable_ip_addr" Cap::ChooseAddressableIPAddr end guest_capability(:darwin, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:darwin, :darwin_version) do require_relative "cap/darwin_version" Cap::DarwinVersion end guest_capability(:darwin, :darwin_major_version) do require_relative "cap/darwin_version" Cap::DarwinVersion end guest_capability(:darwin, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:darwin, :mount_smb_shared_folder) do require_relative "cap/mount_smb_shared_folder" Cap::MountSMBSharedFolder end guest_capability(:darwin, :mount_vmware_shared_folder) do require_relative "cap/mount_vmware_shared_folder" Cap::MountVmwareSharedFolder end guest_capability(:darwin, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end guest_capability(:darwin, :rsync_command) do require_relative "cap/rsync" Cap::RSync end guest_capability(:darwin, :rsync_post) do require_relative "cap/rsync" Cap::RSync end guest_capability(:darwin, :rsync_pre) do require_relative "cap/rsync" Cap::RSync end guest_capability(:darwin, :shell_expand_guest_path) do require_relative "cap/shell_expand_guest_path" Cap::ShellExpandGuestPath end guest_capability(:darwin, :verify_vmware_hgfs) do require_relative "cap/verify_vmware_hgfs" Cap::VerifyVmwareHgfs end end end end ================================================ FILE: plugins/guests/debian/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require 'vagrant/util/guest_hosts' require 'vagrant/util/guest_inspection' require_relative "../../linux/cap/network_interfaces" module VagrantPlugins module GuestDebian module Cap class ChangeHostName extend Vagrant::Util::GuestInspection::Linux extend Vagrant::Util::GuestHosts::Linux def self.change_host_name(machine, name) @logger = Log4r::Logger.new("vagrant::guest::debian::changehostname") comm = machine.communicate if !comm.test("hostname -f | grep '^#{name}$'", sudo: false) network_with_hostname = machine.config.vm.networks.map {|_, c| c if c[:hostname] }.compact[0] if network_with_hostname replace_host(comm, name, network_with_hostname[:ip]) else add_hostname_to_loopback_interface(comm, name) end basename = name.split(".", 2)[0] comm.sudo <<-EOH.gsub(/^ {14}/, '') # Set the hostname echo '#{basename}' > /etc/hostname # Update mailname echo '#{name}' > /etc/mailname EOH if hostnamectl?(comm) comm.sudo("hostnamectl set-hostname '#{basename}'") else comm.sudo <<-EOH.gsub(/^ {14}/, '') hostname -F /etc/hostname # Restart hostname services if test -f /etc/init.d/hostname; then /etc/init.d/hostname start || true fi if test -f /etc/init.d/hostname.sh; then /etc/init.d/hostname.sh start || true fi EOH end restart_command = nil if systemd?(comm) if systemd_networkd?(comm) @logger.debug("Attempting to restart networking with systemd-networkd") restart_command = "systemctl restart systemd-networkd.service" elsif systemd_controlled?(comm, "NetworkManager.service") @logger.debug("Attempting to restart networking with NetworkManager") restart_command = "systemctl restart NetworkManager.service" end end if restart_command comm.sudo(restart_command) else restart_each_interface(machine, @logger) end end end protected # Due to how most Debian systems and older Ubuntu systems handle restarting # networking, we cannot simply run the networking init script or use the ifup/down # tools to restart all interfaces to renew the machines DHCP lease when setting # its hostname. This method is a workaround for those older systems that # cannoy reliably restart networking. It restarts each individual interface # on its own instead. # # @param [Vagrant::Machine] machine # @param [Log4r::Logger] logger def self.restart_each_interface(machine, logger) comm = machine.communicate interfaces = VagrantPlugins::GuestLinux::Cap::NetworkInterfaces.network_interfaces(machine) nettools = true if systemd?(comm) logger.debug("Attempting to restart networking with systemctl") nettools = false else logger.debug("Attempting to restart networking with ifup/down nettools") end interfaces.each do |iface| logger.debug("Restarting interface #{iface} on guest #{machine.name}") if nettools restart_command = "ifdown #{iface};ifup #{iface}" else restart_command = "systemctl stop ifup@#{iface}.service;systemctl start ifup@#{iface}.service" end comm.sudo(restart_command) end end end end end end ================================================ FILE: plugins/guests/debian/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestDebian module Cap class ConfigureNetworks include Vagrant::Util extend Vagrant::Util::GuestInspection::Linux extend Vagrant::Util::Retryable NETPLAN_DEFAULT_VERSION = 2 NETPLAN_DIRECTORY = "/etc/netplan".freeze NETWORKD_DIRECTORY = "/etc/systemd/network".freeze def self.configure_networks(machine, networks) comm = machine.communicate interfaces = machine.guest.capability(:network_interfaces) if systemd?(comm) if netplan?(comm) configure_netplan(machine, interfaces, comm, networks) elsif systemd_networkd?(comm) configure_networkd(machine, interfaces, comm, networks) else configure_nettools(machine, interfaces, comm, networks) end else configure_nettools(machine, interfaces, comm, networks) end end # Configure networking using netplan def self.configure_netplan(machine, interfaces, comm, networks) ethernets = {}.tap do |e_nets| networks.each do |network| e_config = {}.tap do |entry| if network[:type].to_s == "dhcp" entry["dhcp4"] = true else mask = network[:netmask] if mask && IPAddr.new(network[:ip]).ipv4? begin mask = IPAddr.new(mask).to_i.to_s(2).count("1") rescue IPAddr::Error # ignore and use given value end end entry["addresses"] = [[network[:ip], mask].compact.join("/")] end if network[:gateway] entry["gateway4"] = network[:gateway] end end e_nets[interfaces[network[:interface]]] = e_config end end # By default, netplan expects the renderer to be systemd-networkd, # but if any device is managed by NetworkManager, then we use that renderer # ref: https://netplan.io/reference if systemd_networkd?(comm) renderer = "networkd" ethernets.keys.each do |k| if nm_controlled?(comm, k) renderer = "NetworkManager" if !nmcli?(comm) raise Vagrant::Errors::NetworkManagerNotInstalled, device: k end break end end elsif nmcli?(comm) renderer = "NetworkManager" else raise Vagrant::Errors::NetplanNoAvailableRenderers end np_config = {"network" => {"version" => NETPLAN_DEFAULT_VERSION, "renderer" => renderer, "ethernets" => ethernets}} remote_path = upload_tmp_file(comm, np_config.to_yaml) dest_path = "#{NETPLAN_DIRECTORY}/50-vagrant.yaml" comm.sudo(["mv -f '#{remote_path}' '#{dest_path}'", "chown root:root '#{dest_path}'", "chmod 0644 '#{dest_path}'", "netplan apply"].join("\n")) end # Configure guest networking using networkd def self.configure_networkd(machine, interfaces, comm, networks) networks.each do |network| dev_name = interfaces[network[:interface]] net_conf = [] net_conf << "[Match]" net_conf << "Name=#{dev_name}" net_conf << "[Network]" if network[:type].to_s == "dhcp" net_conf << "DHCP=yes" else mask = network[:netmask] if mask && IPAddr.new(network[:ip]).ipv4? begin mask = IPAddr.new(mask).to_i.to_s(2).count("1") rescue IPAddr::Error # ignore and use given value end end address = [network[:ip], mask].compact.join("/") net_conf << "DHCP=no" net_conf << "Address=#{address}" net_conf << "Gateway=#{network[:gateway]}" if network[:gateway] end remote_path = upload_tmp_file(comm, net_conf.join("\n")) dest_path = "#{NETWORKD_DIRECTORY}/50-vagrant-#{dev_name}.network" comm.sudo(["mkdir -p #{NETWORKD_DIRECTORY}", "mv -f '#{remote_path}' '#{dest_path}'", "chown root:root '#{dest_path}'", "chmod 0644 '#{dest_path}'"].join("\n")) end comm.sudo(["systemctl restart systemd-networkd.service"].join("\n")) end # Configure guest networking using net-tools def self.configure_nettools(machine, interfaces, comm, networks) commands = [] entries = [] root_device = interfaces.first networks.each do |network| network[:device] = interfaces[network[:interface]] entry = TemplateRenderer.render("guests/debian/network_#{network[:type]}", options: network.merge(:root_device => root_device), ) entries << entry end content = entries.join("\n") remote_path = "/tmp/vagrant-network-entry" upload_tmp_file(comm, content, remote_path) networks.each do |network| # Ubuntu 16.04+ returns an error when downing an interface that # does not exist. The `|| true` preserves the behavior that older # Ubuntu versions exhibit and Vagrant expects (GH-7155) commands << "/sbin/ifdown '#{network[:device]}' || true" commands << "/sbin/ip addr flush dev '#{network[:device]}'" end # Reconfigure /etc/network/interfaces. commands << <<-EOH.gsub(/^ {12}/, "") # Remove any previous network modifications from the interfaces file sed -e '/^#VAGRANT-BEGIN/,$ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces.pre sed -ne '/^#VAGRANT-END/,$ p' /etc/network/interfaces | tac | sed -e '/^#VAGRANT-END/,$ d' | tac > /tmp/vagrant-network-interfaces.post cat \\ /tmp/vagrant-network-interfaces.pre \\ /tmp/vagrant-network-entry \\ /tmp/vagrant-network-interfaces.post \\ > /etc/network/interfaces rm -f /tmp/vagrant-network-interfaces.pre rm -f /tmp/vagrant-network-entry rm -f /tmp/vagrant-network-interfaces.post EOH comm.sudo(commands.join("\n")) network_up_commands = [] # Bring back up each network interface, reconfigured. networks.each do |network| network_up_commands << "/sbin/ifup '#{network[:device]}'" end retryable(on: Vagrant::Errors::VagrantError, sleep: 2, tries: 2) do comm.sudo(network_up_commands.join("\n")) end end # Simple helper to upload content to guest temporary file # # @param [Vagrant::Plugin::Communicator] comm # @param [String] content # @return [String] remote path def self.upload_tmp_file(comm, content, remote_path=nil) if remote_path.nil? remote_path = "/tmp/vagrant-network-entry-#{Time.now.to_i}" end Tempfile.open("vagrant-debian-configure-networks") do |f| f.binmode f.write(content) f.fsync f.close comm.upload(f.path, remote_path) end remote_path end end end end end ================================================ FILE: plugins/guests/debian/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestDebian module Cap class NFS def self.nfs_client_install(machine) comm = machine.communicate comm.sudo <<-EOH.gsub(/^ {12}/, '') apt-get -yqq update DEBIAN_FRONTEND=noninteractive apt-get -yqq install nfs-common portmap exit $? EOH end end end end end ================================================ FILE: plugins/guests/debian/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestDebian module Cap class RSync def self.rsync_install(machine) comm = machine.communicate comm.sudo <<-EOH.gsub(/^ {14}/, '') apt-get -yqq update apt-get -yqq install rsync EOH end end end end end ================================================ FILE: plugins/guests/debian/cap/smb.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestDebian module Cap class SMB def self.smb_install(machine) comm = machine.communicate if !comm.test("test -f /sbin/mount.cifs") comm.sudo <<-EOH.gsub(/^ {14}/, '') apt-get -yqq update apt-get -yqq install cifs-utils EOH end end end end end end ================================================ FILE: plugins/guests/debian/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../linux/guest' module VagrantPlugins module GuestDebian class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "debian".freeze end end end ================================================ FILE: plugins/guests/debian/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestDebian class Plugin < Vagrant.plugin("2") name "Debian guest" description "Debian guest support." guest(:debian, :linux) do require_relative "guest" Guest end guest_capability(:debian, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:debian, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:debian, :nfs_client_install) do require_relative "cap/nfs" Cap::NFS end guest_capability(:debian, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:debian, :smb_install) do require_relative "cap/smb" Cap::SMB end end end end ================================================ FILE: plugins/guests/dragonflybsd/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestDragonFlyBSD class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("uname -s | grep -i 'DragonFly'") end end end end ================================================ FILE: plugins/guests/dragonflybsd/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestDragonFlyBSD class Plugin < Vagrant.plugin("2") name "DragonFly BSD guest" description "DragonFly BSD guest support." guest(:dragonflybsd, :freebsd) do require_relative "guest" Guest end end end end ================================================ FILE: plugins/guests/elementary/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../linux/guest' module VagrantPlugins module GuestElementary class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "elementary".freeze end end end ================================================ FILE: plugins/guests/elementary/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestElementary class Plugin < Vagrant.plugin("2") name "Elementary guest" description "Elementary guest support." guest(:elementary, :ubuntu) do require_relative "guest" Guest end end end end ================================================ FILE: plugins/guests/esxi/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestEsxi module Cap class ChangeHostName def self.change_host_name(machine, name) if !machine.communicate.test("localcli system hostname get | grep '#{name}'") machine.communicate.execute("localcli system hostname set -H '#{name}'") end end end end end end ================================================ FILE: plugins/guests/esxi/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestEsxi module Cap class ConfigureNetworks def self.configure_networks(machine, networks) networks.each do |network| ix = network[:interface] switch = "vSwitch#{ix}" pg = "VagrantNetwork#{ix}" vmnic = "vmnic#{ix}" device = "vmk#{ix}" if machine.communicate.test("localcli network ip interface ipv4 get -i #{device}") machine.communicate.execute("localcli network ip interface remove -i #{device}") end if machine.communicate.test("localcli network vswitch standard list -v #{switch}") machine.communicate.execute("localcli network vswitch standard remove -v #{switch}") end machine.communicate.execute("localcli network vswitch standard add -v #{switch}") machine.communicate.execute("localcli network vswitch standard uplink add -v #{switch} -u #{vmnic}") machine.communicate.execute("localcli network vswitch standard portgroup add -v #{switch} -p #{pg}") machine.communicate.execute("localcli network ip interface add -i #{device} -p #{pg}") ifconfig = "localcli network ip interface ipv4 set -i #{device}" if network[:type].to_sym == :static machine.communicate.execute("#{ifconfig} -t static --ipv4 #{network[:ip]} --netmask #{network[:netmask]}") elsif network[:type].to_sym == :dhcp machine.communicate.execute("#{ifconfig} -t dhcp") end end end end end end end ================================================ FILE: plugins/guests/esxi/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestEsxi module Cap class Halt def self.halt(machine) begin machine.communicate.execute("/bin/halt -d 0") rescue IOError, Vagrant::Errors::SSHDisconnected # Ignore, this probably means connection closed because it # shut down. end end end end end end ================================================ FILE: plugins/guests/esxi/cap/mount_nfs_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestEsxi module Cap class MountNFSFolder extend Vagrant::Util::Retryable def self.mount_nfs_folder(machine, ip, folders) folders.each do |name, opts| guestpath = opts[:guestpath] volume = guestpath.gsub("/", "_") machine.communicate.tap do |comm| if comm.test("localcli storage nfs list | grep '^#{volume}'") comm.execute("localcli storage nfs remove -v #{volume}") end mount_command = "localcli storage nfs add -H #{ip} -s '#{opts[:hostpath]}' -v '#{volume}'" retryable(on: Vagrant::Errors::NFSMountFailed, tries: 5, sleep: 2) do comm.execute(mount_command, error_class: Vagrant::Errors::NFSMountFailed) end # symlink vmfs volume to :guestpath if comm.test("test -L '#{guestpath}'") comm.execute("rm -f '#{guestpath}'") end if comm.test("test -d '#{guestpath}'") comm.execute("rmdir '#{guestpath}'") end dir = File.dirname(guestpath) if !comm.test("test -d '#{dir}'") comm.execute("mkdir -p '#{dir}'") end comm.execute("ln -s '/vmfs/volumes/#{volume}' '#{guestpath}'") end end end end end end end ================================================ FILE: plugins/guests/esxi/cap/public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "vagrant/util/shell_quote" module VagrantPlugins module GuestEsxi module Cap class PublicKey def self.insert_public_key(machine, contents) comm = machine.communicate contents = contents.strip << "\n" remote_path = "/tmp/vagrant-insert-pubkey-#{Time.now.to_i}" Tempfile.open("vagrant-esxi-insert-public-key") do |f| f.binmode f.write(contents) f.fsync f.close comm.upload(f.path, remote_path) end comm.execute <<-EOH.gsub(/^ {12}/, "") set -e SSH_DIR="$(grep -q '^AuthorizedKeysFile\s*\/etc\/ssh\/keys-%u\/authorized_keys$' /etc/ssh/sshd_config && echo -n /etc/ssh/keys-${USER} || echo -n ~/.ssh)" mkdir -p "${SSH_DIR}" # NB in ESXi 7.0 we cannot change the /etc/ssh/keys-root directory # permissions, so ignore any errors. chmod 0700 "${SSH_DIR}" || true cat '#{remote_path}' >> "${SSH_DIR}/authorized_keys" chmod 0600 "${SSH_DIR}/authorized_keys" rm -f '#{remote_path}' EOH end def self.remove_public_key(machine, contents) comm = machine.communicate contents = contents.strip << "\n" remote_path = "/tmp/vagrant-remove-pubkey-#{Time.now.to_i}" Tempfile.open("vagrant-esxi-remove-public-key") do |f| f.binmode f.write(contents) f.fsync f.close comm.upload(f.path, remote_path) end # Use execute (not sudo) because we want to execute this as the SSH # user (which is "vagrant" by default). comm.execute <<-EOH.sub(/^ {12}/, "") set -e SSH_DIR="$(grep -q '^AuthorizedKeysFile\s*\/etc\/ssh\/keys-%u\/authorized_keys$' /etc/ssh/sshd_config && echo -n /etc/ssh/keys-${USER} || echo -n ~/.ssh)" if test -f "${SSH_DIR}/authorized_keys"; then grep -v -x -f '#{remote_path}' "${SSH_DIR}/authorized_keys" > "${SSH_DIR}/authorized_keys.tmp" mv "${SSH_DIR}/authorized_keys.tmp" "${SSH_DIR}/authorized_keys" chmod 0600 "${SSH_DIR}/authorized_keys" fi rm -f '#{remote_path}' EOH end end end end end ================================================ FILE: plugins/guests/esxi/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestEsxi class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("uname -s | grep VMkernel") end end end end ================================================ FILE: plugins/guests/esxi/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestEsxi class Plugin < Vagrant.plugin("2") name "ESXi guest." description "ESXi guest support." guest(:esxi) do require_relative "guest" Guest end guest_capability(:esxi, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:esxi, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:esxi, :mount_nfs_folder) do require_relative "cap/mount_nfs_folder" Cap::MountNFSFolder end guest_capability(:esxi, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:esxi, :remove_public_key) do require_relative "cap/public_key" Cap::PublicKey end guest_capability(:esxi, :insert_public_key) do require_relative "cap/public_key" Cap::PublicKey end end end end ================================================ FILE: plugins/guests/fedora/cap/flavor.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestFedora module Cap class Flavor def self.flavor(machine) # Read the version file version = nil machine.communicate.sudo("grep VERSION_ID /etc/os-release") do |type, data| if type == :stdout version = data.split("=")[1].chomp.to_i end end if version.nil? return :fedora else return :"fedora_#{version}" end end end end end end ================================================ FILE: plugins/guests/fedora/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" require_relative '../linux/guest' module VagrantPlugins module GuestFedora class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "fedora".freeze end end end ================================================ FILE: plugins/guests/fedora/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestFedora class Plugin < Vagrant.plugin("2") name "Fedora guest" description "Fedora guest support." guest(:fedora, :redhat) do require_relative "guest" Guest end guest_capability(:fedora, :flavor) do require_relative "cap/flavor" Cap::Flavor end end end end ================================================ FILE: plugins/guests/freebsd/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/guest_hosts' module VagrantPlugins module GuestFreeBSD module Cap class ChangeHostName extend Vagrant::Util::GuestHosts::BSD def self.change_host_name(machine, name) comm = machine.communicate if !comm.test("hostname -f | grep '^#{name}$'", sudo: false, shell: "sh") basename = name.split(".", 2)[0] command = <<-EOH.gsub(/^ {14}/, '') # Set the hostname hostname '#{name}' sed -i '' 's/^hostname=.*$/hostname=\"#{name}\"/' /etc/rc.conf EOH comm.sudo(command, shell: "sh") end network_with_hostname = machine.config.vm.networks.map {|_, c| c if c[:hostname] }.compact[0] if network_with_hostname replace_host(comm, name, network_with_hostname[:ip]) else add_hostname_to_loopback_interface(comm, name) end end end end end end ================================================ FILE: plugins/guests/freebsd/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestFreeBSD module Cap class ConfigureNetworks include Vagrant::Util def self.configure_networks(machine, networks) options = { shell: "sh" } comm = machine.communicate commands = [] interfaces = [] # Remove any previous network additions to the configuration file. commands << "sed -i '' -e '/^#VAGRANT-BEGIN/,/^#VAGRANT-END/ d' /etc/rc.conf" comm.sudo("ifconfig -l ether", options) do |_, stdout| interfaces = stdout.split end networks.each.with_index do |network, i| network[:device] = interfaces[network[:interface]] entry = TemplateRenderer.render("guests/freebsd/network_#{network[:type]}", options: network, ) remote_path = "/tmp/vagrant-network-#{network[:device]}-#{Time.now.to_i}-#{i}" Tempfile.open("vagrant-freebsd-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close comm.upload(f.path, remote_path) end commands << <<-EOH.gsub(/^ {14}/, '') cat '#{remote_path}' >> /etc/rc.conf rm -f '#{remote_path}' EOH # If the network is DHCP, then we have to start the dhclient, unless # it is already running. See GH-5852 for more information if network[:type].to_sym == :dhcp file = "/var/run/dhclient.#{network[:device]}.pid" commands << <<-EOH.gsub(/^ {16}/, '') if ! test -f '#{file}' || ! kill -0 $(cat '#{file}'); then dhclient '#{network[:device]}' fi EOH end # For some reason, this returns status 1... every time commands << "/etc/rc.d/netif restart '#{network[:device]}' || true" end comm.sudo(commands.join("\n"), options) end end end end end ================================================ FILE: plugins/guests/freebsd/cap/mount_virtualbox_shared_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestFreeBSD module Cap class MountVirtualBoxSharedFolder extend SyncedFolder::UnixMountHelpers def self.mount_virtualbox_shared_folder(machine, name, guestpath, options) guest_path = Shellwords.escape(guestpath) @@logger.debug("Mounting #{name} (#{options[:hostpath]} to #{guestpath})") builtin_mount_type = "-cit vboxvfs" addon_mount_type = "-t vboxvfs" mount_options = options.fetch(:mount_options, []) detected_ids = detect_owner_group_ids(machine, guest_path, mount_options, options) mount_uid = detected_ids[:uid] mount_gid = detected_ids[:gid] mount_options << "uid=#{mount_uid}" mount_options << "gid=#{mount_gid}" mount_options = mount_options.join(',') mount_command = "mount #{addon_mount_type} -o #{mount_options} #{name} #{guest_path}" # Create the guest path if it doesn't exist machine.communicate.sudo("mkdir -p #{guest_path}") stderr = "" result = machine.communicate.sudo(mount_command, error_check: false) do |type, data| stderr << data if type == :stderr end if result != 0 if stderr.include?("-cit") @@logger.info("Detected builtin vboxvfs module, modifying mount command") mount_command.sub!(addon_mount_type, builtin_mount_type) end # Attempt to mount the folder. We retry here a few times because # it can fail early on. stderr = "" retryable(on: Vagrant::Errors::VirtualBoxMountFailed, tries: 3, sleep: 5) do machine.communicate.sudo(mount_command, error_class: Vagrant::Errors::VirtualBoxMountFailed, error_key: :virtualbox_mount_failed, command: mount_command, output: stderr, ) { |type, data| stderr = data if type == :stderr } end end # Chown the directory to the proper user. We skip this if the # mount options contained a readonly flag, because it won't work. if !options[:mount_options] || !options[:mount_options].include?("ro") chown_command = "chown #{mount_uid}:#{mount_gid} #{guest_path}" machine.communicate.sudo(chown_command) end emit_upstart_notification(machine, guest_path) end def self.unmount_virtualbox_shared_folder(machine, guestpath, options) guest_path = Shellwords.escape(guestpath) result = machine.communicate.sudo("umount #{guest_path}", error_check: false) if result == 0 machine.communicate.sudo("rmdir #{guest_path}", error_check: false) end end end end end end ================================================ FILE: plugins/guests/freebsd/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/rsync/default_unix_cap" module VagrantPlugins module GuestFreeBSD module Cap class RSync extend VagrantPlugins::SyncedFolderRSync::DefaultUnixCap def self.rsync_install(machine) machine.communicate.sudo("pkg install -y rsync") end end end end end ================================================ FILE: plugins/guests/freebsd/cap/shell_expand_guest_path.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestFreeBSD module Cap class ShellExpandGuestPath def self.shell_expand_guest_path(machine, path) real_path = nil path = path.gsub(/ /, '\ ') machine.communicate.execute("printf #{path}", shell: "sh") do |type, data| if type == :stdout real_path ||= "" real_path += data end end if !real_path # If no real guest path was detected, this is really strange # and we raise an exception because this is a bug. raise Vagrant::Errors::ShellExpandFailed end # Chomp the string so that any trailing newlines are killed return real_path.chomp end end end end end ================================================ FILE: plugins/guests/freebsd/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/template_renderer' module VagrantPlugins module GuestFreeBSD # A general Vagrant system implementation for "freebsd". # # Contributed by Kenneth Vestergaard class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("uname -s | grep 'FreeBSD'", {shell: "sh"}) end end end end ================================================ FILE: plugins/guests/freebsd/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestFreeBSD class Plugin < Vagrant.plugin("2") name "FreeBSD guest" description "FreeBSD guest support." guest(:freebsd, :bsd) do require_relative "guest" Guest end guest_capability(:freebsd, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:freebsd, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:freebsd, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:freebsd, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end guest_capability(:freebsd, :rsync_command) do require_relative "cap/rsync" Cap::RSync end guest_capability(:freebsd, :rsync_post) do require_relative "cap/rsync" Cap::RSync end guest_capability(:freebsd, :rsync_pre) do require_relative "cap/rsync" Cap::RSync end guest_capability(:freebsd, :shell_expand_guest_path) do require_relative "cap/shell_expand_guest_path" Cap::ShellExpandGuestPath end guest_capability(:freebsd, :mount_virtualbox_shared_folder) do require_relative "cap/mount_virtualbox_shared_folder" Cap::MountVirtualBoxSharedFolder end guest_capability(:freebsd, :unmount_virtualbox_shared_folder) do require_relative "cap/mount_virtualbox_shared_folder" Cap::MountVirtualBoxSharedFolder end end end end ================================================ FILE: plugins/guests/funtoo/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestFuntoo module Cap class ConfigureNetworks include Vagrant::Util def self.configure_networks(machine, networks) machine.communicate.tap do |comm| # Configure each network interface networks.each do |network| # http://www.funtoo.org/Funtoo_Linux_Networking # dhcpcd generally runs on all interfaces by default # in the future we can change this, dhcpcd has lots of features # it would be nice to expose more of its capabilities... if not /dhcp/i.match(network[:type]) line = "denyinterfaces eth#{network[:interface]}" cmd = "grep '#{line}' /etc/dhcpcd.conf; if [ $? -ne 0 ]; then echo '#{line}' >> /etc/dhcpcd.conf ; fi" comm.sudo(cmd) ifFile = "netif.eth#{network[:interface]}" entry = TemplateRenderer.render("guests/funtoo/network_#{network[:type]}", options: network) # Upload the entry to a temporary location Tempfile.open("vagrant-funtoo-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close comm.upload(f.path, "/tmp/vagrant-#{ifFile}") end comm.sudo("cp /tmp/vagrant-#{ifFile} /etc/conf.d/#{ifFile}") comm.sudo("chmod 0644 /etc/conf.d/#{ifFile}") comm.sudo("ln -fs /etc/init.d/netif.tmpl /etc/init.d/#{ifFile}") comm.sudo("/etc/init.d/#{ifFile} start") end end end end end end end end ================================================ FILE: plugins/guests/funtoo/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestFuntoo class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("grep Funtoo /etc/gentoo-release") end end end end ================================================ FILE: plugins/guests/funtoo/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestFuntoo class Plugin < Vagrant.plugin("2") name "Funtoo guest" description "Funtoo guest support." guest(:funtoo, :gentoo) do require_relative "guest" Guest end guest_capability(:funtoo, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end end end end ================================================ FILE: plugins/guests/gentoo/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../../linux/cap/change_host_name' module VagrantPlugins module GuestGentoo module Cap class ChangeHostName extend VagrantPlugins::GuestLinux::Cap::ChangeHostName def self.change_name_command(name) basename = name.split(".", 2)[0] return <<-EOH.gsub(/^ {14}/, '') # Use hostnamectl on systemd if [[ `systemctl` =~ -\.mount ]]; then systemctl set-hostname '#{name}' else hostname '#{basename}' echo "hostname=#{basename}" > /etc/conf.d/hostname fi EOH end end end end end ================================================ FILE: plugins/guests/gentoo/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "ipaddr" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestGentoo module Cap class ConfigureNetworks include Vagrant::Util def self.configure_networks(machine, networks) comm = machine.communicate commands = [] interfaces = machine.guest.capability(:network_interfaces, "/bin/ip") networks.map! { |n| n[:device] = interfaces[n[:interface]]; n } if comm.test('[[ `systemctl` =~ -\.mount ]]') # Configure networking for Systemd # convert netmasks to CIDR by converting to a binary string and counting the '1's networks.map! { |n| n[:netmask] = IPAddr.new(n[:netmask]).to_i.to_s(2).count("1"); n } # glob networks by device, so that we can write one file per device # (result is hash[devicename] = [net, net, net...]) networks = networks.map { |n| [n[:device], n] }.reduce({}) { |h, (k, v)| (h[k] ||= []) << v; h } # Write one .network file out for each device networks.each_pair do |device_name, device_networks| entry = TemplateRenderer.render('guests/gentoo/network_systemd', networks: device_networks) filename = "50_vagrant_#{device_name}.network" tmpfile = "/tmp/#{filename}" destfile = "/etc/systemd/network/#{filename}" Tempfile.open('vagrant-gentoo-configure-networks') do |f| f.binmode f.write(entry) f.fsync f.close comm.upload(f.path, tmpfile) end commands << "mv #{tmpfile} #{destfile} && chmod 644 #{destfile}" end # tell systemd to reload the networking config commands << 'systemctl daemon-reload && systemctl restart systemd-networkd.service' else # Configure networking for OpenRC # Remove any previous network additions to the configuration file. commands << "sed -i'' -e '/^#VAGRANT-BEGIN/,/^#VAGRANT-END/ d' /etc/conf.d/net" networks.each_with_index do |network, i| entry = TemplateRenderer.render("guests/gentoo/network_#{network[:type]}", options: network, ) remote_path = "/tmp/vagrant-network-#{network[:device]}-#{Time.now.to_i}-#{i}" Tempfile.open("vagrant-gentoo-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close comm.upload(f.path, remote_path) end commands << <<-EOH.gsub(/^ {14}/, '') ln -sf /etc/init.d/net.lo /etc/init.d/net.#{network[:device]} /etc/init.d/net.#{network[:device]} stop || true cat '#{remote_path}' >> /etc/conf.d/net rm -f '#{remote_path}' /etc/init.d/net.#{network[:device]} start EOH end end comm.sudo(commands.join("\n")) end end end end end ================================================ FILE: plugins/guests/gentoo/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestGentoo class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("grep Gentoo /etc/gentoo-release") end end end end ================================================ FILE: plugins/guests/gentoo/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestGentoo class Plugin < Vagrant.plugin("2") name "Gentoo guest" description "Gentoo guest support." guest(:gentoo, :linux) do require_relative "guest" Guest end guest_capability(:gentoo, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:gentoo, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end end end end ================================================ FILE: plugins/guests/haiku/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestHaiku module Cap class ChangeHostName def self.change_host_name(machine, name) comm = machine.communicate if !comm.test("hostname | grep '^#{name}$'", sudo: false) basename = name.split(".", 2)[0] comm.execute <<-EOH.gsub(/^ {14}/, '') # Ensure exit on command error set -e export SYS_SETTINGS_DIR=$(finddir B_SYSTEM_SETTINGS_DIRECTORY) # Set the hostname echo '#{basename}' > $SYS_SETTINGS_DIR/network/hostname hostname '#{basename}' # Remove comments and blank lines from /etc/hosts sed -i'' -e 's/#.*$//' -e '/^$/d' $SYS_SETTINGS_DIR/network/hosts # Prepend ourselves to $SYS_SETTINGS_DIR/network/hosts grep -w '#{name}' $SYS_SETTINGS_DIR/network/hosts || { sed -i'' '1i 127.0.0.1\\t#{name}\\t#{basename}' $SYS_SETTINGS_DIR/network/hosts } EOH end end end end end end ================================================ FILE: plugins/guests/haiku/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestHaiku module Cap class Halt def self.halt(machine) begin machine.communicate.execute("/bin/shutdown") rescue IOError, Vagrant::Errors::SSHDisconnected # Ignore, this probably means connection closed because it # shut down. end end end end end end ================================================ FILE: plugins/guests/haiku/cap/insert_public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/shell_quote" module VagrantPlugins module GuestHaiku module Cap class InsertPublicKey def self.insert_public_key(machine, contents) contents = contents.chomp contents = Vagrant::Util::ShellQuote.escape(contents, "'") machine.communicate.tap do |comm| comm.execute("mkdir -p $(finddir B_USER_SETTINGS_DIRECTORY)/ssh") comm.execute("chmod 0700 $(finddir B_USER_SETTINGS_DIRECTORY)/ssh") comm.execute("printf '#{contents}\\n' >> $(finddir B_USER_SETTINGS_DIRECTORY)/ssh/authorized_keys") comm.execute("chmod 0600 $(finddir B_USER_SETTINGS_DIRECTORY)/ssh/authorized_keys") end end end end end end ================================================ FILE: plugins/guests/haiku/cap/remove_public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/shell_quote" module VagrantPlugins module GuestHaiku module Cap class RemovePublicKey def self.remove_public_key(machine, contents) contents = contents.chomp contents = Vagrant::Util::ShellQuote.escape(contents, "'") machine.communicate.tap do |comm| if comm.test("test -f $(finddir B_USER_SETTINGS_DIRECTORY)/ssh/authorized_keys") comm.execute( "sed -i '/^.*#{contents}.*$/d' $(finddir B_USER_SETTINGS_DIRECTORY)/ssh/authorized_keys") end end end end end end end ================================================ FILE: plugins/guests/haiku/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestHaiku module Cap class RSync def self.rsync_installed(machine) machine.communicate.test("test -f /bin/rsync") end def self.rsync_install(machine) machine.communicate.execute("pkgman install -y rsync") end def self.rsync_command(machine) "rsync -zz" end end end end end ================================================ FILE: plugins/guests/haiku/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestHaiku class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("uname -o | grep 'Haiku'") end end end end ================================================ FILE: plugins/guests/haiku/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestHaiku class Plugin < Vagrant.plugin("2") name "Haiku guest" description "Haiku guest support." guest(:haiku) do require_relative "guest" Guest end guest_capability(:haiku, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:haiku, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:haiku, :insert_public_key) do require_relative "cap/insert_public_key" Cap::InsertPublicKey end guest_capability(:haiku, :remove_public_key) do require_relative "cap/remove_public_key" Cap::RemovePublicKey end guest_capability(:haiku, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:haiku, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end guest_capability(:haiku, :rsync_command) do require_relative "cap/rsync" Cap::RSync end end end end ================================================ FILE: plugins/guests/kali/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../linux/guest' module VagrantPlugins module GuestKali class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "kali".freeze end end end ================================================ FILE: plugins/guests/kali/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestKali class Plugin < Vagrant.plugin("2") name "Kali guest" description "Kali guest support." guest(:kali, :debian) do require_relative "guest" Guest end end end end ================================================ FILE: plugins/guests/linux/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/guest_hosts' module VagrantPlugins module GuestLinux module Cap module ChangeHostName module Methods def change_name_command(name) return <<-EOH.gsub(/^ {14}/, '') # Set the hostname echo '#{name}' > /etc/hostname hostname '#{name}' EOH end def change_host_name?(comm, name) !comm.test("hostname -f | grep '^#{name}$'", sudo: false) end def change_host_name(machine, name) comm = machine.communicate network_with_hostname = machine.config.vm.networks.map {|_, c| c if c[:hostname] }.compact[0] if network_with_hostname replace_host(comm, name, network_with_hostname[:ip]) else add_hostname_to_loopback_interface(comm, name) end if change_host_name?(comm, name) comm.sudo(change_name_command(name)) end end end def self.extended(klass) klass.extend(Vagrant::Util::GuestHosts::Linux) klass.extend(Methods) end extend Vagrant::Util::GuestHosts::Linux extend Methods end end end end ================================================ FILE: plugins/guests/linux/cap/choose_addressable_ip_addr.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestLinux module Cap module ChooseAddressableIPAddr def self.choose_addressable_ip_addr(machine, possible) comm = machine.communicate possible.each do |ip| if comm.test("ping -c1 -w1 -W1 #{ip}") return ip end end return nil end end end end end ================================================ FILE: plugins/guests/linux/cap/file_system.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestLinux module Cap class FileSystem # Create a temporary file or directory on the guest # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [Hash] opts Path options # @return [String] path to temporary file or directory def self.create_tmp_path(machine, opts) template = "vagrant-XXXXXX" if opts[:extension] template << opts[:extension].to_s end cmd = ["mktemp", "--tmpdir"] if opts[:type] == :directory cmd << "-d" end cmd << template tmp_path = "" machine.communicate.execute(cmd.join(" ")) do |type, data| if type == :stdout tmp_path << data end end tmp_path.strip end # Decompress tgz file on guest to given location # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [String] compressed_file Path to compressed file on guest # @param [String] destination Path for decompressed files on guest def self.decompress_tgz(machine, compressed_file, destination, opts={}) comm = machine.communicate extract_dir = create_tmp_path(machine, type: :directory) cmds = [] if opts[:type] == :directory cmds << "mkdir -p '#{destination}'" else cmds << "mkdir -p '#{File.dirname(destination)}'" end cmds += [ "tar -C '#{extract_dir}' -xzf '#{compressed_file}'", "mv '#{extract_dir}'/* '#{destination}'", "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] cmds.each{ |cmd| comm.execute(cmd) } true end # Decompress zip file on guest to given location # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [String] compressed_file Path to compressed file on guest # @param [String] destination Path for decompressed files on guest def self.decompress_zip(machine, compressed_file, destination, opts={}) comm = machine.communicate extract_dir = create_tmp_path(machine, type: :directory) cmds = [] if opts[:type] == :directory cmds << "mkdir -p '#{destination}'" else cmds << "mkdir -p '#{File.dirname(destination)}'" end cmds += [ "unzip '#{compressed_file}' -d '#{extract_dir}'", "mv '#{extract_dir}'/* '#{destination}'", "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] cmds.each{ |cmd| comm.execute(cmd) } true end end end end end ================================================ FILE: plugins/guests/linux/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/guest_inspection' module VagrantPlugins module GuestLinux module Cap class Halt extend Vagrant::Util::GuestInspection::Linux def self.halt(machine) begin if systemd?(machine.communicate) machine.communicate.sudo("systemctl poweroff") else machine.communicate.sudo("shutdown -h now") end rescue IOError, Vagrant::Errors::SSHDisconnected # Do nothing, because it probably means the machine shut down # and SSH connection was lost. end end end end end end ================================================ FILE: plugins/guests/linux/cap/mount_smb_shared_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "shellwords" require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestLinux module Cap class MountSMBSharedFolder extend SyncedFolder::UnixMountHelpers # Mounts and SMB folder on linux guest # # @param [Machine] machine # @param [String] name of mount # @param [String] path of mount on guest # @param [Hash] hash of mount options def self.mount_smb_shared_folder(machine, name, guestpath, options) expanded_guest_path = machine.guest.capability( :shell_expand_guest_path, guestpath) options[:smb_id] ||= name mount_device = options[:plugin].capability(:mount_name, name, options) mount_options, _, _ = options[:plugin].capability( :mount_options, name, expanded_guest_path, options) mount_type = options[:plugin].capability(:mount_type) # If a domain is provided in the username, separate it username, domain = (options[:smb_username] || '').split('@', 2) smb_password = options[:smb_password] # Ensure password is scrubbed Vagrant::Util::CredentialScrubber.sensitive(smb_password) if mount_options.include?("mfsymlinks") display_mfsymlinks_warning(machine.env) end mount_command = "mount -t #{mount_type} -o #{mount_options} #{mount_device} #{expanded_guest_path}" # Create the guest path if it doesn't exist machine.communicate.sudo("mkdir -p #{expanded_guest_path}") # Write the credentials file machine.communicate.sudo(<<-SCRIPT) cat <<"EOF" >/etc/smb_creds_#{name} username=#{username} password=#{smb_password} #{domain ? "domain=#{domain}" : ""} EOF chmod 0600 /etc/smb_creds_#{name} SCRIPT # Attempt to mount the folder. We retry here a few times because # it can fail early on. begin retryable(on: Vagrant::Errors::LinuxMountFailed, tries: 10, sleep: 2) do no_such_device = false stderr = "" status = machine.communicate.sudo(mount_command, error_check: false) do |type, data| if type == :stderr no_such_device = true if data =~ /No such device/i stderr += data.to_s end end if status != 0 || no_such_device raise Vagrant::Errors::LinuxMountFailed, command: mount_command, output: stderr end end ensure # Always remove credentials file after mounting attempts # have been completed if !machine.config.vm.allow_fstab_modification machine.communicate.sudo("rm /etc/smb_creds_#{name}") end end emit_upstart_notification(machine, expanded_guest_path) end def self.display_mfsymlinks_warning(env) d_file = env.data_dir.join("mfsymlinks_warning") if !d_file.exist? FileUtils.touch(d_file.to_path) env.ui.warn(I18n.t("vagrant.actions.vm.smb.mfsymlink_warning")) end end end end end end ================================================ FILE: plugins/guests/linux/cap/mount_virtualbox_shared_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestLinux module Cap class MountVirtualBoxSharedFolder extend SyncedFolder::UnixMountHelpers # Mounts and virtualbox folder on linux guest # # @param [Machine] machine # @param [String] name of mount # @param [String] path of mount on guest # @param [Hash] hash of mount options def self.mount_virtualbox_shared_folder(machine, name, guestpath, options) guest_path = Shellwords.escape(guestpath) mount_type = options[:plugin].capability(:mount_type) @@logger.debug("Mounting #{name} (#{options[:hostpath]} to #{guestpath})") builtin_mount_type = "-cit #{mount_type}" addon_mount_type = "-t #{mount_type}" mount_options, mount_uid, mount_gid = options[:plugin].capability(:mount_options, name, guest_path, options) mount_command = "mount #{addon_mount_type} -o #{mount_options} #{name} #{guest_path}" # Create the guest path if it doesn't exist machine.communicate.sudo("mkdir -p #{guest_path}") stderr = "" result = machine.communicate.sudo(mount_command, error_check: false) do |type, data| stderr << data if type == :stderr end if result != 0 if stderr.include?("-cit") @@logger.info("Detected builtin vboxsf module, modifying mount command") mount_command.sub!(addon_mount_type, builtin_mount_type) end # Attempt to mount the folder. We retry here a few times because # it can fail early on. stderr = "" retryable(on: Vagrant::Errors::VirtualBoxMountFailed, tries: 3, sleep: 5) do machine.communicate.sudo(mount_command, error_class: Vagrant::Errors::VirtualBoxMountFailed, error_key: :virtualbox_mount_failed, command: mount_command, output: stderr, ) { |type, data| stderr = data if type == :stderr } end end # Chown the directory to the proper user. We skip this if the # mount options contained a readonly flag, because it won't work. if !options[:mount_options] || !options[:mount_options].include?("ro") chown_command = "chown #{mount_uid}:#{mount_gid} #{guest_path}" machine.communicate.sudo(chown_command) end emit_upstart_notification(machine, guest_path) end def self.unmount_virtualbox_shared_folder(machine, guestpath, options) guest_path = Shellwords.escape(guestpath) result = machine.communicate.sudo("umount #{guest_path}", error_check: false) if result == 0 machine.communicate.sudo("rmdir #{guest_path}", error_check: false) end end end end end end ================================================ FILE: plugins/guests/linux/cap/network_interfaces.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestLinux module Cap class NetworkInterfaces # Valid ethernet device prefix values. # eth - classic prefix # en - predictable interface names prefix POSSIBLE_ETHERNET_PREFIXES = ["eth".freeze, "en".freeze].freeze @@logger = Log4r::Logger.new("vagrant::guest::linux::network_interfaces") # Get network interfaces as a list. The result will be something like: # # eth0\nenp0s8\nenp0s9 # # @return [Array] def self.network_interfaces(machine, path = "/sbin/ip") s = "" machine.communicate.sudo("#{path} -o -0 addr | grep -v LOOPBACK | awk '{print $2}' | sed 's/://'") do |type, data| s << data if type == :stdout end # In some cases net devices may be added to the guest and will not # properly show up when using `ip`. This pulls any device information # that can be found in /proc and adds it to the list of interfaces s << "\n" machine.communicate.sudo("cat /proc/net/dev | grep -E '^[a-z0-9 ]+:' | awk '{print $1}' | sed 's/://'", error_check: false) do |type, data| s << data if type == :stdout end # Collect all loopback interfaces los = "" machine.communicate.sudo("#{path} -o -0 addr | grep LOOPBACK | awk '{print $2}' | sed 's/://'") do |type, data| los << data if type == :stdout end loifaces = los.split("\n") @@logger.debug("loopback interfaces: #{loifaces.inspect}") ifaces = s.split("\n").reject { |x| x.empty? or loifaces.include?(x) } @@logger.debug("Unsorted list: #{ifaces.inspect}") # Break out integers from strings and sort the arrays to provide # a natural sort for the interface names # NOTE: Devices named with a hex value suffix will _not_ be sorted # as expected. This is generally seen with veth* devices, and proper ordering # is currently not required ifaces = ifaces.map do |iface| iface.scan(/(.+?)(\d+)?/).flatten.map do |iface_part| if iface_part.to_i.to_s == iface_part iface_part.to_i else iface_part end end end ifaces = ifaces.uniq.sort do |lhs, rhs| result = 0 slice_length = [rhs.size, lhs.size].min slice_length.times do |idx| if(lhs[idx].is_a?(rhs[idx].class)) result = lhs[idx] <=> rhs[idx] elsif(lhs[idx].is_a?(String)) result = 1 else result = -1 end break if result != 0 end result end.map(&:join) @@logger.debug("Sorted list: #{ifaces.inspect}") # Extract ethernet devices and place at start of list resorted_ifaces = [] resorted_ifaces += ifaces.find_all do |iface| POSSIBLE_ETHERNET_PREFIXES.any?{|prefix| iface.start_with?(prefix)} && iface.match(/^[a-zA-Z0-9]+$/) end resorted_ifaces += ifaces - resorted_ifaces ifaces = resorted_ifaces @@logger.debug("Ethernet preferred sorted list: #{ifaces.inspect}") ifaces end end end end end ================================================ FILE: plugins/guests/linux/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestLinux module Cap class NFS extend SyncedFolder::UnixMountHelpers def self.nfs_client_installed(machine) machine.communicate.test("test -x /sbin/mount.nfs") end def self.mount_nfs_folder(machine, ip, folders) comm = machine.communicate # Mount each folder separately so we can retry. folders.each do |name, opts| # Shellescape the paths in case they do not have special characters. guest_path = Shellwords.escape(opts[:guestpath]) host_path = Shellwords.escape(opts[:hostpath]) # Build the list of mount options. mount_opts = [] mount_opts << "vers=#{opts[:nfs_version]}" if opts[:nfs_version] mount_opts << "udp" if opts[:nfs_udp] if opts[:mount_options] mount_opts = mount_opts + opts[:mount_options].dup end mount_opts = mount_opts.join(",") machine.communicate.sudo("mkdir -p #{guest_path}") command = "mount -o #{mount_opts} #{ip}:#{host_path} #{guest_path}" # Run the command, raising a specific error. retryable(on: Vagrant::Errors::NFSMountFailed, tries: 3, sleep: 5) do machine.communicate.sudo(command, error_class: Vagrant::Errors::NFSMountFailed, ) end emit_upstart_notification(machine, guest_path) end end end end end end ================================================ FILE: plugins/guests/linux/cap/persist_mount_shared_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util" require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestLinux module Cap class PersistMountSharedFolder extend SyncedFolder::UnixMountHelpers @@logger = Log4r::Logger.new("vagrant::guest::linux::persist_mount_shared_folders") # Inserts fstab entry for a set of synced folders. Will fully replace # the currently managed group of Vagrant managed entries. Note, passing # empty list of folders will just remove entries # # @param [Machine] machine The machine to run the action on # @param [Map] A map of folders to add to fstab def self.persist_mount_shared_folder(machine, folders) if folders.nil? @@logger.info("clearing /etc/fstab") self.remove_vagrant_managed_fstab(machine) return end ssh_info = machine.ssh_info export_folders = folders.map { |type, folder| folder.map { |name, data| if data[:plugin].capability?(:mount_type) guest_path = Shellwords.escape(data[:guestpath]) data[:owner] ||= ssh_info[:username] data[:group] ||= ssh_info[:username] mount_type = data[:plugin].capability(:mount_type) mount_options, _, _ = data[:plugin].capability( :mount_options, name, guest_path, data) if data[:plugin].capability?(:mount_name) name = data[:plugin].capability(:mount_name, name, data) end else next end { name: name, mount_point: guest_path, mount_type: mount_type, mount_options: mount_options, } } }.flatten.compact fstab_entry = Vagrant::Util::TemplateRenderer.render('guests/linux/etc_fstab', folders: export_folders) self.remove_vagrant_managed_fstab(machine) machine.communicate.sudo("echo '#{fstab_entry}' >> /etc/fstab") end private def self.fstab_exists?(machine) machine.communicate.test("test -f /etc/fstab") end def self.contains_vagrant_data?(machine) machine.communicate.test("grep '#VAGRANT-BEGIN' /etc/fstab") end def self.remove_vagrant_managed_fstab(machine) if fstab_exists?(machine) if contains_vagrant_data?(machine) machine.communicate.sudo("sed -i '/\#VAGRANT-BEGIN/,/\#VAGRANT-END/d' /etc/fstab") else @@logger.info("no vagrant data in fstab file, carrying on") end else @@logger.info("no fstab file found, carrying on") end end end end end end ================================================ FILE: plugins/guests/linux/cap/port.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestLinux module Cap class Port def self.port_open_check(machine, port) machine.communicate.test("nc -z -w2 127.0.0.1 #{port}") end end end end end ================================================ FILE: plugins/guests/linux/cap/public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "vagrant/util/shell_quote" module VagrantPlugins module GuestLinux module Cap class PublicKey def self.insert_public_key(machine, contents) comm = machine.communicate contents = contents.strip << "\n" remote_path = "/tmp/vagrant-insert-pubkey-#{Time.now.to_i}" Tempfile.open("vagrant-linux-insert-public-key") do |f| f.binmode f.write(contents) f.fsync f.close comm.upload(f.path, remote_path) end # Use execute (not sudo) because we want to execute this as the SSH # user (which is "vagrant" by default). comm.execute <<-EOH.gsub(/^ */, "") mkdir -p ~/.ssh chmod 0700 ~/.ssh cat '#{remote_path}' >> ~/.ssh/authorized_keys && chmod 0600 ~/.ssh/authorized_keys result=$? rm -f '#{remote_path}' exit $result EOH end def self.remove_public_key(machine, contents) comm = machine.communicate contents = contents.strip << "\n" remote_path = "/tmp/vagrant-remove-pubkey-#{Time.now.to_i}" Tempfile.open("vagrant-linux-remove-public-key") do |f| f.binmode f.write(contents) f.fsync f.close comm.upload(f.path, remote_path) end # Use execute (not sudo) because we want to execute this as the SSH # user (which is "vagrant" by default). comm.execute <<-EOH.sub(/^ */, "") if test -f ~/.ssh/authorized_keys; then grep -v -x -f '#{remote_path}' ~/.ssh/authorized_keys > ~/.ssh/authorized_keys.tmp mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys && chmod 0600 ~/.ssh/authorized_keys result=$? fi rm -f '#{remote_path}' exit $result EOH end end end end end ================================================ FILE: plugins/guests/linux/cap/read_ip_address.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestLinux module Cap class ReadIPAddress def self.read_ip_address(machine) comm = machine.communicate if comm.test("which ip") command = "LANG=en ip addr | grep -Po 'inet \\K[\\d.]+' | grep -v 127.0.0.1" else command = "LANG=en ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | awk '{ print $1 }'" end result = "" comm.sudo(command) do |type, data| result << data if type == :stdout end result.chomp.split("\n").first end end end end end ================================================ FILE: plugins/guests/linux/cap/reboot.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/guest_inspection' require "log4r" module VagrantPlugins module GuestLinux module Cap class Reboot extend Vagrant::Util::GuestInspection::Linux DEFAULT_MAX_REBOOT_RETRY_DURATION = 120 WAIT_SLEEP_TIME = 5 def self.reboot(machine) @logger = Log4r::Logger.new("vagrant::linux::reboot") reboot_script = "ps -q 1 -o comm=,start= > /tmp/.vagrant-reboot" if systemd?(machine.communicate) reboot_cmd = "systemctl reboot" else reboot_cmd = "reboot" end comm = machine.communicate reboot_script += "; #{reboot_cmd}" @logger.debug("Issuing reboot command for guest") comm.sudo(reboot_script) machine.ui.info(I18n.t("vagrant.guests.capabilities.rebooting")) @logger.debug("Waiting for machine to finish rebooting") wait_remaining = ENV.fetch("VAGRANT_MAX_REBOOT_RETRY_DURATION", DEFAULT_MAX_REBOOT_RETRY_DURATION).to_i wait_remaining = DEFAULT_MAX_REBOOT_RETRY_DURATION if wait_remaining < 1 begin wait_for_reboot(machine) rescue Vagrant::Errors::MachineGuestNotReady raise if wait_remaining < 0 @logger.warn("Machine not ready, cannot start reboot yet. Trying again") sleep(WAIT_SLEEP_TIME) wait_remaining -= WAIT_SLEEP_TIME retry end end def self.wait_for_reboot(machine) caught = false begin check_script = 'grep "$(ps -q 1 -o comm=,start=)" /tmp/.vagrant-reboot' while machine.guest.ready? && machine.communicate.execute(check_script, error_check: false) == 0 sleep 10 end rescue # The check script execution may result in an exception # getting raised depending on the state of the communicator # when executing. We'll allow for it to happen once, and then # raise if we get an exception again if caught raise end caught = true sleep 10 retry end end end end end end ================================================ FILE: plugins/guests/linux/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/rsync/default_unix_cap" module VagrantPlugins module GuestLinux module Cap class RSync extend VagrantPlugins::SyncedFolderRSync::DefaultUnixCap end end end end ================================================ FILE: plugins/guests/linux/cap/shell_expand_guest_path.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestLinux module Cap class ShellExpandGuestPath def self.shell_expand_guest_path(machine, path) real_path = nil path = path.gsub(/ /, '\ ') machine.communicate.execute("echo; printf #{path}") do |type, data| if type == :stdout real_path ||= "" real_path += data end end if real_path # The last line is the path we care about real_path = real_path.split("\n").last.chomp end if !real_path # If no real guest path was detected, this is really strange # and we raise an exception because this is a bug. raise Vagrant::Errors::ShellExpandFailed end # Chomp the string so that any trailing newlines are killed return real_path.chomp end end end end end ================================================ FILE: plugins/guests/linux/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestLinux class Guest < Vagrant.plugin("2", :guest) # Name used for guest detection GUEST_DETECTION_NAME = "linux".freeze def detect?(machine) machine.communicate.test <<-EOH.gsub(/^ */, '') if test -r /etc/os-release; then source /etc/os-release && test 'x#{self.class.const_get(:GUEST_DETECTION_NAME)}' = "x$ID" && exit fi if test -x /usr/bin/lsb_release; then /usr/bin/lsb_release -i 2>/dev/null | grep -qi '#{self.class.const_get(:GUEST_DETECTION_NAME)}' && exit fi if test -r /etc/issue; then cat /etc/issue | grep -qi '#{self.class.const_get(:GUEST_DETECTION_NAME)}' && exit fi exit 1 EOH end end end end ================================================ FILE: plugins/guests/linux/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestLinux class Plugin < Vagrant.plugin("2") name "Linux guest." description "Linux guest support." guest(:linux) do require_relative "guest" Guest end guest_capability(:linux, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:linux, :choose_addressable_ip_addr) do require_relative "cap/choose_addressable_ip_addr" Cap::ChooseAddressableIPAddr end guest_capability(:linux, :create_tmp_path) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:linux, :decompress_tgz) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:linux, :decompress_zip) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:linux, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:linux, :insert_public_key) do require_relative "cap/public_key" Cap::PublicKey end guest_capability(:linux, :shell_expand_guest_path) do require_relative "cap/shell_expand_guest_path" Cap::ShellExpandGuestPath end guest_capability(:linux, :mount_nfs_folder) do require_relative "cap/nfs" Cap::NFS end guest_capability(:linux, :mount_smb_shared_folder) do require_relative "cap/mount_smb_shared_folder" Cap::MountSMBSharedFolder end guest_capability(:linux, :mount_virtualbox_shared_folder) do require_relative "cap/mount_virtualbox_shared_folder" Cap::MountVirtualBoxSharedFolder end guest_capability(:linux, :persist_mount_shared_folder) do require_relative "cap/persist_mount_shared_folder" Cap::PersistMountSharedFolder end guest_capability(:linux, :network_interfaces) do require_relative "cap/network_interfaces" Cap::NetworkInterfaces end guest_capability(:linux, :nfs_client_installed) do require_relative "cap/nfs" Cap::NFS end # For the Docker provider guest_capability(:linux, :port_open_check) do require_relative "cap/port" Cap::Port end guest_capability(:linux, :read_ip_address) do require_relative "cap/read_ip_address" Cap::ReadIPAddress end guest_capability(:linux, :wait_for_reboot) do require_relative "cap/reboot" Cap::Reboot end guest_capability(:linux, :reboot) do require_relative "cap/reboot" Cap::Reboot end guest_capability(:linux, :remove_public_key) do require_relative "cap/public_key" Cap::PublicKey end guest_capability(:linux, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end guest_capability(:linux, :rsync_command) do require_relative "cap/rsync" Cap::RSync end guest_capability(:linux, :rsync_post) do require_relative "cap/rsync" Cap::RSync end guest_capability(:linux, :rsync_pre) do require_relative "cap/rsync" Cap::RSync end guest_capability(:linux, :unmount_virtualbox_shared_folder) do require_relative "cap/mount_virtualbox_shared_folder" Cap::MountVirtualBoxSharedFolder end end end end ================================================ FILE: plugins/guests/mint/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../linux/guest' module VagrantPlugins module GuestMint class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "Linux Mint".freeze end end end ================================================ FILE: plugins/guests/mint/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestMint class Plugin < Vagrant.plugin("2") name "Mint guest" description "Mint guest support." guest(:mint, :ubuntu) do require_relative "guest" Guest end end end end ================================================ FILE: plugins/guests/netbsd/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestNetBSD module Cap class ChangeHostName def self.change_host_name(machine, name) if !machine.communicate.test("hostname -s | grep '^#{name}$'") machine.communicate.sudo(< /tmp/rc.conf.vagrant_changehostname_#{name} && mv /tmp/rc.conf.vagrant_changehostname_#{name} /etc/rc.conf && hostname #{name} CMDS end end end end end end ================================================ FILE: plugins/guests/netbsd/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestNetBSD module Cap class ConfigureNetworks include Vagrant::Util def self.configure_networks(machine, networks) # setup a new rc.conf file newrcconf = "/tmp/rc.conf.vagrant_configurenetworks" machine.communicate.sudo("sed -e '/^#VAGRANT-BEGIN/,/^#VAGRANT-END/ d' /etc/rc.conf > #{newrcconf}") networks.each do |network| # create an interface configuration file fragment entry = TemplateRenderer.render("guests/netbsd/network_#{network[:type]}", options: network) Tempfile.open("vagrant-netbsd-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close machine.communicate.upload(f.path, "/tmp/vagrant-network-entry") end machine.communicate.sudo("cat /tmp/vagrant-network-entry >> #{newrcconf}") machine.communicate.sudo("rm -f /tmp/vagrant-network-entry") ifname = "wm#{network[:interface]}" # remove old configuration machine.communicate.sudo("/sbin/dhcpcd -x #{ifname}", { error_check: false }) machine.communicate.sudo("/sbin/ifconfig #{ifname} inet delete", { error_check: false }) # live new configuration if network[:type].to_sym == :static machine.communicate.sudo("/sbin/ifconfig #{ifname} media autoselect up;/sbin/ifconfig #{ifname} inet #{network[:ip]} netmask #{network[:netmask]}") elsif network[:type].to_sym == :dhcp machine.communicate.sudo("/sbin/dhcpcd -n -q #{ifname}") end end # install new rc.conf machine.communicate.sudo("install -c -o 0 -g 0 -m 644 #{newrcconf} /etc/rc.conf") end end end end end ================================================ FILE: plugins/guests/netbsd/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/rsync/default_unix_cap" module VagrantPlugins module GuestNetBSD module Cap class RSync extend VagrantPlugins::SyncedFolderRSync::DefaultUnixCap def self.rsync_install(machine) machine.communicate.sudo( 'PATH=$PATH:/usr/sbin '\ 'PKG_PATH="http://ftp.NetBSD.org/pub/pkgsrc/packages/NetBSD/' \ '`uname -m`/`uname -r | cut -d. -f1-2`/All" ' \ 'pkg_add rsync' ) end end end end end ================================================ FILE: plugins/guests/netbsd/cap/shell_expand_guest_path.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestNetBSD module Cap class ShellExpandGuestPath def self.shell_expand_guest_path(machine, path) real_path = nil path = path.gsub(/ /, '\ ') machine.communicate.execute("printf #{path}") do |type, data| if type == :stdout real_path ||= "" real_path += data end end if !real_path # If no real guest path was detected, this is really strange # and we raise an exception because this is a bug. raise Vagrant::Errors::ShellExpandFailed end # Chomp the string so that any trailing newlines are killed return real_path.chomp end end end end end ================================================ FILE: plugins/guests/netbsd/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestNetBSD class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("uname -s | grep NetBSD") end end end end ================================================ FILE: plugins/guests/netbsd/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestNetBSD class Plugin < Vagrant.plugin("2") name "NetBSD guest" description "NetBSD guest support." guest(:netbsd, :bsd) do require_relative "guest" Guest end guest_capability(:netbsd, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:netbsd, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:netbsd, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:netbsd, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end guest_capability(:netbsd, :rsync_command) do require_relative "cap/rsync" Cap::RSync end guest_capability(:netbsd, :rsync_post) do require_relative "cap/rsync" Cap::RSync end guest_capability(:netbsd, :rsync_pre) do require_relative "cap/rsync" Cap::RSync end guest_capability(:netbsd, :shell_expand_guest_path) do require_relative "cap/shell_expand_guest_path" Cap::ShellExpandGuestPath end end end end ================================================ FILE: plugins/guests/nixos/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestNixos module Cap class ChangeHostName include Vagrant::Util def self.change_host_name(machine, name) # upload the config file hostname_module = TemplateRenderer.render("guests/nixos/hostname", name: name) upload(machine, hostname_module, "/etc/nixos/vagrant-hostname.nix") end # Upload a file. def self.upload(machine, content, remote_path) remote_temp = mktemp(machine) Tempfile.open("vagrant-nixos-change-host-name") do |f| f.binmode f.write(content) f.fsync f.close machine.communicate.upload(f.path, "#{remote_temp}") end machine.communicate.sudo("mv #{remote_temp} #{remote_path}") end # Create a temp file. def self.mktemp(machine) path = nil machine.communicate.execute("mktemp --suffix -vagrant-upload") do |type, result| path = result.chomp if type == :stdout end path end end end end end ================================================ FILE: plugins/guests/nixos/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ipaddr" require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestNixos module Cap class ConfigureNetworks include Vagrant::Util def self.configure_networks(machine, networks) # set the prefix length. networks.each do |network| network[:prefix_length] = (network[:netmask] && netmask_to_cidr(network[:netmask])) end # set the device names. assign_device_names(machine, networks) # upload the config file network_module = TemplateRenderer.render("guests/nixos/network", networks: networks) upload(machine, network_module, "/etc/nixos/vagrant-network.nix") end # Set :device on each network. # Attempts to use biosdevname when available to detect interface names, # and falls back to ifconfig otherwise. def self.assign_device_names(machine, networks) if machine.communicate.test("command -v biosdevname") # use biosdevname to get info about the interfaces interfaces = get_interfaces(machine) if machine.provider.capability?(:nic_mac_addresses) # find device name by MAC lookup. mac_addresses = machine.provider.capability(:nic_mac_addresses) networks.each do |network| mac_address = mac_addresses[network[:interface]+1] interface = interfaces.detect {|nic| nic[:mac_address].gsub(":","") == mac_address} if mac_address network[:device] = interface[:kernel] if interface end else # assume interface numbers correspond to (ethN+1). networks.each do |network| interface = interfaces.detect {|nic| nic[:ethn] == network[:interface]} network[:device] = interface[:kernel] if interface end end else # assume interface numbers correspond to the order of interfaces. interfaces = get_interface_names(machine) networks.each do |network| network[:device] = interfaces[network[:interface]] end end end def self.get_interface_names(machine) output = nil machine.communicate.execute("ifconfig -a") do |type, result| output = result.chomp if type == :stdout end names = output.scan(/^[^:\s]+/).reject {|name| name =~ /^lo/ } names end # Upload a file. def self.upload(machine, content, remote_path) remote_temp = mktemp(machine) Tempfile.open("vagrant-nixos-configure-networks") do |f| f.binmode f.write(content) f.fsync f.close machine.communicate.upload(f.path, "#{remote_temp}") end machine.communicate.sudo("mv #{remote_temp} #{remote_path}") end # Create a temp file. def self.mktemp(machine) path = nil machine.communicate.execute("mktemp --suffix -vagrant-upload") do |type, result| path = result.chomp if type == :stdout end path end # using biosdevname, get all interfaces as a list of hashes, where: # :kernel - the kernel's name for the device, # :ethn - the calculated ethN-style name converted to integer. # :mac_address - the permanent mac address. ethN-style name converted to integer. def self.get_interfaces(machine) interfaces = [] # get all adapters, as named by the kernel output = nil machine.communicate.sudo("biosdevname -d") do |type, result| output = result if type == :stdout end kernel_if_names = output.scan(/Kernel name: ([^\n]+)/).flatten mac_addresses = output.scan(/Permanent MAC: ([^\n]+)/).flatten # get ethN-style names ethns = [] kernel_if_names.each do |name| machine.communicate.sudo("biosdevname --policy=all_ethN -i #{name}") do |type, result| ethns << result.gsub(/[^\d]/,'').to_i if type == :stdout end end # populate the interface list kernel_if_names.each_index do |i| interfaces << { kernel: kernel_if_names[i], ethn: ethns[i], mac_address: mac_addresses[i] } end interfaces end def self.netmask_to_cidr(mask) IPAddr.new(mask).to_i.to_s(2).count("1") end end end end end ================================================ FILE: plugins/guests/nixos/cap/nfs_client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestNixos module Cap class NFSClient def self.nfs_client_installed(machine) machine.communicate.test("test -x /run/current-system/sw/sbin/mount.nfs") end end end end end ================================================ FILE: plugins/guests/nixos/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestNixos class Guest < Vagrant.plugin("2", :guest) def detect?(machine) # For some reason our test passes on Windows, so just short # circuit because we're not Windows. if machine.config.vm.communicator == :winrm return false end machine.communicate.test("test -f /run/current-system/nixos-version") end end end end ================================================ FILE: plugins/guests/nixos/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestNixos class Plugin < Vagrant.plugin("2") name "NixOS guest" description "NixOS guest support." guest(:nixos, :linux) do require_relative "guest" Guest end guest_capability(:nixos, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:nixos, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:nixos, :nfs_client_installed) do require_relative "cap/nfs_client" Cap::NFSClient end end end end ================================================ FILE: plugins/guests/omnios/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../../linux/cap/change_host_name' module VagrantPlugins module GuestOmniOS module Cap class ChangeHostName extend VagrantPlugins::GuestLinux::Cap::ChangeHostName def self.change_name_command(name) basename = name.split(".", 2)[0] return <<-EOH.gsub(/^ {14}/, "") # Set hostname echo '#{name}' > /etc/nodename hostname '#{name}' EOH end end end end end ================================================ FILE: plugins/guests/omnios/cap/mount_nfs_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestOmniOS module Cap class MountNFSFolder def self.mount_nfs_folder(machine, ip, folders) comm = machine.communicate commands = [] folders.each do |_, opts| commands << <<-EOH.gsub(/^ {14}/, '') mkdir -p '#{opts[:guestpath]}' /sbin/mount '#{ip}:#{opts[:hostpath]}' '#{opts[:guestpath]}' EOH end comm.sudo(commands.join("\n")) end end end end end ================================================ FILE: plugins/guests/omnios/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestOmniOS module Cap class RSync def self.rsync_install(machine) machine.communicate.sudo("pkg install rsync") end end end end end ================================================ FILE: plugins/guests/omnios/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestOmniOS class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("cat /etc/release | grep -i OmniOS") end end end end ================================================ FILE: plugins/guests/omnios/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestOmniOS class Plugin < Vagrant.plugin("2") name "OmniOS guest." description "OmniOS guest support." guest(:omnios, :solaris) do require_relative "guest" Guest end guest_capability(:omnios, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:omnios, :mount_nfs_folder) do require_relative "cap/mount_nfs_folder" Cap::MountNFSFolder end guest_capability(:omnios, :rsync_install) do require_relative "cap/rsync" Cap::RSync end end end end ================================================ FILE: plugins/guests/openbsd/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/guest_hosts' module VagrantPlugins module GuestOpenBSD module Cap class ChangeHostName extend Vagrant::Util::GuestHosts::BSD def self.change_host_name(machine, name) comm = machine.communicate if !comm.test("hostname -f | grep '^#{name}$'", sudo: false, shell: "sh") basename = name.split(".", 2)[0] command = <<-EOH.gsub(/^ {14}/, '') # Set the hostname hostname '#{name}' sed -i'' 's/^hostname=.*$/hostname=\"#{name}\"/' /etc/rc.conf echo '#{name}' > /etc/myname EOH comm.sudo(command, shell: "sh") end network_with_hostname = machine.config.vm.networks.map {|_, c| c if c[:hostname] }.compact[0] if network_with_hostname replace_host(comm, name, network_with_hostname[:ip]) else add_hostname_to_loopback_interface(comm, name) end end end end end end ================================================ FILE: plugins/guests/openbsd/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestOpenBSD module Cap class ConfigureNetworks include Vagrant::Util def self.configure_networks(machine, networks) networks.each do |network| entry = TemplateRenderer.render("guests/openbsd/network_#{network[:type]}", options: network) Tempfile.open("vagrant-openbsd-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close machine.communicate.upload(f.path, "/tmp/vagrant-network-entry") end # Determine the interface prefix... command = "ifconfig -a | grep -o ^[0-9a-z]*" result = "" ifname = "" machine.communicate.execute(command) do |type, data| result << data if type == :stdout if result.split(/\n/).any?{|i| i.match(/vio*/)} ifname = "vio#{network[:interface]}" else ifname = "em#{network[:interface]}" end end machine.communicate.sudo("mv /tmp/vagrant-network-entry /etc/hostname.#{ifname}") # apply new configurations machine.communicate.sudo("sh /etc/netstart #{ifname}") end end end end end end ================================================ FILE: plugins/guests/openbsd/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestOpenBSD module Cap class Halt def self.halt(machine) begin # Versions of OpenBSD prior to 5.7 require the -h option to be # provided with the -p option. Later options allow the -h to # be optional. machine.communicate.sudo("/sbin/shutdown -p -h now", shell: "sh") rescue IOError, Vagrant::Errors::SSHDisconnected # Do nothing, because it probably means the machine shut down # and SSH connection was lost. end end end end end end ================================================ FILE: plugins/guests/openbsd/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/rsync/default_unix_cap" module VagrantPlugins module GuestOpenBSD module Cap class RSync extend VagrantPlugins::SyncedFolderRSync::DefaultUnixCap def self.rsync_install(machine) install_output = {:stderr => '', :stdout => ''} command = 'PKG_PATH="http://ftp.openbsd.org/pub/OpenBSD/' \ '`uname -r`/packages/`arch -s`/" ' \ 'pkg_add -I rsync--' machine.communicate.sudo(command) do |type, data| install_output[type] << data if install_output.key?(type) end # pkg_add returns 0 even if package was not found, so # validate package is actually installed machine.communicate.sudo('pkg_info -cA | grep inst:rsync-[[:digit:]]', error_class: Vagrant::Errors::RSyncNotInstalledInGuest, command: command, stderr: install_output[:stderr], stdout: install_output[:stdout] ) end end end end end ================================================ FILE: plugins/guests/openbsd/cap/shell_expand_guest_path.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestOpenBSD module Cap class ShellExpandGuestPath def self.shell_expand_guest_path(machine, path) real_path = nil path = path.gsub(/ /, '\ ') machine.communicate.execute("printf #{path}") do |type, data| if type == :stdout real_path ||= "" real_path += data end end if !real_path # If no real guest path was detected, this is really strange # and we raise an exception because this is a bug. raise Vagrant::Errors::ShellExpandFailed end # Chomp the string so that any trailing newlines are killed return real_path.chomp end end end end end ================================================ FILE: plugins/guests/openbsd/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestOpenBSD class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("uname -s | grep 'OpenBSD'") end end end end ================================================ FILE: plugins/guests/openbsd/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestOpenBSD class Plugin < Vagrant.plugin("2") name "OpenBSD guest" description "OpenBSD guest support." guest(:openbsd, :bsd) do require_relative "guest" Guest end guest_capability(:openbsd, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:openbsd, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:openbsd, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:openbsd, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openbsd, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openbsd, :rsync_command) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openbsd, :rsync_post) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openbsd, :rsync_pre) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openbsd, :shell_expand_guest_path) do require_relative "cap/shell_expand_guest_path" Cap::ShellExpandGuestPath end end end end ================================================ FILE: plugins/guests/openwrt/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestOpenWrt module Cap class ChangeHostName def self.change_host_name(machine, name) comm = machine.communicate if !comm.test("uci get system.@system[0].hostname | grep '^#{name}$'", sudo: false) comm.execute <<~EOH uci set system.@system[0].hostname='#{name}' uci commit system /etc/init.d/system reload EOH end end end end end end ================================================ FILE: plugins/guests/openwrt/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestOpenWrt module Cap class Halt def self.halt(machine) begin machine.communicate.execute("halt") rescue IOError, Vagrant::Errors::SSHDisconnected # Ignore, this probably means connection closed because it # shut down. end end end end end end ================================================ FILE: plugins/guests/openwrt/cap/insert_public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/shell_quote" module VagrantPlugins module GuestOpenWrt module Cap class InsertPublicKey def self.insert_public_key(machine, contents) contents = contents.chomp contents = Vagrant::Util::ShellQuote.escape(contents, "'") machine.communicate.tap do |comm| comm.execute <<~EOH printf '#{contents}\\n' >> /etc/dropbear/authorized_keys EOH end end end end end end ================================================ FILE: plugins/guests/openwrt/cap/remove_public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/shell_quote" module VagrantPlugins module GuestOpenWrt module Cap class RemovePublicKey def self.remove_public_key(machine, contents) contents = contents.chomp contents = Vagrant::Util::ShellQuote.escape(contents, "'") machine.communicate.tap do |comm| comm.execute <<~EOH if test -f /etc/dropbear/authorized_keys ; then sed -i '/^.*#{contents}.*$/d' /etc/dropbear/authorized_keys fi EOH end end end end end end ================================================ FILE: plugins/guests/openwrt/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestOpenWrt module Cap class RSync def self.rsync_installed(machine) machine.communicate.test("which rsync") end def self.rsync_install(machine) machine.communicate.tap do |comm| comm.execute <<~EOH opkg update opkg install rsync EOH end end def self.rsync_pre(machine, opts) machine.communicate.tap do |comm| comm.execute("mkdir -p '#{opts[:guestpath]}'") end end def self.rsync_command(machine) "rsync -zz" end def self.rsync_post(machine, opts) # Don't do anything because BusyBox's `find` doesn't support the # syntax in plugins/synced_folders/rsync/default_unix_cap.rb. end end end end end ================================================ FILE: plugins/guests/openwrt/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestOpenWrt class Guest < Vagrant.plugin("2", :guest) # Name used for guest detection GUEST_DETECTION_NAME = "openwrt".freeze def detect?(machine) machine.communicate.test <<~EOH if test -e /etc/openwrt_release; then exit fi if test -r /etc/os-release; then source /etc/os-release && test 'x#{self.class.const_get(:GUEST_DETECTION_NAME)}' = "x$ID" && exit fi if test -r /etc/banner; then cat /etc/banner | grep -qi '#{self.class.const_get(:GUEST_DETECTION_NAME)}' && exit fi exit 1 EOH end end end end ================================================ FILE: plugins/guests/openwrt/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestOpenWrt class Plugin < Vagrant.plugin("2") name "OpenWrt guest" description "OpenWrt guest support." guest(:openwrt, :linux) do require_relative "guest" Guest end guest_capability(:openwrt, :insert_public_key) do require_relative "cap/insert_public_key" Cap::InsertPublicKey end guest_capability(:openwrt, :remove_public_key) do require_relative "cap/remove_public_key" Cap::RemovePublicKey end guest_capability(:openwrt, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:openwrt, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openwrt, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openwrt, :rsync_pre) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openwrt, :rsync_command) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openwrt, :rsync_post) do require_relative "cap/rsync" Cap::RSync end guest_capability(:openwrt, :halt) do require_relative "cap/halt" Cap::Halt end end end end ================================================ FILE: plugins/guests/photon/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../../linux/cap/change_host_name' module VagrantPlugins module GuestPhoton module Cap class ChangeHostName extend VagrantPlugins::GuestLinux::Cap::ChangeHostName def self.change_name_command(name) return <<-EOH.gsub(/^ {14}/, "") # Set the hostname echo '#{name}' > /etc/hostname hostname '#{name}' EOH end end end end end ================================================ FILE: plugins/guests/photon/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestPhoton module Cap class ConfigureNetworks include Vagrant::Util def self.configure_networks(machine, networks) comm = machine.communicate commands = [] interfaces = [] comm.sudo("ifconfig | grep 'eth' | cut -f1 -d' '") do |_, result| interfaces = result.split("\n") end networks.each do |network| device = interfaces[network[:interface]] command = "ifconfig #{device}" command << " #{network[:ip]}" if network[:ip] command << " netmask #{network[:netmask]}" if network[:netmask] commands << command end comm.sudo(commands.join("\n")) end end end end end ================================================ FILE: plugins/guests/photon/cap/docker.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestPhoton module Cap module Docker def self.docker_daemon_running(machine) machine.communicate.test('test -S /run/docker.sock') end end end end end ================================================ FILE: plugins/guests/photon/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestPhoton class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("cat /etc/photon-release | grep 'VMware Photon'") end end end end ================================================ FILE: plugins/guests/photon/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestPhoton class Plugin < Vagrant.plugin("2") name "VMware Photon guest" description "VMware Photon guest support." guest(:photon, :linux) do require_relative "guest" Guest end guest_capability(:photon, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:photon, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:photon, :docker_daemon_running) do require_relative "cap/docker" Cap::Docker end end end end ================================================ FILE: plugins/guests/pld/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../../linux/cap/change_host_name' module VagrantPlugins module GuestPld module Cap class ChangeHostName extend VagrantPlugins::GuestLinux::Cap::ChangeHostName def self.change_name_command(name) return <<-EOH.gsub(/^ {14}/, "") hostname '#{name}' sed -i 's/\\(HOSTNAME=\\).*/\\1#{name}/' /etc/sysconfig/network sed -i 's/\\(DHCP_HOSTNAME=\\).*/\\1\"#{name}\"/' /etc/sysconfig/interfaces/ifcfg-* # Restart networking service network restart EOH end end end end end ================================================ FILE: plugins/guests/pld/cap/flavor.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestPld module Cap class Flavor def self.flavor(machine) return :pld end end end end end ================================================ FILE: plugins/guests/pld/cap/network_scripts_dir.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestPld module Cap class NetworkScriptsDir def self.network_scripts_dir(machine) "/etc/sysconfig/interfaces" end end end end end ================================================ FILE: plugins/guests/pld/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestPld class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("cat /etc/pld-release") end end end end ================================================ FILE: plugins/guests/pld/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestPld class Plugin < Vagrant.plugin("2") name "PLD Linux guest" description "PLD Linux guest support." guest(:pld, :redhat) do require_relative "guest" Guest end guest_capability(:pld, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:pld, :network_scripts_dir) do require_relative "cap/network_scripts_dir" Cap::NetworkScriptsDir end guest_capability(:pld, :flavor) do require_relative "cap/flavor" Cap::Flavor end end end end ================================================ FILE: plugins/guests/redhat/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/guest_hosts' module VagrantPlugins module GuestRedHat module Cap class ChangeHostName extend Vagrant::Util::GuestInspection::Linux extend Vagrant::Util::GuestHosts::Linux def self.change_host_name(machine, name) comm = machine.communicate if !comm.test("hostname -f | grep '^#{name}$'", sudo: false) basename = name.split('.', 2)[0] comm.sudo <<-EOH.gsub(/^ {14}/, '') # Update sysconfig if [ -f /etc/sysconfig/network ]; then sed -i 's/\\(HOSTNAME=\\).*/\\1#{name}/' /etc/sysconfig/network fi # Update DNS find /etc/sysconfig/network-scripts -maxdepth 1 -type f -name 'ifcfg-*' | xargs -r sed -i 's/\\(DHCP_HOSTNAME=\\).*/\\1\"#{basename}\"/' # Set the hostname - use hostnamectl if available echo '#{name}' > /etc/hostname EOH if hostnamectl?(comm) comm.sudo("hostnamectl set-hostname --static '#{name}' ; " \ "hostnamectl set-hostname --transient '#{name}'") else comm.sudo("hostname -F /etc/hostname") end restart_command = "service network restart" if systemd?(comm) if systemd_networkd?(comm) restart_command = "systemctl restart systemd-networkd.service" elsif systemd_controlled?(comm, "NetworkManager.service") restart_command = "systemctl restart NetworkManager.service" end end comm.sudo(restart_command) end network_with_hostname = machine.config.vm.networks.map {|_, c| c if c[:hostname] }.compact[0] if network_with_hostname replace_host(comm, name, network_with_hostname[:ip]) else add_hostname_to_loopback_interface(comm, name) end end end end end end ================================================ FILE: plugins/guests/redhat/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "securerandom" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestRedHat module Cap class ConfigureNetworks include Vagrant::Util extend Vagrant::Util::GuestInspection::Linux extend Vagrant::Util::GuestNetworks::Linux def self.configure_networks(machine, networks) @logger = Log4r::Logger.new("vagrant::guest::redhat::configurenetworks") # Start with the scripts directory to determine how to configure network_scripts_dir = machine.guest.capability(:network_scripts_dir) @logger.debug("guest network scripts directory: #{network_scripts_dir}") # The legacy configuration will handle rhel/centos pre-10 # versions. The newer versions have a different path for # network configuration files. if network_scripts_dir.end_with?("network-scripts") configure_networks_legacy(machine, networks) else # Recent versions use Network Manager configure_network_manager(machine, networks) end end def self.configure_networks_legacy(machine, networks) comm = machine.communicate network_scripts_dir = machine.guest.capability(:network_scripts_dir) commands = {:start => [], :middle => [], :end => []} interfaces = machine.guest.capability(:network_interfaces) # Check if NetworkManager is installed on the system nmcli_installed = nmcli?(comm) net_configs = machine.config.vm.networks.map do |type, opts| opts if type.to_s.end_with?("_network") end.compact networks.each.with_index do |network, i| network[:device] = interfaces[network[:interface]] extra_opts = net_configs[i] ? net_configs[i].dup : {} if nmcli_installed # Now check if the device is actively being managed by NetworkManager nm_controlled = nm_controlled?(comm, network[:device]) end if !extra_opts.key?(:nm_controlled) extra_opts[:nm_controlled] = !!nm_controlled end extra_opts[:nm_controlled] = case extra_opts[:nm_controlled] when true "yes" when false, nil "no" else extra_opts[:nm_controlled].to_s end if extra_opts[:nm_controlled] == "yes" && !nmcli_installed raise Vagrant::Errors::NetworkManagerNotInstalled, device: network[:device] end # Render a new configuration entry = TemplateRenderer.render("guests/redhat/network_#{network[:type]}", options: extra_opts.merge(network), ) # Upload the new configuration remote_path = "/tmp/vagrant-network-entry-#{network[:device]}-#{Time.now.to_i}-#{i}" Tempfile.open("vagrant-redhat-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close machine.communicate.upload(f.path, remote_path) end # Add the new interface and bring it back up final_path = "#{network_scripts_dir}/ifcfg-#{network[:device]}" if nm_controlled commands[:start] << "nmcli d disconnect iface '#{network[:device]}'" else commands[:start] << "/sbin/ifdown '#{network[:device]}'" end commands[:middle] << "mv -f '#{remote_path}' '#{final_path}'" if extra_opts[:nm_controlled] == "no" commands[:end] << "/sbin/ifup '#{network[:device]}'" end end if nmcli_installed commands[:middle] << "(test -f /etc/init.d/NetworkManager && /etc/init.d/NetworkManager restart) || " \ "((systemctl | grep NetworkManager.service) && systemctl restart NetworkManager)" end commands = commands[:start] + commands[:middle] + commands[:end] comm.sudo(commands.join("\n")) comm.wait_for_ready(5) end end end end end ================================================ FILE: plugins/guests/redhat/cap/flavor.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestRedHat module Cap class Flavor def self.flavor(machine) # Pick up version info from `/etc/os-release`. This file started to exist # in RHEL 7. For versions before that (i.e. RHEL 6) just plain `:rhel` # should do. version = nil if machine.communicate.test("test -f /etc/os-release") begin machine.communicate.execute("source /etc/os-release && printf $VERSION_ID") do |type, data| if type == :stdout version = data.split(".").first.to_i end end rescue end end if version.nil? || version < 1 return :rhel else return "rhel_#{version}".to_sym end end end end end end ================================================ FILE: plugins/guests/redhat/cap/network_scripts_dir.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestRedHat module Cap class NetworkScriptsDir def self.network_scripts_dir(machine) if machine.communicate.test("test -d /etc/sysconfig/network-scripts") "/etc/sysconfig/network-scripts" else "/etc/NetworkManager/system-connections" end end end end end end ================================================ FILE: plugins/guests/redhat/cap/nfs_client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestRedHat module Cap class NFSClient def self.nfs_client_install(machine) machine.communicate.sudo <<-EOH.gsub(/^ {12}/, '') if command -v dnf; then if `dnf info -q libnfs-utils > /dev/null 2>&1` ; then dnf -y install nfs-utils libnfs-utils portmap elif `dnf info -q nfs-utils-lib > /dev/null 2>&1` ; then dnf -y install nfs-utils nfs-utils-lib portmap else dnf -y install nfs-utils portmap fi else yum -y install nfs-utils nfs-utils-lib portmap fi if test $(ps -o comm= 1) == 'systemd'; then /bin/systemctl restart rpcbind nfs-server else /etc/init.d/rpcbind restart /etc/init.d/nfs restart fi EOH end end end end end ================================================ FILE: plugins/guests/redhat/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestRedHat module Cap class RSync def self.rsync_install(machine) machine.communicate.sudo <<-EOH.gsub(/^ {12}/, '') if command -v dnf; then dnf -y install rsync else yum -y install rsync fi EOH end end end end end ================================================ FILE: plugins/guests/redhat/cap/smb.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestRedHat module Cap class SMB def self.smb_install(machine) comm = machine.communicate if !comm.test("test -f /sbin/mount.cifs") comm.sudo <<-EOH.gsub(/^ {14}/, '') if command -v dnf; then dnf -y install cifs-utils else yum -y install cifs-utils fi EOH end end end end end end ================================================ FILE: plugins/guests/redhat/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestRedHat class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("cat /etc/redhat-release") end end end end ================================================ FILE: plugins/guests/redhat/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestRedHat class Plugin < Vagrant.plugin("2") name "Red Hat Enterprise Linux guest" description "Red Hat Enterprise Linux guest support." guest(:redhat, :linux) do require_relative "guest" Guest end guest_capability(:redhat, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:redhat, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:redhat, :flavor) do require_relative "cap/flavor" Cap::Flavor end guest_capability(:redhat, :network_scripts_dir) do require_relative "cap/network_scripts_dir" Cap::NetworkScriptsDir end guest_capability(:redhat, :nfs_client_install) do require_relative "cap/nfs_client" Cap::NFSClient end guest_capability(:redhat, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:redhat, :smb_install) do require_relative "cap/smb" Cap::SMB end end end end ================================================ FILE: plugins/guests/rocky/cap/flavor.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestRocky module Cap class Flavor def self.flavor(machine) # Read the version file version = "" machine.communicate.sudo("source /etc/os-release && printf $VERSION_ID") do |type, data| if type == :stdout version = data.split(".").first.to_i end end if version.nil? || version < 1 :rocky else "rocky_#{version}".to_sym end end end end end end ================================================ FILE: plugins/guests/rocky/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../linux/guest" module VagrantPlugins module GuestRocky class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "rocky".freeze end end end ================================================ FILE: plugins/guests/rocky/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestRocky class Plugin < Vagrant.plugin("2") name "Rocky guest" description "Rocky guest support." guest(:rocky, :redhat) do require_relative "guest" Guest end guest_capability(:rocky, :flavor) do require_relative "cap/flavor" Cap::Flavor end end end end ================================================ FILE: plugins/guests/slackware/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../../linux/cap/change_host_name' module VagrantPlugins module GuestSlackware module Cap class ChangeHostName extend VagrantPlugins::GuestLinux::Cap::ChangeHostName def self.change_name_command(name) return <<-EOH.gsub(/^ {14}/, "") # Set the hostname chmod o+w /etc/hostname echo '#{name}' > /etc/hostname chmod o-w /etc/hostname hostname -F /etc/hostname EOH end end end end end ================================================ FILE: plugins/guests/slackware/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestSlackware module Cap class ConfigureNetworks include Vagrant::Util def self.configure_networks(machine, networks) comm = machine.communicate commands = [] interfaces = machine.guest.capability(:network_interfaces) # Remove any previous configuration commands << "sed -i'' -e '/^#VAGRANT-BEGIN/,/^#VAGRANT-END/ d' /etc/rc.d/rc.inet1.conf" networks.each.with_index do |network, i| network[:device] = interfaces[network[:interface]] entry = TemplateRenderer.render("guests/slackware/network_#{network[:type]}", i: i+1, options: network, ) remote_path = "/tmp/vagrant-network-#{network[:device]}-#{Time.now}-#{i}" Tempfile.open("vagrant-slackware-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close comm.upload(f.path, remote_path) end commands << "cat '#{remote_path}' >> /etc/rc.d/rc.inet1.conf" end # Restart networking commands << "/etc/rc.d/rc.inet1" comm.sudo(commands.join("\n")) end end end end end ================================================ FILE: plugins/guests/slackware/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSlackware class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("cat /etc/slackware-version") end end end end ================================================ FILE: plugins/guests/slackware/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestSlackware class Plugin < Vagrant.plugin("2") name "Slackware guest" description "Slackware guest support." guest(:slackware, :linux) do require_relative "guest" 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 ================================================ FILE: plugins/guests/smartos/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSmartos module Cap class ChangeHostName def self.change_host_name(machine, name) sudo = machine.config.smartos.suexec_cmd machine.communicate.tap do |comm| comm.execute <<-EOH.sub(/^ */, '') if hostname | grep '#{name}' ; then exit 0 fi if [ -d /usbkey ] && [ "$(zonename)" == "global" ] ; then #{sudo} sed -i '' 's/hostname=.*/hostname=#{name}/' /usbkey/config fi #{sudo} echo '#{name}' > /etc/nodename #{sudo} hostname #{name} EOH end end end end end end ================================================ FILE: plugins/guests/smartos/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSmartos module Cap class ConfigureNetworks def self.configure_networks(machine, networks) su_cmd = machine.config.smartos.suexec_cmd networks.each do |network| device = "#{machine.config.smartos.device}#{network[:interface]}" ifconfig_cmd = "#{su_cmd} /sbin/ifconfig #{device}" machine.communicate.execute("#{ifconfig_cmd} plumb") if network[:type].to_sym == :static machine.communicate.execute("#{ifconfig_cmd} inet #{network[:ip]} netmask #{network[:netmask]}") machine.communicate.execute("#{ifconfig_cmd} up") machine.communicate.execute("#{su_cmd} sh -c \"echo '#{network[:ip]}' > /etc/hostname.#{device}\"") elsif network[:type].to_sym == :dhcp machine.communicate.execute("#{ifconfig_cmd} dhcp start") end end end end end end end ================================================ FILE: plugins/guests/smartos/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSmartos module Cap class Halt def self.halt(machine) # There should be an exception raised if the line # # vagrant::::profiles=Primary Administrator # # does not exist in /etc/user_attr. TODO begin machine.communicate.execute( "#{machine.config.smartos.suexec_cmd} /usr/sbin/poweroff") rescue IOError, Vagrant::Errors::SSHDisconnected # Ignore, this probably means connection closed because it # shut down. end end end end end end ================================================ FILE: plugins/guests/smartos/cap/insert_public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/shell_quote" module VagrantPlugins module GuestSmartos module Cap class InsertPublicKey def self.insert_public_key(machine, contents) contents = contents.chomp contents = Vagrant::Util::ShellQuote.escape(contents, "'") machine.communicate.tap do |comm| comm.execute <<-EOH.sub(/^ */, '') if [ -d /usbkey ] && [ "$(zonename)" == "global" ] ; then printf '#{contents}\\n' >> /usbkey/config.inc/authorized_keys cp /usbkey/config.inc/authorized_keys ~/.ssh/authorized_keys else mkdir -p ~/.ssh chmod 0700 ~/.ssh printf '#{contents}\\n' >> ~/.ssh/authorized_keys chmod 0600 ~/.ssh/authorized_keys fi EOH end end end end end end ================================================ FILE: plugins/guests/smartos/cap/mount_nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSmartos module Cap class MountNFS def self.mount_nfs_folder(machine, ip, folders) sudo = machine.config.smartos.suexec_cmd folders.each do |name, opts| machine.communicate.tap do |comm| nfsDescription = "#{ip}:#{opts[:hostpath]}:#{opts[:guestpath]}" comm.execute <<-EOH.sub(/^ */, '') if [ -d /usbkey ] && [ "$(zonename)" == "global" ] ; then #{sudo} mkdir -p /usbkey/config.inc printf '#{nfsDescription}\\n' | #{sudo} tee -a /usbkey/config.inc/nfs_mounts fi #{sudo} mkdir -p #{opts[:guestpath]} #{sudo} /usr/sbin/mount -F nfs '#{ip}:#{opts[:hostpath]}' '#{opts[:guestpath]}' EOH end end end end end end end ================================================ FILE: plugins/guests/smartos/cap/remove_public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/shell_quote" module VagrantPlugins module GuestSmartos module Cap class RemovePublicKey def self.remove_public_key(machine, contents) contents = contents.chomp contents = Vagrant::Util::ShellQuote.escape(contents, "'") machine.communicate.tap do |comm| comm.execute <<-EOH.sub(/^ */, '') if test -f /usbkey/config.inc/authorized_keys ; then sed -i '' '/^.*#{contents}.*$/d' /usbkey/config.inc/authorized_keys fi if test -f ~/.ssh/authorized_keys ; then sed -i '' '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys fi EOH end end end end end end ================================================ FILE: plugins/guests/smartos/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/rsync/default_unix_cap" module VagrantPlugins module GuestSmartos module Cap class RSync extend VagrantPlugins::SyncedFolderRSync::DefaultUnixCap def self.rsync_installed(machine) machine.communicate.test("which rsync") end def self.rsync_command(machine) "#{machine.config.smartos.suexec_cmd} rsync" end def self.rsync_pre(machine, opts) machine.communicate.tap do |comm| comm.execute("#{machine.config.smartos.suexec_cmd} mkdir -p '#{opts[:guestpath]}'") end end def self.rsync_post(machine, opts) if opts.key?(:chown) && !opts[:chown] return end suexec_cmd = machine.config.smartos.suexec_cmd machine.communicate.execute("#{suexec_cmd} #{build_rsync_chown(opts)}") end end end end end ================================================ FILE: plugins/guests/smartos/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSmartos class Config < Vagrant.plugin("2", :config) # This sets the command to use to execute items as a superuser. # @default sudo attr_accessor :suexec_cmd attr_accessor :device def initialize @suexec_cmd = 'pfexec' @device = "e1000g" end end end end ================================================ FILE: plugins/guests/smartos/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestSmartos class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("cat /etc/release | grep -i SmartOS") end end end end ================================================ FILE: plugins/guests/smartos/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestSmartos class Plugin < Vagrant.plugin("2") name "SmartOS guest." description "SmartOS guest support." config(:smartos) do require_relative "config" Config end guest(:smartos, :solaris) do require_relative "guest" Guest end guest_capability(:smartos, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:smartos, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:smartos, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:smartos, :insert_public_key) do require_relative "cap/insert_public_key" Cap::InsertPublicKey end guest_capability(:smartos, :mount_nfs_folder) do require_relative "cap/mount_nfs" Cap::MountNFS end guest_capability(:smartos, :remove_public_key) do require_relative "cap/remove_public_key" Cap::RemovePublicKey end guest_capability(:smartos, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end guest_capability(:smartos, :rsync_command) do require_relative "cap/rsync" Cap::RSync end guest_capability(:smartos, :rsync_post) do require_relative "cap/rsync" Cap::RSync end guest_capability(:smartos, :rsync_pre) do require_relative "cap/rsync" Cap::RSync end end end end ================================================ FILE: plugins/guests/solaris/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSolaris module Cap class ChangeHostName def self.change_host_name(machine, name) su_cmd = machine.config.solaris.suexec_cmd # Only do this if the hostname is not already set if !machine.communicate.test("#{su_cmd} hostname | grep '#{name}'") machine.communicate.execute("#{su_cmd} sh -c \"echo '#{name}' > /etc/nodename\"") machine.communicate.execute("#{su_cmd} uname -S #{name}") end end end end end end ================================================ FILE: plugins/guests/solaris/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSolaris module Cap class ConfigureNetworks def self.configure_networks(machine, networks) networks.each do |network| device = "#{machine.config.solaris.device}#{network[:interface]}" su_cmd = machine.config.solaris.suexec_cmd ifconfig_cmd = "#{su_cmd} /sbin/ifconfig #{device}" machine.communicate.execute("#{ifconfig_cmd} plumb") if network[:type].to_sym == :static machine.communicate.execute("#{ifconfig_cmd} inet #{network[:ip]} netmask #{network[:netmask]}") machine.communicate.execute("#{ifconfig_cmd} up") machine.communicate.execute("#{su_cmd} sh -c \"echo '#{network[:ip]}' > /etc/hostname.#{device}\"") elsif network[:type].to_sym == :dhcp machine.communicate.execute("#{ifconfig_cmd} dhcp start") end end end end end end end ================================================ FILE: plugins/guests/solaris/cap/file_system.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSolaris module Cap class FileSystem # Create a temporary file or directory on the guest # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [Hash] opts Path options # @return [String] path to temporary file or directory def self.create_tmp_path(machine, opts) template = "vagrant-XXXXXX" cmd = ["mktemp"] if opts[:type] == :directory cmd << "-d" end cmd << "-t" cmd << template tmp_path = "" machine.communicate.execute(cmd.join(" ")) do |type, data| if type == :stdout tmp_path << data end end tmp_path.strip end # Decompress tgz file on guest to given location # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [String] compressed_file Path to compressed file on guest # @param [String] destination Path for decompressed files on guest def self.decompress_tgz(machine, compressed_file, destination, opts={}) comm = machine.communicate extract_dir = create_tmp_path(machine, type: :directory) cmds = [] if opts[:type] == :directory cmds << "mkdir -p '#{destination}'" else cmds << "mkdir -p '#{File.dirname(destination)}'" end cmds += [ "tar xzf '#{compressed_file}' -C '#{extract_dir}'", "mv '#{extract_dir}'/* '#{destination}'", "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] cmds.each{ |cmd| comm.execute(cmd) } true end # Decompress zip file on guest to given location # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [String] compressed_file Path to compressed file on guest # @param [String] destination Path for decompressed files on guest def self.decompress_zip(machine, compressed_file, destination, opts={}) comm = machine.communicate extract_dir = create_tmp_path(machine, type: :directory) cmds = [] if opts[:type] == :directory cmds << "mkdir -p '#{destination}'" else cmds << "mkdir -p '#{File.dirname(destination)}'" end cmds += [ "unzip '#{compressed_file}' -d '#{extract_dir}'", "mv '#{extract_dir}'/* '#{destination}'", "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] cmds.each{ |cmd| comm.execute(cmd) } true end end end end end ================================================ FILE: plugins/guests/solaris/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSolaris module Cap class Halt def self.halt(machine) # There should be an exception raised if the line # # vagrant::::profiles=Primary Administrator # # does not exist in /etc/user_attr. TODO begin machine.communicate.execute( "#{machine.config.solaris.suexec_cmd} /usr/sbin/shutdown -y -i5 -g0") rescue IOError, Vagrant::Errors::SSHDisconnected # Ignore, this probably means connection closed because it # shut down. end end end end end end ================================================ FILE: plugins/guests/solaris/cap/insert_public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/shell_quote" module VagrantPlugins module GuestSolaris module Cap class InsertPublicKey def self.insert_public_key(machine, contents) # TODO: Code is identical to linux/cap/insert_public_key contents = contents.chomp contents = Vagrant::Util::ShellQuote.escape(contents, "'") machine.communicate.tap do |comm| comm.execute("mkdir -p ~/.ssh") comm.execute("chmod 0700 ~/.ssh") comm.execute("printf '#{contents}\\n' >> ~/.ssh/authorized_keys") comm.execute("chmod 0600 ~/.ssh/authorized_keys") end end end end end end ================================================ FILE: plugins/guests/solaris/cap/mount_virtualbox_shared_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSolaris module Cap class MountVirtualBoxSharedFolder def self.mount_virtualbox_shared_folder(machine, name, guestpath, options) # These are just far easier to use than the full options syntax owner = options[:owner] group = options[:group] # Create the shared folder machine.communicate.execute("#{machine.config.solaris.suexec_cmd} mkdir -p #{guestpath}") if owner.is_a? Integer mount_uid = owner else # We have to use this `id` command instead of `/usr/bin/id` since this # one accepts the "-u" and "-g" flags. mount_uid = "`/usr/xpg4/bin/id -u #{owner}`" end if group.is_a? Integer mount_gid = group else mount_gid = "`/usr/xpg4/bin/id -g #{group}`" end # Mount the folder with the proper owner/group mount_options = "-o uid=#{mount_uid},gid=#{mount_gid}" if options[:mount_options] mount_options += ",#{options[:mount_options].join(",")}" end machine.communicate.execute("#{machine.config.solaris.suexec_cmd} /sbin/mount -F vboxfs #{mount_options} #{name} #{guestpath}") # chown the folder to the proper owner/group machine.communicate.execute("#{machine.config.solaris.suexec_cmd} chown #{mount_uid}:#{mount_gid} #{guestpath}") end end end end end ================================================ FILE: plugins/guests/solaris/cap/remove_public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/shell_quote" module VagrantPlugins module GuestSolaris module Cap class RemovePublicKey def self.remove_public_key(machine, contents) # "sed -i" is specific to GNU sed and is not a posix standard option contents = contents.chomp contents = Vagrant::Util::ShellQuote.escape(contents, "'") machine.communicate.tap do |comm| if comm.test("test -f ~/.ssh/authorized_keys") comm.execute( "cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.temp && sed '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys.temp > ~/.ssh/authorized_keys && rm ~/.ssh/authorized_keys.temp") end end end end end end end ================================================ FILE: plugins/guests/solaris/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/rsync/default_unix_cap" module VagrantPlugins module GuestSolaris module Cap class RSync extend VagrantPlugins::SyncedFolderRSync::DefaultUnixCap def self.rsync_installed(machine) machine.communicate.test("which rsync") end def self.rsync_command(machine) "#{machine.config.solaris.suexec_cmd} rsync" end def self.rsync_pre(machine, opts) machine.communicate.tap do |comm| comm.sudo("mkdir -p '#{opts[:guestpath]}'") end end def self.rsync_post(machine, opts) if opts.key?(:chown) && !opts[:chown] return end suexec_cmd = machine.config.solaris.suexec_cmd machine.communicate.execute("#{suexec_cmd} #{build_rsync_chown(opts)}") end end end end end ================================================ FILE: plugins/guests/solaris/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSolaris class Config < Vagrant.plugin("2", :config) attr_accessor :halt_timeout attr_accessor :halt_check_interval attr_accessor :suexec_cmd attr_accessor :device def initialize @halt_timeout = UNSET_VALUE @halt_check_interval = UNSET_VALUE @suexec_cmd = UNSET_VALUE @device = UNSET_VALUE end def finalize! if @halt_timeout != UNSET_VALUE puts "solaris.halt_timeout is deprecated and will be removed in Vagrant 1.7" end if @halt_check_interval != UNSET_VALUE puts "solaris.halt_check_interval is deprecated and will be removed in Vagrant 1.7" end @suexec_cmd = "sudo" if @suexec_cmd == UNSET_VALUE @device = "e1000g" if @device == UNSET_VALUE end end end end ================================================ FILE: plugins/guests/solaris/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestSolaris # A general Vagrant system implementation for "solaris". # # Contributed by Blake Irvin class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("uname -sr | grep SunOS | grep -v 5.11") end end end end ================================================ FILE: plugins/guests/solaris/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestSolaris class Plugin < Vagrant.plugin("2") name "Solaris guest." description "Solaris guest support." config(:solaris) do require_relative "config" Config end guest(:solaris) do require_relative "guest" Guest end guest_capability(:solaris, :create_tmp_path) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:solaris, :decompress_tgz) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:solaris, :decompress_zip) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:solaris, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:solaris, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:solaris, :insert_public_key) do require_relative "cap/insert_public_key" Cap::InsertPublicKey end guest_capability(:solaris, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:solaris, :mount_virtualbox_shared_folder) do require_relative "cap/mount_virtualbox_shared_folder" Cap::MountVirtualBoxSharedFolder end guest_capability(:solaris, :remove_public_key) do require_relative "cap/remove_public_key" Cap::RemovePublicKey end guest_capability(:solaris, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end guest_capability(:solaris, :rsync_command) do require_relative "cap/rsync" Cap::RSync end guest_capability(:solaris, :rsync_post) do require_relative "cap/rsync" Cap::RSync end guest_capability(:solaris, :rsync_pre) do require_relative "cap/rsync" Cap::RSync end end end end ================================================ FILE: plugins/guests/solaris11/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # A general Vagrant system implementation for "solaris 11". # # Contributed by Jan Thomas Moldung module VagrantPlugins module GuestSolaris11 module Cap class ChangeHostName def self.change_host_name(machine, name) su_cmd = machine.config.solaris11.suexec_cmd # Only do this if the hostname is not already set if !machine.communicate.test("/usr/sbin/svccfg -s system/identity:node listprop config/nodename | /usr/bin/grep '#{name}'") machine.communicate.execute("#{su_cmd} /usr/sbin/svccfg -s system/identity:node setprop config/nodename=\"#{name}\"") machine.communicate.execute("#{su_cmd} /usr/sbin/svccfg -s system/identity:node setprop config/loopback=\"#{name}\"") machine.communicate.execute("#{su_cmd} /usr/sbin/svccfg -s system/identity:node refresh ") machine.communicate.execute("#{su_cmd} /usr/sbin/svcadm restart system/identity:node ") end end end end end end ================================================ FILE: plugins/guests/solaris11/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # A general Vagrant system implementation for "solaris 11". # # Contributed by Jan Thomas Moldung module VagrantPlugins module GuestSolaris11 module Cap class ConfigureNetworks def self.configure_networks(machine, networks) networks.each do |network| device = "#{machine.config.solaris11.device}#{network[:interface]}" su_cmd = machine.config.solaris11.suexec_cmd mask = "#{network[:netmask]}" cidr = mask.split(".").map { |e| e.to_i.to_s(2).rjust(8, "0") }.join.count("1").to_s if network[:type].to_sym == :static unless machine.communicate.test("ipadm show-if #{device}") machine.communicate.execute("#{su_cmd} ipadm create-ip #{device}") end if machine.communicate.test("ipadm | grep #{device}/v4") machine.communicate.execute("#{su_cmd} ipadm delete-addr #{device}/v4") end machine.communicate.execute("#{su_cmd} ipadm create-addr -T static -a #{network[:ip]}/#{cidr} #{device}/v4") elsif network[:type].to_sym == :dhcp if machine.communicate.test("ipadm show-if -o all | grep #{device} | tr -s ' ' | cut -d ' ' -f 6 | grep '4\|6'") machine.communicate.execute("#{su_cmd} ipadm create-addr -T addrconf #{device}/v4") end end end end end end end end ================================================ FILE: plugins/guests/solaris11/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # A general Vagrant system implementation for "solaris 11". # # Contributed by Jan Thomas Moldung module VagrantPlugins module GuestSolaris11 class Config < Vagrant.plugin("2", :config) attr_accessor :halt_timeout attr_accessor :halt_check_interval # This sets the command to use to execute items as a superuser. sudo is default attr_accessor :suexec_cmd attr_accessor :device def initialize @halt_timeout = UNSET_VALUE @halt_check_interval = UNSET_VALUE @suexec_cmd = UNSET_VALUE @device = UNSET_VALUE end def finalize! if @halt_timeout != UNSET_VALUE puts "solaris11.halt_timeout is deprecated and will be removed in Vagrant 1.7" end if @halt_check_interval != UNSET_VALUE puts "solaris11.halt_check_interval is deprecated and will be removed in Vagrant 1.7" end @suexec_cmd = "sudo" if @suexec_cmd == UNSET_VALUE @device = "net" if @device == UNSET_VALUE end end end end ================================================ FILE: plugins/guests/solaris11/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # A general Vagrant system implementation for "solaris 11". # # Contributed by Jan Thomas Moldung require "vagrant" module VagrantPlugins module GuestSolaris11 class Guest < Vagrant.plugin("2", :guest) def detect?(machine) success = machine.communicate.test("grep 'Solaris 11' /etc/release") return success if success # for solaris derived guests like openindiana machine.communicate.test("uname -sr | grep 'SunOS 5.11'") end end end end ================================================ FILE: plugins/guests/solaris11/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # A general Vagrant system implementation for "solaris 11". # # Contributed by Jan Thomas Moldung require "vagrant" module VagrantPlugins module GuestSolaris11 class Plugin < Vagrant.plugin("2") name "Solaris 11 guest." description "Solaris 11 guest support." guest(:solaris11, :solaris) do require_relative "guest" Guest end config(:solaris11) do require_relative "config" Config end guest_capability(:solaris11, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:solaris11, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:solaris11, :shell_expand_guest_path) do require_relative "../linux/cap/shell_expand_guest_path" VagrantPlugins::GuestLinux::Cap::ShellExpandGuestPath end end end end ================================================ FILE: plugins/guests/suse/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/guest_hosts' require 'vagrant/util/guest_inspection' module VagrantPlugins module GuestSUSE module Cap class ChangeHostName extend Vagrant::Util::GuestInspection::Linux extend Vagrant::Util::GuestHosts::Linux def self.change_host_name(machine, name) comm = machine.communicate basename = name.split(".", 2)[0] network_with_hostname = machine.config.vm.networks.map {|_, c| c if c[:hostname] }.compact[0] if network_with_hostname replace_host(comm, name, network_with_hostname[:ip]) else add_hostname_to_loopback_interface(comm, name) end if hostnamectl?(comm) if !comm.test("test \"$(hostnamectl --static status)\" = \"#{basename}\"", sudo: false) cmd = <<-EOH.gsub(/^ {14}/, "") hostnamectl set-hostname '#{basename}' echo #{name} > /etc/HOSTNAME EOH comm.sudo(cmd) end else if !comm.test("hostname -f | grep '^#{name}$'", sudo: false) cmd = <<-EOH.gsub(/^ {14}/, "") echo #{name} > /etc/HOSTNAME hostname '#{basename}' EOH comm.sudo(cmd) end end end end end end end ================================================ FILE: plugins/guests/suse/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/template_renderer" module VagrantPlugins module GuestSUSE module Cap class ConfigureNetworks extend Vagrant::Util::Retryable include Vagrant::Util def self.configure_networks(machine, networks) comm = machine.communicate network_scripts_dir = machine.guest.capability(:network_scripts_dir) commands = [] interfaces = machine.guest.capability(:network_interfaces) networks.each.with_index do |network, i| network[:device] = interfaces[network[:interface]] entry = TemplateRenderer.render("guests/suse/network_#{network[:type]}", options: network, ) remote_path = "/tmp/vagrant-network-#{network[:device]}-#{Time.now.to_i}-#{i}" Tempfile.open("vagrant-suse-configure-networks") do |f| f.binmode f.write(entry) f.fsync f.close comm.upload(f.path, remote_path) end local_path = "#{network_scripts_dir}/ifcfg-#{network[:device]}" commands << <<-EOH.gsub(/^ {14}/, '') /sbin/ifdown '#{network[:device]}' || true mv '#{remote_path}' '#{local_path}' /sbin/ifup '#{network[:device]}' EOH end comm.sudo(commands.join("\n")) end end end end end ================================================ FILE: plugins/guests/suse/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSUSE module Cap class Halt def self.halt(machine) begin if machine.communicate.test("test -e /usr/bin/systemctl") machine.communicate.sudo("/usr/bin/systemctl poweroff &") else machine.communicate.sudo("/sbin/shutdown -h now &") end rescue IOError, Vagrant::Errors::SSHDisconnected # Do nothing, because it probably means the machine shut down # and SSH connection was lost. end end end end end end ================================================ FILE: plugins/guests/suse/cap/network_scripts_dir.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSUSE module Cap class NetworkScriptsDir def self.network_scripts_dir(machine) "/etc/sysconfig/network" end end end end end ================================================ FILE: plugins/guests/suse/cap/nfs_client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSUSE module Cap class NFSClient def self.nfs_client_install(machine) machine.communicate.sudo <<-EOH.gsub(/^ {12}/, '') zypper -n install nfs-client /usr/bin/systemctl restart rpcbind /usr/bin/systemctl restart nfs-client.target EOH end end end end end ================================================ FILE: plugins/guests/suse/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSUSE module Cap class RSync def self.rsync_installed(machine) machine.communicate.test("test -f /usr/bin/rsync") end def self.rsync_install(machine) machine.communicate.sudo("zypper -n install rsync") end end end end end ================================================ FILE: plugins/guests/suse/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestSUSE class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("test -f /etc/SuSE-release || grep -q SUSE /etc/os-release") end end end end ================================================ FILE: plugins/guests/suse/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestSUSE class Plugin < Vagrant.plugin("2") name "SUSE guest" description "SUSE guest support." guest(:suse, :linux) do require_relative "guest" Guest end guest_capability(:suse, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:suse, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:suse, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:suse, :network_scripts_dir) do require_relative "cap/network_scripts_dir" Cap::NetworkScriptsDir end guest_capability(:suse, :nfs_client_install) do require_relative "cap/nfs_client" Cap::NFSClient end guest_capability(:suse, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:suse, :rsync_installed) do require_relative "cap/rsync" Cap::RSync end end end end ================================================ FILE: plugins/guests/tinycore/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestTinyCore module Cap class ChangeHostName def self.change_host_name(machine, name) if !machine.communicate.test("hostname | grep '^#{name}$'") machine.communicate.sudo("/usr/bin/sethostname #{name}") end end end end end end ================================================ FILE: plugins/guests/tinycore/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ipaddr" module VagrantPlugins module GuestTinyCore module Cap class ConfigureNetworks def self.configure_networks(machine, networks) machine.communicate.tap do |comm| networks.each do |n| if n[:type] == :dhcp comm.sudo("/sbin/udhcpc -b -i eth#{n[:interface]} -p /var/run/udhcpc.eth#{n[:interface]}.pid") return end ifc = "/sbin/ifconfig eth#{n[:interface]}" broadcast = (IPAddr.new(n[:ip]) | (~ IPAddr.new(n[:netmask]))).to_s comm.sudo("#{ifc} down") comm.sudo("#{ifc} #{n[:ip]} netmask #{n[:netmask]} broadcast #{broadcast}") comm.sudo("#{ifc} up") end end end end end end end ================================================ FILE: plugins/guests/tinycore/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestTinyCore module Cap class Halt def self.halt(machine) begin machine.communicate.sudo("poweroff") rescue IOError, Vagrant::Errors::SSHDisconnected # Do nothing, because it probably means the machine shut down # and SSH connection was lost. end end end end end end ================================================ FILE: plugins/guests/tinycore/cap/mount_nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module GuestTinyCore module Cap class MountNFS extend SyncedFolder::UnixMountHelpers def self.mount_nfs_folder(machine, ip, folders) folders.each do |name, opts| # Expand the guest path so we can handle things like "~/vagrant" expanded_guest_path = machine.guest.capability( :shell_expand_guest_path, opts[:guestpath]) # Do the actual creating and mounting machine.communicate.sudo("mkdir -p #{expanded_guest_path}") # Mount hostpath = opts[:hostpath].dup hostpath.gsub!("'", "'\\\\''") # Figure out any options mount_opts = ["vers=#{opts[:nfs_version]}"] mount_opts << "udp" if opts[:nfs_udp] if opts[:mount_options] mount_opts = opts[:mount_options].dup end mount_command = "mount.nfs -o '#{mount_opts.join(",")}' #{ip}:'#{hostpath}' #{expanded_guest_path}" retryable(on: Vagrant::Errors::NFSMountFailed, tries: 8, sleep: 3) do machine.communicate.sudo(mount_command, error_class: Vagrant::Errors::NFSMountFailed) end emit_upstart_notification(machine, expanded_guest_path) end end end end end end ================================================ FILE: plugins/guests/tinycore/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestTinyCore module Cap class RSync def self.rsync_install(machine) machine.communicate.tap do |comm| # Run it but don't error check because this is always failing currently comm.execute("tce-load -wi acl attr rsync", error_check: false) # Verify it by executing rsync comm.execute("rsync --help") end end end end end end ================================================ FILE: plugins/guests/tinycore/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../linux/guest' module VagrantPlugins module GuestTinyCore class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "Core Linux".freeze end end end ================================================ FILE: plugins/guests/tinycore/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestTinyCore class Plugin < Vagrant.plugin("2") name "TinyCore Linux guest." description "TinyCore Linux guest support." guest(:tinycore, :linux) do require_relative "guest" Guest end guest_capability(:tinycore, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:tinycore, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:tinycore, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:tinycore, :rsync_install) do require_relative "cap/rsync" Cap::RSync end guest_capability(:tinycore, :mount_nfs_folder) do require_relative "cap/mount_nfs" Cap::MountNFS end end end end ================================================ FILE: plugins/guests/trisquel/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestTrisquel class Guest < Vagrant.plugin("2", :guest) def detect?(machine) machine.communicate.test("[ -x /usr/bin/lsb_release ] && /usr/bin/lsb_release -i 2>/dev/null | grep Trisquel") end end end end ================================================ FILE: plugins/guests/trisquel/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestTrisquel class Plugin < Vagrant.plugin("2") name "Trisquel guest" description "Trisquel guest support." guest(:trisquel, :ubuntu) do require_relative "guest" Guest end end end end ================================================ FILE: plugins/guests/ubuntu/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../linux/guest' module VagrantPlugins module GuestUbuntu class Guest < VagrantPlugins::GuestLinux::Guest # Name used for guest detection GUEST_DETECTION_NAME = "ubuntu".freeze end end end ================================================ FILE: plugins/guests/ubuntu/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestUbuntu class Plugin < Vagrant.plugin("2") name "Ubuntu guest" description "Ubuntu guest support." guest(:ubuntu, :debian) do require_relative "guest" Guest end end end end ================================================ FILE: plugins/guests/windows/cap/change_host_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module GuestWindows module Cap module ChangeHostName def self.change_host_name(machine, name) change_host_name_and_wait(machine, name, machine.config.vm.graceful_halt_timeout) end def self.change_host_name_and_wait(machine, name, sleep_timeout) # If the configured name matches the current name, then bail # We cannot use %ComputerName% because it truncates at 15 chars return if machine.communicate.test("if ([System.Net.Dns]::GetHostName() -eq '#{name}') { exit 0 } exit 1") # Rename and reboot host if rename succeeded script = <<-EOH $computer = Get-WmiObject -Class Win32_ComputerSystem $retval = $computer.rename("#{name}").returnvalue exit $retval EOH machine.communicate.execute( script, error_class: Errors::RenameComputerFailed, error_key: :rename_computer_failed) machine.guest.capability(:reboot) end end end end end ================================================ FILE: plugins/guests/windows/cap/choose_addressable_ip_addr.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestWindows module Cap module ChooseAddressableIPAddr def self.choose_addressable_ip_addr(machine, possible) machine.communicate.tap do |comm| possible.each do |ip| command = "ping -n 1 -w 1 #{ip}" if comm.test(command) return ip end end end nil end end end end end ================================================ FILE: plugins/guests/windows/cap/configure_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require_relative "../guest_network" module VagrantPlugins module GuestWindows module Cap module ConfigureNetworks @@logger = Log4r::Logger.new("vagrant::guest::windows::configure_networks") def self.configure_networks(machine, networks) @@logger.debug("Networks: #{networks.inspect}") guest_network = GuestNetwork.new(machine.communicate) if machine.provider_name.to_s.start_with?("vmware") machine.ui.warn("Configuring secondary network adapters through VMware ") machine.ui.warn("on Windows is not yet supported. You will need to manually") machine.ui.warn("configure the network adapter.") else vm_interface_map = create_vm_interface_map(machine, guest_network) networks.each do |network| interface = vm_interface_map[network[:interface]+1] if interface.nil? @@logger.warn("Could not find interface for network #{network.inspect}") next end network_type = network[:type].to_sym if network_type == :static guest_network.configure_static_interface( interface[:index], interface[:net_connection_id], network[:ip], network[:netmask]) elsif network_type == :dhcp guest_network.configure_dhcp_interface( interface[:index], interface[:net_connection_id]) else raise "#{network_type} network type is not supported, try static or dhcp" end end end if machine.config.windows.set_work_network guest_network.set_all_networks_to_work end end def self.create_vm_interface_map(machine, guest_network) if !machine.provider.capability?(:nic_mac_addresses) raise Vagrant::Errors::CantReadMACAddresses, provider: machine.provider_name.to_s end driver_mac_address = machine.provider.capability(:nic_mac_addresses).invert @@logger.debug("mac addresses: #{driver_mac_address.inspect}") vm_interface_map = {} guest_network.network_adapters.each do |nic| @@logger.debug("nic: #{nic.inspect}") naked_mac = nic[:mac_address].gsub(':','') # If the :net_connection_id entry is nil then it is probably a virtual connection # and should be ignored. if driver_mac_address[naked_mac] && !nic[:net_connection_id].nil? vm_interface_map[driver_mac_address[naked_mac]] = { net_connection_id: nic[:net_connection_id], mac_address: naked_mac, interface_index: nic[:interface_index], index: nic[:index] } end end @@logger.debug("vm_interface_map: #{vm_interface_map.inspect}") vm_interface_map end end end end end ================================================ FILE: plugins/guests/windows/cap/file_system.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestWindows module Cap class FileSystem # Create a temporary file or directory on the guest # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [Hash] opts Path options # @return [String] path to temporary file or directory def self.create_tmp_path(machine, opts) comm = machine.communicate path = "" cmd = "Write-Output ([System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), " \ "[System.IO.Path]::GetRandomFileName())) | Out-String -Width 2048" comm.execute(cmd, shell: :powershell) do |type, data| if type == :stdout path << data end end path.strip! if opts[:extension] path << opts[:extension].to_s end if opts[:type] == :directory comm.execute("[System.IO.Directory]::CreateDirectory('#{path}')") end path end # Decompress zip file on guest to given location # # @param [Vagrant::Machine] machine Vagrant guest machine # @param [String] compressed_file Path to compressed file on guest # @param [String] destination Path for decompressed files on guest def self.decompress_zip(machine, compressed_file, destination, opts={}) comm = machine.communicate extract_dir = create_tmp_path(machine, type: :directory) cmds = [] destination = destination.tr("/", "\\") if opts[:type] == :directory cmds << "New-Item -ItemType Directory -Force -Path \"#{destination}\"" else d_parts = destination.split("\\") d_parts.pop parent_dir = d_parts.join("\\") + "\\" cmds << "New-Item -ItemType Directory -Force -Path \"#{parent_dir}\"" end cmd = "$f = \"#{compressed_file}\"; $d = \"#{extract_dir}\"; " cmd << '$s = New-Object -ComObject "Shell.Application"; $z = $s.NameSpace($f); ' cmd << 'foreach($i in $z.items()){ $s.Namespace($d).copyhere($i); }' cmds << cmd cmds += [ "Move-Item -Force -Path \"#{extract_dir}\\*\" -Destination \"#{destination}\\\"", "Remove-Item -Path \"#{compressed_file}\" -Force", "Remove-Item -Path \"#{extract_dir}\" -Recurse -Force" ] cmds.each do |cmd| comm.execute(cmd, shell: :powershell) end true end end end end end ================================================ FILE: plugins/guests/windows/cap/halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestWindows module Cap module Halt def self.halt(machine) # Fix vagrant-windows GH-129, if there's an existing scheduled # reboot cancel it so shutdown succeeds machine.communicate.execute("shutdown -a", error_check: false) # Force shutdown the machine now machine.communicate.execute("shutdown /s /t 1 /c \"Vagrant Halt\" /f /d p:4:1") end end end end end ================================================ FILE: plugins/guests/windows/cap/mount_shared_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/template_renderer" require "base64" module VagrantPlugins module GuestWindows module Cap class MountSharedFolder def self.mount_virtualbox_shared_folder(machine, name, guestpath, options) mount_shared_folder(machine, name, guestpath, "\\\\vboxsvr\\") end def self.mount_vmware_shared_folder(machine, name, guestpath, options) mount_shared_folder(machine, name, guestpath, "\\\\vmware-host\\Shared Folders\\") end def self.mount_parallels_shared_folder(machine, name, guestpath, options) mount_shared_folder(machine, name, guestpath, "\\\\psf\\") end def self.mount_smb_shared_folder(machine, name, guestpath, options) if !options[:smb_password].to_s.empty? # Ensure password is scrubbed Vagrant::Util::CredentialScrubber.sensitive(options[:smb_password]) end machine.communicate.execute("cmdkey /add:#{options[:smb_host]} /user:#{options[:smb_username]} /pass:\"#{options[:smb_password]}\"", {shell: :powershell, elevated: true}) mount_shared_folder(machine, name, guestpath, "\\\\#{options[:smb_host]}\\") end protected def self.mount_shared_folder(machine, name, guestpath, vm_provider_unc_base) name = name.gsub(/[\/\/]/,'_').sub(/^_/, '') path = File.expand_path("../../scripts/mount_volume.ps1", __FILE__) script = Vagrant::Util::TemplateRenderer.render(path, options: { mount_point: guestpath, share_name: name, vm_provider_unc_path: vm_provider_unc_base + name, }) if machine.config.vm.communicator == :winrm || machine.config.vm.communicator == :winssh machine.communicate.execute(script, shell: :powershell) else # Convert script to double byte unicode string then base64 encode # just like PowerShell -EncodedCommand expects. # Suppress the progress stream from leaking to stderr. wrapped_encoded_command = Base64.strict_encode64( "$ProgressPreference='SilentlyContinue'; #{script}; exit $LASTEXITCODE".encode('UTF-16LE', 'UTF-8')) # Execute encoded PowerShell script via OpenSSH shell machine.communicate.execute("powershell.exe -noprofile -executionpolicy bypass " + "-encodedcommand '#{wrapped_encoded_command}'", shell: "sh") end end end end end end ================================================ FILE: plugins/guests/windows/cap/public_key.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative '../../../communicators/winssh/communicator' module VagrantPlugins module GuestWindows module Cap class PublicKey def self.insert_public_key(machine, contents) if machine.communicate.is_a?(CommunicatorWinSSH::Communicator) contents = contents.strip winssh_modify_authorized_keys machine do |keys| if !keys.include?(contents) keys << contents end end else raise Vagrant::Errors::SSHInsertKeyUnsupported end end def self.remove_public_key(machine, contents) if machine.communicate.is_a?(CommunicatorWinSSH::Communicator) winssh_modify_authorized_keys machine do |keys| keys.delete(contents) end else raise Vagrant::Errors::SSHInsertKeyUnsupported end end def self.winssh_modify_authorized_keys(machine) comm = machine.communicate directories = fetch_guest_paths(comm) home_dir = directories[:home] temp_dir = directories[:temp] # Ensure the user's ssh directory exists remote_ssh_dir = "#{home_dir}\\.ssh" comm.execute("New-Item -Path '#{remote_ssh_dir}' -ItemType directory -Force", shell: "powershell") remote_upload_path = "#{temp_dir}\\vagrant-insert-pubkey-#{Time.now.to_i}" remote_authkeys_path = "#{remote_ssh_dir}\\authorized_keys" keys_file = Tempfile.new("vagrant-windows-insert-public-key") keys_file.close # Check if an authorized_keys file already exists result = comm.execute("dir \"#{remote_authkeys_path}\"", shell: "cmd", error_check: false) if result == 0 comm.download(remote_authkeys_path, keys_file.path) keys = File.read(keys_file.path).split(/[\r\n]+/) else keys = [] end yield keys File.write(keys_file.path, keys.join("\r\n") + "\r\n") comm.upload(keys_file.path, remote_upload_path) keys_file.delete comm.execute(<<-EOC.gsub(/^\s*/, ""), shell: "powershell") Set-Acl "#{remote_upload_path}" (Get-Acl "#{remote_authkeys_path}") Move-Item -Force "#{remote_upload_path}" "#{remote_authkeys_path}" EOC end # Fetch user's temporary and home directory paths from the Windows guest # # @param [Communicator] # @return [Hash] {:temp, :home} def self.fetch_guest_paths(communicator) output = "" communicator.execute("Write-Output $env:TEMP\nWrite-Output $env:USERPROFILE", shell: "powershell") do |type, data| if type == :stdout output << data end end temp_dir, home_dir = output.strip.split(/[\r\n]+/) if temp_dir.nil? || home_dir.nil? raise Errors::PublicKeyDirectoryFailure end {temp: temp_dir, home: home_dir} end end end end end ================================================ FILE: plugins/guests/windows/cap/reboot.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module GuestWindows module Cap class Reboot DEFAULT_MAX_REBOOT_RETRY_DURATION = 120 WAIT_SLEEP_TIME = 5 def self.reboot(machine) @logger = Log4r::Logger.new("vagrant::windows::reboot") reboot_script = "shutdown /r /t 5 /f /d p:4:1 /c \"Vagrant Reboot Computer\"" comm = machine.communicate script = File.expand_path("../../scripts/reboot_detect.ps1", __FILE__) script = File.read(script) if comm.test(script, error_check: false, shell: :powershell) @logger.debug("Issuing reboot command for guest") comm.execute(reboot_script, shell: :powershell) else @logger.debug("A reboot is already in progress") end machine.ui.info(I18n.t("vagrant.guests.capabilities.rebooting")) @logger.debug("Waiting for machine to finish rebooting") wait_remaining = ENV.fetch("VAGRANT_MAX_REBOOT_RETRY_DURATION", DEFAULT_MAX_REBOOT_RETRY_DURATION).to_i wait_remaining = DEFAULT_MAX_REBOOT_RETRY_DURATION if wait_remaining < 1 begin wait_for_reboot(machine) rescue => err raise if wait_remaining < 0 @logger.debug("Exception caught while waiting for reboot: #{err}") @logger.warn("Machine not ready, cannot start reboot yet. Trying again") sleep(WAIT_SLEEP_TIME) wait_remaining -= WAIT_SLEEP_TIME retry end end def self.wait_for_reboot(machine) script = File.expand_path("../../scripts/reboot_detect.ps1", __FILE__) script = File.read(script) while machine.guest.ready? && machine.communicate.execute(script, error_check: false, shell: :powershell) != 0 sleep 10 end # This re-establishes our symbolic links if they were # created between now and a reboot machine.communicate.execute("net use", error_check: false, shell: :powershell) end end end end end ================================================ FILE: plugins/guests/windows/cap/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 # NB: Per #11878, the `mkdir` command on Windows is different than used on Unix. # This formulation matches the form used in the WinRM communicator plugin. # This will ignore any -p switches, which are redundant in PowerShell, # and ambiguous in PowerShell 4+ comm.execute("mkdir \"#{opts[:guestpath]}\" -force") end end end end end end ================================================ FILE: plugins/guests/windows/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestWindows class Config < Vagrant.plugin("2", :config) attr_accessor :set_work_network def initialize @set_work_network = UNSET_VALUE end def validate(machine) errors = [] errors << "windows.set_work_network cannot be nil." if @set_work_network.nil? { "Windows Guest" => errors } end def finalize! @set_work_network = false if @set_work_network == UNSET_VALUE end end end end ================================================ FILE: plugins/guests/windows/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestWindows module Errors # A convenient superclass for all our errors. class WindowsError < Vagrant::Errors::VagrantError error_namespace("vagrant_windows.errors") end class NetworkWinRMRequired < WindowsError error_key(:network_winrm_required) end class RenameComputerFailed < WindowsError error_key(:rename_computer_failed) end class PublicKeyDirectoryFailure < WindowsError error_key(:public_key_directory_failure) end end end end ================================================ FILE: plugins/guests/windows/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module GuestWindows class Guest < Vagrant.plugin("2", :guest) def detect?(machine) # See if the Windows directory is present. machine.communicate.test("test -d $Env:SystemRoot") end end end end ================================================ FILE: plugins/guests/windows/guest_network.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module GuestWindows # Manages the remote Windows guest network. class GuestNetwork PS_GET_WSMAN_VER = '((test-wsman).productversion.split(" ") | select -last 1).split("\.")[0]' WQL_NET_ADAPTERS_V2 = 'SELECT * FROM Win32_NetworkAdapter WHERE MACAddress IS NOT NULL' def initialize(communicator) @logger = Log4r::Logger.new("vagrant::windows::guestnetwork") @communicator = communicator end # Returns an array of all NICs on the guest. Each array entry is a # Hash of the NICs properties. # # @return [Array] def network_adapters wsman_version == 2? network_adapters_v2_winrm : network_adapters_v3_winrm end # Checks to see if the specified NIC is currently configured for DHCP. # # @return [Boolean] def is_dhcp_enabled(nic_index) cmd = <<-EOH if (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "Index=#{nic_index} and DHCPEnabled=True") { exit 0 } exit 1 EOH @communicator.test(cmd) end # Configures the specified interface for DHCP # # @param [Integer] The interface index. # @param [String] The unique name of the NIC, such as 'Local Area Connection'. def configure_dhcp_interface(nic_index, net_connection_id) @logger.info("Configuring NIC #{net_connection_id} for DHCP") if !is_dhcp_enabled(nic_index) netsh = "netsh interface ip set address \"#{net_connection_id}\" dhcp" @communicator.execute(netsh) end end # Configures the specified interface using a static address # # @param [Integer] The interface index. # @param [String] The unique name of the NIC, such as 'Local Area Connection'. # @param [String] The static IP address to assign to the specified NIC. # @param [String] The network mask to use with the static IP. def configure_static_interface(nic_index, net_connection_id, ip, netmask) @logger.info("Configuring NIC #{net_connection_id} using static ip #{ip}") #netsh interface ip set address "Local Area Connection 2" static 192.168.33.10 255.255.255.0 netsh = "netsh interface ip set address \"#{net_connection_id}\" static #{ip} #{netmask}" @communicator.execute(netsh) end # Sets all networks on the guest to 'Work Network' mode. This is # to allow guest access from the host via a private IP on Win7 # https://github.com/WinRb/vagrant-windows/issues/63 def set_all_networks_to_work @logger.info("Setting all networks to 'Work Network'") command = File.read(File.expand_path("../scripts/set_work_network.ps1", __FILE__)) @communicator.execute(command, { shell: :powershell }) end protected # Checks the WinRS version on the guest. Usually 2 on Windows 7/2008 # and 3 on Windows 8/2012. # # @return [Integer] def wsman_version @logger.debug("querying WSMan version") version = '' @communicator.execute(PS_GET_WSMAN_VER, { shell: :powershell }) do |type, line| version = version + "#{line}" if type == :stdout && !line.nil? end @logger.debug("wsman version: #{version}") Integer(version) end # Returns an array of all NICs on the guest. Each array entry is a # Hash of the NICs properties. This method should only be used on # guests that have WinRS version 2. # # @return [Array] def network_adapters_v2_winrm @logger.debug("querying network adapters") # Get all NICs that have a MAC address # https://msdn.microsoft.com/en-us/library/windows/desktop/aa394216(v=vs.85).aspx adapters = @communicator.execute(WQL_NET_ADAPTERS_V2, { shell: :wql } )[:win32_network_adapter] @logger.debug("#{adapters.inspect}") return adapters end # Returns an array of all NICs on the guest. Each array entry is a # Hash of the NICs properties. This method should only be used on # guests that have WinRS version 3. # # This method is a workaround until the WinRM gem supports WinRS version 3. # # @return [Array] def network_adapters_v3_winrm command = File.read(File.expand_path("../scripts/winrs_v3_get_adapters.ps1", __FILE__)) output = "" @communicator.execute(command, { shell: :powershell }) do |type, line| output = output + "#{line}" if type == :stdout && !line.nil? end adapters = [] JSON.parse(output).each do |nic| adapters << nic.inject({}){ |memo,(k,v)| memo[k.to_sym] = v; memo } end @logger.debug("#{adapters.inspect}") return adapters end end end end ================================================ FILE: plugins/guests/windows/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module GuestWindows autoload :Errors, File.expand_path("../errors", __FILE__) class Plugin < Vagrant.plugin("2") name "Windows guest." description "Windows guest support." config(:windows) do require_relative "config" Config end guest(:windows) do require_relative "guest" init! Guest end guest_capability(:windows, :change_host_name) do require_relative "cap/change_host_name" Cap::ChangeHostName end guest_capability(:windows, :configure_networks) do require_relative "cap/configure_networks" Cap::ConfigureNetworks end guest_capability(:windows, :halt) do require_relative "cap/halt" Cap::Halt end guest_capability(:windows, :create_tmp_path) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:windows, :decompress_zip) do require_relative "cap/file_system" Cap::FileSystem end guest_capability(:windows, :mount_virtualbox_shared_folder) do require_relative "cap/mount_shared_folder" Cap::MountSharedFolder end guest_capability(:windows, :mount_vmware_shared_folder) do require_relative "cap/mount_shared_folder" Cap::MountSharedFolder end guest_capability(:windows, :mount_parallels_shared_folder) do require_relative "cap/mount_shared_folder" Cap::MountSharedFolder end guest_capability(:windows, :wait_for_reboot) do require_relative "cap/reboot" Cap::Reboot end guest_capability(:windows, :reboot) do require_relative "cap/reboot" Cap::Reboot end guest_capability(:windows, :choose_addressable_ip_addr) do require_relative "cap/choose_addressable_ip_addr" Cap::ChooseAddressableIPAddr end guest_capability(:windows, :mount_smb_shared_folder) do require_relative "cap/mount_shared_folder" 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 end guest_capability(:windows, :insert_public_key) do require_relative "cap/public_key" Cap::PublicKey end guest_capability(:windows, :remove_public_key) do require_relative "cap/public_key" Cap::PublicKey end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path( "templates/locales/guest_windows.yml", Vagrant.source_root) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/guests/windows/scripts/mount_volume.ps1.erb ================================================ function Test-ReparsePoint([string]$path) { $file = Get-Item $path -Force -ea 0 return [bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint) } $MountPoint = [System.IO.Path]::GetFullPath("<%= options[:mount_point] %>") $ShareName = "<%= options[:share_name] %>" $VmProviderUncPath = "<%= options[:vm_provider_unc_path] %>" # https://github.com/BIAINC/vagrant-windows/issues/4 # Not sure why this works, but it does. & net use $ShareName 2>&1 | Out-Null Write-Debug "Attempting to mount $ShareName to $MountPoint" if( (Test-Path "$MountPoint") -and (Test-ReparsePoint "$MountPoint") ) { Write-Debug "Junction already exists, so I will delete it" # Powershell refuses to delete junctions, oh well use cmd cmd.exe /c rd "$MountPoint" if ( $LASTEXITCODE -ne 0 ) { Write-Error "Failed to delete symbolic link at $MountPoint" exit 1 } } elseif(Test-Path $MountPoint) { Write-Error "Mount point already exists and is not a symbolic link" exit 1 } $BaseDirectory = [System.IO.Path]::GetDirectoryName($MountPoint) if (-not (Test-Path $BaseDirectory)) { Write-Debug "Creating parent directory for mount point $BaseDirectory" New-Item $BaseDirectory -Type Directory -Force | Out-Null } cmd.exe /c mklink /D "$MountPoint" "$VmProviderUncPath" | out-null if ( $LASTEXITCODE -ne 0 ) { exit 1 } ================================================ FILE: plugins/guests/windows/scripts/reboot_detect.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # Function to check whether machine is currently shutting down function ShuttingDown { [string]$sourceCode = @" using System; using System.Runtime.InteropServices; namespace Vagrant { public static class RemoteManager { private const int SM_SHUTTINGDOWN = 0x2000; [DllImport("User32.dll", CharSet = CharSet.Unicode)] private static extern int GetSystemMetrics(int Index); public static bool Shutdown() { return (0 != GetSystemMetrics(SM_SHUTTINGDOWN)); } } } "@ $type = Add-Type -TypeDefinition $sourceCode -PassThru return $type::Shutdown() } if (ShuttingDown) { exit 1 } else { # See if a reboot is scheduled in the future by trying to schedule a reboot . shutdown.exe -f -r -t 60 if ($LASTEXITCODE -eq 1190) { # reboot is already pending exit 2 } if ($LASTEXITCODE -eq 1115) { # A system shutdown is in progress exit 2 } # Remove the pending reboot we just created above if ($LASTEXITCODE -eq 0) { . shutdown.exe -a } } # no reboot in progress or scheduled exit 0 ================================================ FILE: plugins/guests/windows/scripts/set_work_network.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # Get network connections $networkListManager = [Activator]::CreateInstance([Type]::GetTypeFromCLSID([Guid]"{DCB00C01-570F-4A9B-8D69-199FDBA5723B}")) $connections = $networkListManager.GetNetworkConnections() # Set network location to Private for all networks $connections | % {$_.GetNetwork().SetCategory(1)} ================================================ FILE: plugins/guests/windows/scripts/winrs_v3_get_adapters.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 $adapters = get-ciminstance win32_networkadapter -filter "macaddress is not null" $processed = @() foreach ($adapter in $adapters) { $Processed += new-object PSObject -Property @{ mac_address = $adapter.macaddress net_connection_id = $adapter.netconnectionid interface_index = $adapter.interfaceindex index = $adapter.index } } convertto-json -inputobject $processed ================================================ FILE: plugins/hosts/alt/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/subprocess" require "vagrant/util/which" module VagrantPlugins module HostALT module Cap class NFS def self.nfs_check_command(env) if systemd? return "systemctl status --no-pager nfs-server.service" else return "/etc/init.d/nfs status" end end def self.nfs_start_command(env) if systemd? return "systemctl start rpcbind nfs-server.service" else return "/etc/init.d/nfs restart" end end def self.nfs_installed(environment) if systemd? system("systemctl --no-pager --no-legend --plain list-unit-files --all --type=service | grep --fixed-strings --quiet nfs-server.service") else system("rpm -q nfs-server --quiet 2>&1") end end protected # This tests to see if systemd is used on the system. This is used # in newer versions of ALT, and requires a change in behavior. def self.systemd? result = Vagrant::Util::Subprocess.execute("ps", "-o", "comm=", "1") return result.stdout.chomp == "systemd" end end end end end ================================================ FILE: plugins/hosts/alt/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostALT class Host < Vagrant.plugin("2", :host) def detect?(env) File.exist?("/etc/altlinux-release") end end end end ================================================ FILE: plugins/hosts/alt/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostALT class Plugin < Vagrant.plugin("2") name "ALT Platform host" description "ALT Platform host support." host("alt", "linux") do require_relative "host" Host end host_capability("alt", "nfs_installed") do require_relative "cap/nfs" Cap::NFS end # Linux-specific helpers we need to determine paths that can # be overridden. host_capability("alt", "nfs_check_command") do require_relative "cap/nfs" Cap::NFS end host_capability("alt", "nfs_start_command") do require_relative "cap/nfs" Cap::NFS end end end end ================================================ FILE: plugins/hosts/arch/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostArch module Cap class NFS def self.nfs_check_command(env) return "/usr/sbin/systemctl status --no-pager nfs-server.service" end def self.nfs_start_command(env) return "/usr/sbin/systemctl start nfs-server.service" end def self.nfs_installed(environment) Kernel.system("systemctl --no-pager --no-legend --plain list-unit-files --all --type=service | grep --fixed-strings --quiet nfs-server.service") end end end end end ================================================ FILE: plugins/hosts/arch/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostArch class Host < Vagrant.plugin("2", :host) def detect?(env) File.exist?("/etc/arch-release") && !File.exist?("/etc/artix-release") end end end end ================================================ FILE: plugins/hosts/arch/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostArch class Plugin < Vagrant.plugin("2") name "Arch host" description "Arch host support." host("arch", "linux") do require_relative "host" Host end host_capability("arch", "nfs_installed") do require_relative "cap/nfs" Cap::NFS end # Linux-specific helpers we need to determine paths that can # be overridden. host_capability("arch", "nfs_check_command") do require_relative "cap/nfs" Cap::NFS end host_capability("arch", "nfs_start_command") do require_relative "cap/nfs" Cap::NFS end end end end ================================================ FILE: plugins/hosts/bsd/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/util" require "vagrant/util/shell_quote" require "vagrant/util/which" module VagrantPlugins module HostBSD module Cap class NFS def self.nfs_export(environment, ui, id, ips, folders) nfs_exports_template = environment.host.capability(:nfs_exports_template) nfs_restart_command = environment.host.capability(:nfs_restart_command) nfs_status_command = environment.host.capability(:nfs_status_command) nfs_update_command = environment.host.capability(:nfs_update_command) logger = Log4r::Logger.new("vagrant::hosts::bsd") nfs_checkexports! if File.file?("/etc/exports") # We need to build up mapping of directories that are enclosed # within each other because the exports file has to have subdirectories # of an exported directory on the same line. e.g.: # # "/foo" "/foo/bar" ... # "/bar" # # We build up this mapping within the following hash. logger.debug("Compiling map of sub-directories for NFS exports...") dirmap = {} folders.sort_by { |_, opts| opts[:hostpath] }.each do |_, opts| opts[:hostpath] = environment.host.capability(:resolve_host_path, opts[:hostpath].gsub('"', '\"')) hostpath = opts[:hostpath].dup found = false dirmap.each do |dirs, diropts| dirs.each do |dir| if dir.start_with?(hostpath) || hostpath.start_with?(dir) # TODO: verify opts and diropts are _identical_, raise an error # if not. NFS mandates subdirectories have identical options. dirs << hostpath found = true break end end break if found end if !found dirmap[[hostpath]] = opts.dup end end # Sort all the keys by length so that the directory closest to # the root is exported first. Also, remove duplicates so that # checkexports will work properly. dirmap.each do |dirs, _| dirs.uniq! dirs.sort_by! { |d| d.length } end # Setup the NFS options dirmap.each do |dirs, opts| if !opts[:bsd__nfs_options] opts[:bsd__nfs_options] = ["alldirs"] end hasmapall = false opts[:bsd__nfs_options].each do |opt| # mapall/maproot are mutually exclusive, so we have to check # for both here. if opt =~ /^mapall=/ || opt =~ /^maproot=/ hasmapall = true break end end if !hasmapall opts[:bsd__nfs_options] << "mapall=#{opts[:map_uid]}:#{opts[:map_gid]}" end opts[:bsd__compiled_nfs_options] = opts[:bsd__nfs_options].map do |opt| "-#{opt}" end.join(" ") end logger.info("Exporting the following for NFS...") dirmap.each do |dirs, opts| logger.info("NFS DIR: #{dirs.inspect}") logger.info("NFS OPTS: #{opts.inspect}") end output = Vagrant::Util::TemplateRenderer.render(nfs_exports_template, uuid: id, ips: ips, folders: dirmap, user: Process.uid) # The sleep ensures that the output is truly flushed before any `sudo` # commands are issued. ui.info I18n.t("vagrant.hosts.bsd.nfs_export") sleep 0.5 # First, clean up the old entry nfs_cleanup(id) # Only use "sudo" if we can't write to /etc/exports directly sudo_command = "" sudo_command = "sudo " if !File.writable?("/etc/exports") # Output the rendered template into the exports output.split("\n").each do |line| line = Vagrant::Util::ShellQuote.escape(line, "'") system( "echo '#{line}' | " + "#{sudo_command}/usr/bin/tee -a /etc/exports >/dev/null") end # Check if nfsd is running, and update or restart depending on the result if nfs_running?(nfs_status_command) system(*nfs_update_command) else system(*nfs_restart_command) end end def self.nfs_exports_template(environment) "nfs/exports_bsd" end def self.nfs_installed(environment) !!Vagrant::Util::Which.which("nfsd") end def self.nfs_prune(environment, ui, valid_ids) return if !File.exist?("/etc/exports") logger = Log4r::Logger.new("vagrant::hosts::bsd") logger.info("Pruning invalid NFS entries...") output = false user = Process.uid File.read("/etc/exports").lines.each do |line| if id = line[/^# VAGRANT-BEGIN:( #{user})? ([\.\/A-Za-z0-9\-_:]+?)$/, 2] if valid_ids.include?(id) logger.debug("Valid ID: #{id}") else if !output # We want to warn the user but we only want to output once ui.info I18n.t("vagrant.hosts.bsd.nfs_prune") output = true end logger.info("Invalid ID, pruning: #{id}") nfs_cleanup(id) end end end rescue Errno::EACCES raise Vagrant::Errors::NFSCantReadExports end def self.nfs_running?(check_command) Vagrant::Util::Subprocess.execute(*check_command).exit_code == 0 end def self.nfs_restart_command(environment) ["sudo", "nfsd", "restart"] end def self.nfs_update_command(environment) ["sudo", "nfsd", "update"] end def self.nfs_status_command(environment) ["sudo", "nfsd", "status"] end protected def self.nfs_cleanup(id) return if !File.exist?("/etc/exports") # Escape sed-sensitive characters: id = id.gsub("/", "\\/") id = id.gsub(".", "\\.") user = Process.uid command = [] command << "sudo" if !File.writable?("/etc/exports") command += [ "sed", "-E", "-e", "/^# VAGRANT-BEGIN:( #{user})? #{id}/," + "/^# VAGRANT-END:( #{user})? #{id}/ d", "-ibak", "/etc/exports" ] # Use sed to just strip out the block of code which was inserted # by Vagrant, and restart NFS. system(*command) end def self.nfs_checkexports! r = Vagrant::Util::Subprocess.execute("nfsd", "checkexports") if r.exit_code != 0 raise Vagrant::Errors::NFSBadExports, output: r.stderr end end end end end end ================================================ FILE: plugins/hosts/bsd/cap/path.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostBSD module Cap class Path def self.resolve_host_path(env, path) path end end end end end ================================================ FILE: plugins/hosts/bsd/cap/ssh.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostBSD module Cap class SSH # Set the ownership and permissions for SSH # private key # # @param [Vagrant::Environment] env # @param [Pathname] key_path def self.set_ssh_key_permissions(env, key_path) key_path.chmod(0600) end end end end end ================================================ FILE: plugins/hosts/bsd/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostBSD # Represents a BSD host, such as FreeBSD. class Host < Vagrant.plugin("2", :host) def detect?(env) Vagrant::Util::Platform.darwin? end end end end ================================================ FILE: plugins/hosts/bsd/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostBSD class Plugin < Vagrant.plugin("2") name "BSD host" description "BSD host support." host("bsd") do require File.expand_path("../host", __FILE__) Host end host_capability("bsd", "nfs_export") do require_relative "cap/nfs" Cap::NFS end host_capability("bsd", "nfs_exports_template") do require_relative "cap/nfs" Cap::NFS end host_capability("bsd", "nfs_installed") do require_relative "cap/nfs" Cap::NFS end host_capability("bsd", "nfs_prune") do require_relative "cap/nfs" Cap::NFS end host_capability("bsd", "nfs_restart_command") do require_relative "cap/nfs" Cap::NFS end host_capability("bsd", "nfs_update_command") do require_relative "cap/nfs" Cap::NFS end host_capability("bsd", "nfs_status_command") do require_relative "cap/nfs" Cap::NFS end host_capability("bsd", "resolve_host_path") do require_relative "cap/path" Cap::Path end host_capability("bsd", "set_ssh_key_permissions") do require_relative "cap/ssh" Cap::SSH end end end end ================================================ FILE: plugins/hosts/darwin/cap/configured_ip_addresses.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "socket" module VagrantPlugins module HostDarwin module Cap class ConfiguredIPAddresses def self.configured_ip_addresses(env) Socket.getifaddrs.map do |interface| if interface.addr.ipv4? && !interface.addr.ipv4_loopback? interface.addr.ip_address end end.compact end end end end end ================================================ FILE: plugins/hosts/darwin/cap/fs_iso.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "vagrant/util/caps" module VagrantPlugins module HostDarwin module Cap class FsISO extend Vagrant::Util::Caps::BuildISO @@logger = Log4r::Logger.new("vagrant::host::darwin::fs_iso") BUILD_ISO_CMD = "hdiutil".freeze # Check that the host has the ability to generate ISOs # # @param [Vagrant::Environment] env # @return [Boolean] def self.isofs_available(env) !!Vagrant::Util::Which.which(BUILD_ISO_CMD) end # Generate an ISO file of the given source directory # # @param [Vagrant::Environment] env # @param [String] source_directory Contents of ISO # @param [Map] extra arguments to pass to the iso building command # :file_destination (string) location to store ISO # :volume_id (String) to set the volume name # @return [Pathname] ISO location # @note If file_destination exists, source_directory will be checked # for recent modifications and a new ISO will be generated if requried. def self.create_iso(env, source_directory, extra_opts={}) source_directory = Pathname.new(source_directory) file_destination = self.ensure_output_iso(extra_opts[:file_destination]) iso_command = [BUILD_ISO_CMD, "makehybrid", "-iso", "-joliet", "-ov"] iso_command.concat(["-default-volume-name", extra_opts[:volume_id]]) if extra_opts[:volume_id] iso_command << "-o" iso_command << file_destination.to_s iso_command << source_directory.to_s self.build_iso(iso_command, source_directory, file_destination) @@logger.info("ISO available at #{file_destination}") file_destination end end end end end ================================================ FILE: plugins/hosts/darwin/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostDarwin module Cap class NFS def self.nfs_exports_template(environment) "nfs/exports_darwin" end end end end end ================================================ FILE: plugins/hosts/darwin/cap/path.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostDarwin module Cap class Path @@logger = Log4r::Logger.new("vagrant::host::darwin::path") FIRMLINK_DEFS = "/usr/share/firmlinks".freeze FIRMLINK_DATA_PATH = "/System/Volumes/Data".freeze CATALINA_CONSTRAINT = Gem::Requirement.new("~> 10.15") # Resolve the given host path to the actual # usable system path by detecting firmlinks # if available on the current system # # @param [String] path Host system path # @return [String] resolved path def self.resolve_host_path(env, path) path = File.expand_path(path) # Only expand firmlink paths on Catalina host_version = env.host.capability(:version) return path if !CATALINA_CONSTRAINT.satisfied_by?(host_version) firmlink = firmlink_map.detect do |mount_path, data_path| path.start_with?(mount_path) end return path if firmlink.nil? current_prefix, new_suffix = firmlink new_prefix = File.join(FIRMLINK_DATA_PATH, new_suffix) new_path = path.sub(current_prefix, new_prefix) @@logger.debug("Resolved given path `#{path}` to `#{new_path}`") new_path end # Generate mapping of firmlinks if available on the host # # @return [Hash] def self.firmlink_map if !@firmlink_map return @firmlink_map = {} if !File.exist?(FIRMLINK_DEFS) begin @firmlink_map = Hash[ File.readlines(FIRMLINK_DEFS).map { |d| d.strip.split(/\s+/, 2) } ] rescue => err @@logger.warn("Failed to parse firmlink definitions: #{err}") @firmlink_map = {} end end @firmlink_map end # @private # Reset the cached values for capability. This is not considered a public # API and should only be used for testing. def self.reset! instance_variables.each(&method(:remove_instance_variable)) end end end end end ================================================ FILE: plugins/hosts/darwin/cap/provider_install_virtualbox.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tempfile" require "vagrant/util/downloader" require "vagrant/util/file_checksum" 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.10/VirtualBox-5.0.10-104061-OSX.dmg".freeze VERSION = "5.0.10".freeze SHA256SUM = "62f933115498e51ddf5f2dab47dc1eebb42eb78ea1a7665cb91c53edacc847c6".freeze def self.provider_install_virtualbox(env) path = Dir::Tmpname.create("vagrant-provider-install-virtualbox") {} # 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, path, ui: ui) dl.download! # Validate that the file checksum matches actual = FileChecksum.new(path, Digest::SHA2).checksum if actual != SHA256SUM raise Vagrant::Errors::ProviderChecksumMismatch, provider: "virtualbox", actual: actual, expected: SHA256SUM end # 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, 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 ================================================ FILE: plugins/hosts/darwin/cap/rdp.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require "vagrant/util/subprocess" module VagrantPlugins module HostDarwin module Cap class RDP def self.rdp_client(env, rdp_info) config_path = self.generate_config_file(rdp_info) begin Vagrant::Util::Subprocess.execute("open", config_path.to_s) ensure # Note: this technically will never get run; neither would an # at_exit call. The reason is that `exec` replaces this process, # effectively the same as `kill -9`. This is solely here to prove # that and so that future developers do not waste a ton of time # try to identify why Vagrant is leaking RDP connection files. # There is a catch-22 here in that we can't delete the file before # we exec, and we can't delete the file after we exec :(. File.unlink(config_path) if File.file?(config_path) end end protected # Generates an RDP connection file and returns the resulting path. # @return [String] def self.generate_config_file(rdp_info) opts = { "drivestoredirect:s" => "*", "full address:s" => "#{rdp_info[:host]}:#{rdp_info[:port]}", "prompt for credentials:i" => "1", "username:s" => rdp_info[:username], } # Create the ".rdp" file t = ::Tempfile.new(["vagrant-rdp", ".rdp"]).tap do |f| f.binmode opts.each do |k, v| f.puts("#{k}:#{v}") end if rdp_info[:extra_args] rdp_info[:extra_args].each do |arg| f.puts("#{arg}") end end f.fsync f.close end return t.path end end end end end ================================================ FILE: plugins/hosts/darwin/cap/smb.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostDarwin module Cap class SMB @@logger = Log4r::Logger.new("vagrant::host::darwin::smb") # If we have the sharing binary available, smb is installed def self.smb_installed(env) File.exist?("/usr/sbin/sharing") end # Check if the required SMB services are loaded and enabled. If they are # not, then start them up def self.smb_start(env) result = Vagrant::Util::Subprocess.execute("pwpolicy", "gethashtypes") if result.exit_code == 0 && !result.stdout.include?("SMB-NT") @@logger.error("SMB compatible password has not been stored") raise SyncedFolderSMB::Errors::SMBCredentialsMissing end result = Vagrant::Util::Subprocess.execute("launchctl", "list", "com.apple.smb.preferences") if result.exit_code != 0 @@logger.warn("smb preferences service not enabled. enabling and starting...") cmd = ["/bin/launchctl", "load", "-w", "/System/Library/LaunchDaemons/com.apple.smb.preferences.plist"] result = Vagrant::Util::Subprocess.execute("/usr/bin/sudo", *cmd) if result.exit_code != 0 raise SyncedFolderSMB::Errors::SMBStartFailed, command: cmd.join(" "), stderr: result.stderr, stdout: result.stdout end end result = Vagrant::Util::Subprocess.execute("launchctl", "list", "com.apple.smbd") if result.exit_code != 0 @@logger.warn("smbd service not enabled. enabling and starting...") cmd = ["/bin/launchctl", "load", "-w", "/System/Library/LaunchDaemons/com.apple.smbd.plist"] result = Vagrant::Util::Subprocess.execute("/usr/bin/sudo", *cmd) if result.exit_code != 0 raise SyncedFolderSMB::Errors::SMBStartFailed, command: cmd.join(" "), stderr: result.stderr, stdout: result.stdout end Vagrant::Util::Subprocess.execute("/usr/bin/sudo", "/bin/launchctl", "start", "com.apple.smbd") end end # Required options for mounting a share hosted # on macos. def self.smb_mount_options(env) ["sec=ntlmssp", "nounix", "noperm"] end def self.smb_cleanup(env, machine, opts) m_id = machine_id(machine) result = Vagrant::Util::Subprocess.execute("/usr/bin/sudo", "/usr/sbin/sharing", "-l") if result.exit_code != 0 @@logger.warn("failed to locate any shares for cleanup") end shares = result.stdout.split("\n").map do |line| if line.start_with?("name:") share_name = line.sub("name:", "").strip share_name if share_name.start_with?("vgt-#{m_id}") end end.compact @@logger.debug("shares to be removed: #{shares}") shares.each do |share_name| @@logger.info("removing share name=#{share_name}") share_name.strip! result = Vagrant::Util::Subprocess.execute("/usr/bin/sudo", "/usr/sbin/sharing", "-r", share_name) if result.exit_code != 0 # Removing always returns 0 even if there are currently # guests attached so if we get a non-zero value just # log it as unexpected @@logger.warn("removing share `#{share_name}` returned non-zero") end end end def self.smb_prepare(env, machine, folders, opts) folders.each do |id, data| hostpath = data[:hostpath] chksum_id = Digest::MD5.hexdigest(id) name = "vgt-#{machine_id(machine)}-#{chksum_id}" data[:smb_id] ||= name @@logger.info("creating new share name=#{name} id=#{data[:smb_id]}") cmd = [ "/usr/bin/sudo", "/usr/sbin/sharing", "-a", hostpath, "-S", data[:smb_id], "-s", "001", "-g", "000", "-n", name ] r = Vagrant::Util::Subprocess.execute(*cmd) if r.exit_code != 0 raise VagrantPlugins::SyncedFolderSMB::Errors::DefineShareFailed, host: hostpath.to_s, stderr: r.stderr, stdout: r.stdout end end end # Generates a unique identifier for the given machine # based on the name, provider name, and working directory # of the environment. # # @param [Vagrant::Machine] machine # @return [String] def self.machine_id(machine) @@logger.debug("generating machine ID name=#{machine.name} cwd=#{machine.env.cwd}") Digest::MD5.hexdigest("#{machine.name}-#{machine.provider_name}-#{machine.env.cwd}") end end end end end ================================================ FILE: plugins/hosts/darwin/cap/version.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostDarwin module Cap class Version def self.version(env) r = Vagrant::Util::Subprocess.execute("sw_vers", "-productVersion") if r.exit_code != 0 raise Vagrant::Errors::DarwinVersionFailed, version: r.stdout, error: r.stderr end begin Gem::Version.new(r.stdout) rescue => err raise Vagrant::Errors::DarwinVersionFailed, version: r.stdout, error: err.message end end end end end end ================================================ FILE: plugins/hosts/darwin/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/platform" module VagrantPlugins module HostDarwin class Host < Vagrant.plugin("2", :host) def detect?(env) Vagrant::Util::Platform.darwin? end end end end ================================================ FILE: plugins/hosts/darwin/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostDarwin class Plugin < Vagrant.plugin("2") name "Mac OS X host" description "Mac OS X host support." host("darwin", "bsd") do require_relative "host" Host end host_capability("darwin", "isofs_available") do require_relative "cap/fs_iso" Cap::FsISO end host_capability("darwin", "create_iso") do require_relative "cap/fs_iso" Cap::FsISO end host_capability("darwin", "provider_install_virtualbox") do require_relative "cap/provider_install_virtualbox" Cap::ProviderInstallVirtualBox end host_capability("darwin", "resolve_host_path") do require_relative "cap/path" Cap::Path end host_capability("darwin", "rdp_client") do require_relative "cap/rdp" Cap::RDP end host_capability("darwin", "smb_installed") do require_relative "cap/smb" Cap::SMB end host_capability("darwin", "smb_prepare") do require_relative "cap/smb" Cap::SMB end host_capability("darwin", "smb_mount_options") do require_relative "cap/smb" Cap::SMB end host_capability("darwin", "smb_cleanup") do require_relative "cap/smb" Cap::SMB end host_capability("darwin", "smb_start") do require_relative "cap/smb" Cap::SMB end host_capability("darwin", "configured_ip_addresses") do require_relative "cap/configured_ip_addresses" Cap::ConfiguredIPAddresses end host_capability("darwin", "nfs_exports_template") do require_relative "cap/nfs" Cap::NFS end host_capability("darwin", "version") do require_relative "cap/version" Cap::Version end end end end ================================================ FILE: plugins/hosts/darwin/scripts/install_virtualbox.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 ================================================ FILE: plugins/hosts/freebsd/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util" require "vagrant/util/retryable" require Vagrant.source_root.join("plugins", "hosts", "bsd", "cap", "nfs") module VagrantPlugins module HostFreeBSD module Cap class NFS def self.nfs_export(environment, ui, id, ips, folders) folders.each do |folder_name, folder_values| if folder_values[:hostpath] =~ /\s+/ raise Vagrant::Errors::VagrantError, _key: :freebsd_nfs_whitespace end end HostBSD::Cap::NFS.nfs_export(environment, ui, id, ips, folders) end def self.nfs_exports_template(environment) "nfs/exports_bsd" end def self.nfs_restart_command(environment) ["sudo", "/etc/rc.d/mountd", "onereload"] end end end end end ================================================ FILE: plugins/hosts/freebsd/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" require 'vagrant/util/platform' module VagrantPlugins module HostFreeBSD class Host < Vagrant.plugin("2", :host) def detect?(env) Vagrant::Util::Platform.freebsd? end end end end ================================================ FILE: plugins/hosts/freebsd/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostFreeBSD class Plugin < Vagrant.plugin("2") name "FreeBSD host" description "FreeBSD host support." host("freebsd", "bsd") do require_relative "host" Host end host_capability("freebsd", "nfs_export") do require_relative "cap/nfs" Cap::NFS end # BSD-specific helpers host_capability("freebsd", "nfs_exports_template") do require_relative "cap/nfs" Cap::NFS end host_capability("freebsd", "nfs_restart_command") do require_relative "cap/nfs" Cap::NFS end end end end ================================================ FILE: plugins/hosts/gentoo/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/subprocess" require "vagrant/util/which" module VagrantPlugins module HostGentoo module Cap class NFS def self.nfs_check_command(env) if Vagrant::Util::Platform.systemd? "#{systemctl_path} status --no-pager nfs-server.service" else "/etc/init.d/nfs status" end end def self.nfs_start_command(env) if Vagrant::Util::Platform.systemd? "#{systemctl_path} start rpcbind nfs-server.service" else "/etc/init.d/nfs restart" end end protected def self.systemctl_path path = Vagrant::Util::Which.which("systemctl") return path if path folders = ["/usr/bin", "/usr/sbin"] folders.each do |folder| path = "#{folder}/systemctl" return path if File.file?(path) end end end end end end ================================================ FILE: plugins/hosts/gentoo/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostGentoo class Host < Vagrant.plugin("2", :host) def detect?(env) File.exist?("/etc/gentoo-release") end end end end ================================================ FILE: plugins/hosts/gentoo/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostGentoo class Plugin < Vagrant.plugin("2") name "Gentoo host" description "Gentoo host support." host("gentoo", "linux") do require_relative "host" Host end # Linux-specific helpers we need to determine paths that can # be overridden. host_capability("gentoo", "nfs_check_command") do require_relative "cap/nfs" Cap::NFS end host_capability("gentoo", "nfs_start_command") do require_relative "cap/nfs" Cap::NFS end end end end ================================================ FILE: plugins/hosts/linux/cap/fs_iso.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "vagrant/util/caps" module VagrantPlugins module HostLinux module Cap class FsISO extend Vagrant::Util::Caps::BuildISO @@logger = Log4r::Logger.new("vagrant::host::linux::fs_iso") BUILD_ISO_CMD = "mkisofs".freeze # Check that the host has the ability to generate ISOs # # @param [Vagrant::Environment] env # @return [Boolean] def self.isofs_available(env) !!Vagrant::Util::Which.which(BUILD_ISO_CMD) end # Generate an ISO file of the given source directory # # @param [Vagrant::Environment] env # @param [String] source_directory Contents of ISO # @param [Map] extra arguments to pass to the iso building command # :file_destination (string) location to store ISO # :volume_id (String) to set the volume name # @return [Pathname] ISO location # @note If file_destination exists, source_directory will be checked # for recent modifications and a new ISO will be generated if requried. def self.create_iso(env, source_directory, extra_opts={}) source_directory = Pathname.new(source_directory) file_destination = self.ensure_output_iso(extra_opts[:file_destination]) iso_command = [BUILD_ISO_CMD, "-joliet"] iso_command.concat(["-volid", extra_opts[:volume_id]]) if extra_opts[:volume_id] iso_command << "-o" iso_command << file_destination.to_s iso_command << source_directory.to_s self.build_iso(iso_command, source_directory, file_destination) @@logger.info("ISO available at #{file_destination}") file_destination end end end end end ================================================ FILE: plugins/hosts/linux/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "shellwords" require "vagrant/util" require "vagrant/util/shell_quote" require "vagrant/util/retryable" module VagrantPlugins module HostLinux module Cap class NFS NFS_EXPORTS_PATH = "/etc/exports".freeze NFS_DEFAULT_NAME_SYSTEMD = "nfs-server.service".freeze NFS_DEFAULT_NAME_SYSV = "nfs-kernel-server".freeze extend Vagrant::Util::Retryable def self.nfs_service_name_systemd if !defined?(@_nfs_systemd) result = Vagrant::Util::Subprocess.execute("systemctl", "list-units", "*nfs*server*", "--no-pager", "--no-legend") if result.exit_code == 0 @_nfs_systemd = result.stdout.to_s.split(/\s+/).first end if @_nfs_systemd.to_s.empty? @_nfs_systemd = NFS_DEFAULT_NAME_SYSTEMD end end @_nfs_systemd end def self.nfs_service_name_sysv if !defined?(@_nfs_sysv) @_nfs_sysv = Dir.glob("/etc/init.d/*nfs*server*").first.to_s if @_nfs_sysv.empty? @_nfs_sysv = NFS_DEFAULT_NAME_SYSV else @_nfs_sysv = File.basename(@_nfs_sysv) end end @_nfs_sysv end def self.nfs_apply_command(env) "exportfs -ar" end def self.nfs_check_command(env) if Vagrant::Util::Platform.systemd? "systemctl status --no-pager #{nfs_service_name_systemd}" else "/etc/init.d/#{nfs_service_name_sysv} status" end end def self.nfs_start_command(env) if Vagrant::Util::Platform.systemd? "systemctl start #{nfs_service_name_systemd}" else "/etc/init.d/#{nfs_service_name_sysv} start" end end def self.nfs_export(env, ui, id, ips, folders) # Get some values we need before we do anything nfs_apply_command = env.host.capability(:nfs_apply_command) nfs_check_command = env.host.capability(:nfs_check_command) nfs_start_command = env.host.capability(:nfs_start_command) nfs_opts_setup(folders) folders = folder_dupe_check(folders) ips = ips.uniq output = Vagrant::Util::TemplateRenderer.render('nfs/exports_linux', uuid: id, ips: ips, folders: folders, user: Process.uid) ui.info I18n.t("vagrant.hosts.linux.nfs_export") sleep 0.5 nfs_cleanup("#{Process.uid} #{id}") output = nfs_exports_content + output nfs_write_exports(output) if nfs_running?(nfs_check_command) Vagrant::Util::Subprocess.execute("sudo", *Shellwords.split(nfs_apply_command)).exit_code == 0 else Vagrant::Util::Subprocess.execute("sudo", *Shellwords.split(nfs_start_command)).exit_code == 0 end end def self.nfs_installed(environment) if Vagrant::Util::Platform.systemd? Vagrant::Util::Subprocess.execute("/bin/sh", "-c", "systemctl --no-pager --no-legend --plain list-unit-files --all --type=service " \ "| grep #{nfs_service_name_systemd}").exit_code == 0 else Vagrant::Util::Subprocess.execute(modinfo_path, "nfsd").exit_code == 0 || Vagrant::Util::Subprocess.execute("grep", "nfsd", "/proc/filesystems").exit_code == 0 end end def self.nfs_prune(environment, ui, valid_ids) return if !File.exist?(NFS_EXPORTS_PATH) logger = Log4r::Logger.new("vagrant::hosts::linux") logger.info("Pruning invalid NFS entries...") user = Process.uid # Create editor instance for removing invalid IDs editor = Vagrant::Util::StringBlockEditor.new(nfs_exports_content) # Build composite IDs with UID information and discover invalid entries composite_ids = valid_ids.map do |v_id| "#{user} #{v_id}" end remove_ids = editor.keys - composite_ids logger.debug("Known valid NFS export IDs: #{valid_ids}") logger.debug("Composite valid NFS export IDs with user: #{composite_ids}") logger.debug("NFS export IDs to be removed: #{remove_ids}") if !remove_ids.empty? ui.info I18n.t("vagrant.hosts.linux.nfs_prune") nfs_cleanup(remove_ids) end end protected # Takes a hash of folders and removes any duplicate exports that # share the same hostpath to avoid duplicate entries in /etc/exports # ref: GH-4666 def self.folder_dupe_check(folders) return_folders = {} # Group by hostpath to see if there are multiple exports coming # from the same folder export_groups = folders.values.group_by { |h| h[:hostpath] } # We need to check that each group key only has 1 value, # and if not, check each nfs option. If all nfs options are the same # we're good, otherwise throw an exception export_groups.each do |path,group| if group.size > 1 # if the linux nfs options aren't all the same throw an exception group1_opts = group.first[:linux__nfs_options] if !group.all? {|g| g[:linux__nfs_options] == group1_opts} raise Vagrant::Errors::NFSDupePerms, hostpath: group.first[:hostpath] else # if they're the same just pick the first one return_folders[path] = group.first end else # just return folder, there are no duplicates return_folders[path] = group.first end end return_folders end def self.nfs_cleanup(remove_ids) return if !File.exist?(NFS_EXPORTS_PATH) editor = Vagrant::Util::StringBlockEditor.new(nfs_exports_content) remove_ids = Array(remove_ids) # Remove all invalid ID entries remove_ids.each do |r_id| editor.delete(r_id) end nfs_write_exports(editor.value) end def self.nfs_write_exports(new_exports_content) if(nfs_exports_content != new_exports_content.strip) begin exports_path = Pathname.new(NFS_EXPORTS_PATH) # Write contents out to temporary file new_exports_path = File.join(Dir.tmpdir, "vagrant-exports") FileUtils.rm_f(new_exports_path) new_exports_file = File.open(new_exports_path, "w+") new_exports_file.puts(new_exports_content) new_exports_file.close # Ensure new file mode and uid/gid match existing file to replace existing_stat = File.stat(NFS_EXPORTS_PATH) new_stat = File.stat(new_exports_path) if existing_stat.mode != new_stat.mode File.chmod(existing_stat.mode, new_exports_path) end if existing_stat.uid != new_stat.uid || existing_stat.gid != new_stat.gid chown_cmd = "sudo chown #{existing_stat.uid}:#{existing_stat.gid} #{new_exports_path}" result = Vagrant::Util::Subprocess.execute(*Shellwords.split(chown_cmd)) if result.exit_code != 0 raise Vagrant::Errors::NFSExportsFailed, command: chown_cmd, stderr: result.stderr, stdout: result.stdout end end # Always force move the file to prevent overwrite prompting sudo_command = "sudo " if !exports_path.writable? || !exports_path.dirname.writable? mv_cmd = "#{sudo_command}mv -f #{new_exports_path} #{NFS_EXPORTS_PATH}" result = Vagrant::Util::Subprocess.execute(*Shellwords.split(mv_cmd)) if result.exit_code != 0 raise Vagrant::Errors::NFSExportsFailed, command: mv_cmd, stderr: result.stderr, stdout: result.stdout end ensure if !new_exports_path.nil? && File.exist?(new_exports_path) File.unlink(new_exports_path) end end end end def self.nfs_exports_content if(File.exist?(NFS_EXPORTS_PATH)) if(File.readable?(NFS_EXPORTS_PATH)) File.read(NFS_EXPORTS_PATH) else cmd = "sudo cat #{NFS_EXPORTS_PATH}" result = Vagrant::Util::Subprocess.execute(*Shellwords.split(cmd)) if result.exit_code != 0 raise Vagrant::Errors::NFSExportsFailed, command: cmd, stderr: result.stderr, stdout: result.stdout else result.stdout end end else "" end end def self.nfs_opts_setup(folders) folders.each do |k, opts| if !opts[:linux__nfs_options] opts[:linux__nfs_options] ||= ["rw", "no_subtree_check", "all_squash"] end # Only automatically set anonuid/anongid if they weren't # explicitly set by the user. hasgid = false hasuid = false opts[:linux__nfs_options].each do |opt| hasgid = !!(opt =~ /^anongid=/) if !hasgid hasuid = !!(opt =~ /^anonuid=/) if !hasuid end opts[:linux__nfs_options] << "anonuid=#{opts[:map_uid]}" if !hasuid opts[:linux__nfs_options] << "anongid=#{opts[:map_gid]}" if !hasgid opts[:linux__nfs_options] << "fsid=#{opts[:uuid]}" end end def self.nfs_running?(check_command) Vagrant::Util::Subprocess.execute(*Shellwords.split(check_command)).exit_code == 0 end def self.modinfo_path if !defined?(@_modinfo_path) @_modinfo_path = Vagrant::Util::Which.which("modinfo") if @_modinfo_path.to_s.empty? path = "/sbin/modinfo" if File.file?(path) @_modinfo_path = path end end if @_modinfo_path.to_s.empty? @_modinfo_path = "modinfo" end end @_modinfo_path end # @private # Reset the cached values for capability. This is not considered a public # API and should only be used for testing. def self.reset! instance_variables.each(&method(:remove_instance_variable)) end end end end end ================================================ FILE: plugins/hosts/linux/cap/rdp.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/which" module VagrantPlugins module HostLinux module Cap class RDP def self.rdp_client(env, rdp_info) # 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 if Vagrant::Util::Platform.wsl? "mstsc.exe" else raise Vagrant::Errors::LinuxRDPClientNotFound end end 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]}" when "mstsc.exe" # Setup password cmdKeyArgs = [ "/add:#{rdp_info[:host]}:#{rdp_info[:port]}", "/user:#{rdp_info[:username]}", "/pass:#{rdp_info[:password]}", ] Vagrant::Util::Subprocess.execute("cmdkey.exe", *cmdKeyArgs) args = ["/v:#{rdp_info[:host]}:#{rdp_info[:port]}"] args += rdp_info[:extra_args] if rdp_info[:extra_args] end # Finally, run the client. Vagrant::Util::Subprocess.execute(rdp_client, *args, {:detach => true}) end end end end end ================================================ FILE: plugins/hosts/linux/cap/ssh.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostLinux module Cap class SSH # Set the ownership and permissions for SSH # private key # # @param [Vagrant::Environment] env # @param [Pathname] key_path def self.set_ssh_key_permissions(env, key_path) key_path.chmod(0600) end end end end end ================================================ FILE: plugins/hosts/linux/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostLinux # Represents a Linux based host, such as Ubuntu. class Host < Vagrant.plugin("2", :host) def detect?(env) Vagrant::Util::Platform.linux? end end end end ================================================ FILE: plugins/hosts/linux/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostLinux class Plugin < Vagrant.plugin("2") name "Linux host" description "Linux host support." host("linux") do require_relative "host" Host end host_capability("linux", "isofs_available") do require_relative "cap/fs_iso" Cap::FsISO end host_capability("linux", "create_iso") do require_relative "cap/fs_iso" Cap::FsISO end host_capability("linux", "nfs_export") do require_relative "cap/nfs" Cap::NFS end host_capability("linux", "nfs_installed") do require_relative "cap/nfs" Cap::NFS end host_capability("linux", "nfs_prune") do require_relative "cap/nfs" Cap::NFS end host_capability("linux", "rdp_client") do require_relative "cap/rdp" Cap::RDP end # Linux-specific helpers we need to determine paths that can # be overridden. host_capability("linux", "nfs_apply_command") do require_relative "cap/nfs" Cap::NFS end host_capability("linux", "nfs_check_command") do require_relative "cap/nfs" Cap::NFS end host_capability("linux", "nfs_start_command") do require_relative "cap/nfs" Cap::NFS end host_capability("linux", "set_ssh_key_permissions") do require_relative "cap/ssh" Cap::SSH end end end end ================================================ FILE: plugins/hosts/null/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostNull class Host < Vagrant.plugin("2", :host) def detect?(env) # This host can only be explicitly chosen. false end end end end ================================================ FILE: plugins/hosts/null/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostNull class Plugin < Vagrant.plugin("2") name "null host" description "A host that implements no capabilities." host("null") do require_relative "host" Host end end end end ================================================ FILE: plugins/hosts/redhat/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" module VagrantPlugins module HostRedHat module Cap class NFS def self.nfs_check_command(env) if Vagrant::Util::Platform.systemd? "systemctl status --no-pager nfs-server.service" else "#{nfs_server_binary} status" end end def self.nfs_start_command(env) if Vagrant::Util::Platform.systemd? "systemctl start nfs-server.service" else "#{nfs_server_binary} start" end end protected def self.nfs_server_binary nfs_server_binary = "/etc/init.d/nfs" # On Fedora 16+, systemd replaced init.d, so we have to use the # proper NFS binary. This checks to see if we need to do that. release_file = Pathname.new("/etc/redhat-release") begin release_file.open("r:ISO-8859-1:UTF-8") do |f| match = /(Red Hat|CentOS|Fedora).* release ([0-9]+)/.match(f.gets) if match distribution = match[1] version_number = match[2].to_i if (distribution =~ /Fedora/ && version_number >= 16) || (distribution =~ /Red Hat|CentOS/ && version_number >= 7) # "service nfs-server" will redirect properly to systemctl # when "service nfs-server restart" is called. nfs_server_binary = "/usr/sbin/service nfs-server" end end end rescue Errno::ENOENT # File doesn't exist, not a big deal, assume we're on a # lower version. end nfs_server_binary end end end end end ================================================ FILE: plugins/hosts/redhat/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" module VagrantPlugins module HostRedHat class Host < Vagrant.plugin("2", :host) def detect?(env) release_file = Pathname.new("/etc/redhat-release") if release_file.exist? release_file.open("r:ISO-8859-1:UTF-8") do |f| contents = f.gets return true if contents =~ /^CentOS/ # CentOS return true if contents =~ /^Fedora/ # Fedora return true if contents =~ /^Korora/ # Korora # Oracle Linux < 5.3 return true if contents =~ /^Enterprise Linux Enterprise Linux/ # Red Hat Enterprise Linux and Oracle Linux >= 5.3 return true if contents =~ /^Red Hat Enterprise Linux/ end end false end end end end ================================================ FILE: plugins/hosts/redhat/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostRedHat class Plugin < Vagrant.plugin("2") name "Red Hat Enterprise Linux host" description "Red Hat Enterprise Linux host support." host("redhat", "linux") do require File.expand_path("../host", __FILE__) Host end # Linux-specific helpers we need to determine paths that can # be overridden. host_capability("redhat", "nfs_check_command") do require_relative "cap/nfs" Cap::NFS end host_capability("redhat", "nfs_start_command") do require_relative "cap/nfs" Cap::NFS end end end end ================================================ FILE: plugins/hosts/slackware/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostSlackware module Cap class NFS def self.nfs_check_command(env) "/sbin/pidof nfsd >/dev/null" end def self.nfs_start_command(env) "/etc/rc.d/rc.nfsd start" end end end end end ================================================ FILE: plugins/hosts/slackware/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostSlackware class Host < Vagrant.plugin("2", :host) def detect?(env) return File.exist?("/etc/slackware-version") || !Dir.glob("/usr/lib/setup/Plamo-*").empty? end end end end ================================================ FILE: plugins/hosts/slackware/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostSlackware class Plugin < Vagrant.plugin("2") name "Slackware host" description "Slackware and derivatives host support." host("slackware", "linux") do require File.expand_path("../host", __FILE__) Host end # Linux-specific helpers we need to determine paths that can # be overridden. host_capability("slackware", "nfs_check_command") do require_relative "cap/nfs" Cap::NFS end host_capability("slackware", "nfs_start_command") do require_relative "cap/nfs" Cap::NFS end end end end ================================================ FILE: plugins/hosts/suse/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostSUSE module Cap class NFS def self.nfs_installed(env) system("rpm -q nfs-kernel-server > /dev/null 2>&1") end def self.nfs_check_command(env) "systemctl status --no-pager nfs-server" end def self.nfs_start_command(env) "systemctl start --no-pager nfs-server" end end end end end ================================================ FILE: plugins/hosts/suse/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" module VagrantPlugins module HostSUSE class Host < Vagrant.plugin("2", :host) def detect?(env) old_release_file = Pathname.new("/etc/SuSE-release") if old_release_file.exist? old_release_file.open("r") do |f| return true if f.gets =~ /^(openSUSE|SUSE Linux Enterprise)/ end end new_release_file = Pathname.new("/etc/os-release") if new_release_file.exist? new_release_file.open("r") do |f| return true if f.gets =~ /(openSUSE|SLES)/ end end false end end end end ================================================ FILE: plugins/hosts/suse/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostSUSE class Plugin < Vagrant.plugin("2") name "SUSE host" description "SUSE host support." host("suse", "linux") do require_relative "host" Host end host_capability("suse", "nfs_installed") do require_relative "cap/nfs" Cap::NFS end host_capability("suse", "nfs_check_command") do require_relative "cap/nfs" Cap::NFS end host_capability("suse", "nfs_start_command") do require_relative "cap/nfs" Cap::NFS end end end end ================================================ FILE: plugins/hosts/void/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostVoid module Cap class NFS def self.nfs_check_command(env) "sudo /usr/bin/sv status nfs-server" end def self.nfs_start_command(env) <<-EOF /usr/bin/ln -s /etc/sv/statd /var/service/ && \ /usr/bin/ln -s /etc/sv/rpcbind /var/service/ && \ /usr/bin/ln -s /etc/sv/nfs-server /var/service/ EOF end def self.nfs_installed(env) result = Vagrant::Util::Subprocess.execute("/usr/bin/xbps-query", "nfs-utils") result.exit_code == 0 end end end end end ================================================ FILE: plugins/hosts/void/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'pathname' module VagrantPlugins module HostVoid class Host < Vagrant.plugin("2", :host) def detect?(env) os_file = Pathname.new("/etc/os-release") if os_file.exist? file = os_file.open while (line = file.gets) do return true if line =~ /^ID="void"/ end end false end end end end ================================================ FILE: plugins/hosts/void/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostVoid class Plugin < Vagrant.plugin("2") name "Void host" description "Void linux host support." host("void", "linux") do require_relative "host" Host end host_capability("void", "nfs_installed") do require_relative "cap/nfs" Cap::NFS end host_capability("void", "nfs_check_command") do require_relative "cap/nfs" Cap::NFS end host_capability("void", "nfs_start_command") do require_relative "cap/nfs" Cap::NFS end end end end ================================================ FILE: plugins/hosts/windows/cap/configured_ip_addresses.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tempfile" require "vagrant/util/downloader" require "vagrant/util/file_checksum" require "vagrant/util/powershell" require "vagrant/util/subprocess" module VagrantPlugins module HostWindows module Cap class ConfiguredIPAddresses def self.configured_ip_addresses(env) script_path = File.expand_path("../../scripts/host_info.ps1", __FILE__) r = Vagrant::Util::PowerShell.execute(script_path) if r.exit_code != 0 raise Vagrant::Errors::PowerShellError, script: script_path, stderr: r.stderr end res = JSON.parse(r.stdout)["ip_addresses"] Array(res) end end end end end ================================================ FILE: plugins/hosts/windows/cap/fs_iso.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "vagrant/util/caps" module VagrantPlugins module HostWindows module Cap class FsISO extend Vagrant::Util::Caps::BuildISO @@logger = Log4r::Logger.new("vagrant::host::windows::fs_iso") BUILD_ISO_CMD = "oscdimg.exe".freeze DEPLOYMENT_KIT_PATHS = [ "C:/Program Files (x86)/Windows Kits/10/Assessment and Deployment Kit/Deployment Tools".freeze, ].freeze # Check that the host has the ability to generate ISOs # # @param [Vagrant::Environment] env # @return [Boolean] def self.isofs_available(env) begin oscdimg_path true rescue Vagrant::Errors::OscdimgCommandMissingError false end end # Generate an ISO file of the given source directory # # @param [Vagrant::Environment] env # @param [String] source_directory Contents of ISO # @param [Map] extra arguments to pass to the iso building command # :file_destination (string) location to store ISO # :volume_id (String) to set the volume name # @return [Pathname] ISO location # @note If file_destination exists, source_directory will be checked # for recent modifications and a new ISO will be generated if requried. def self.create_iso(env, source_directory, extra_opts={}) source_directory = Pathname.new(source_directory) file_destination = self.ensure_output_iso(extra_opts[:file_destination]) iso_command = [oscdimg_path, "-j1", "-o", "-m"] iso_command << "-l#{extra_opts[:volume_id]}" if extra_opts[:volume_id] iso_command << source_directory.to_s iso_command << file_destination.to_s self.build_iso(iso_command, source_directory, file_destination) @@logger.info("ISO available at #{file_destination}") file_destination end # @return [String] oscdimg executable def self.oscdimg_path return BUILD_ISO_CMD if Vagrant::Util::Which.which(BUILD_ISO_CMD) @@logger.debug("#{BUILD_ISO_CMD} not found on PATH") DEPLOYMENT_KIT_PATHS.each do |base| path = File.join(base, Vagrant::Util::Platform.architecture, "Oscdimg", BUILD_ISO_CMD) @@logger.debug("#{BUILD_ISO_CMD} check at #{path}") return path if File.executable?(path) end raise Vagrant::Errors::OscdimgCommandMissingError end end end end end ================================================ FILE: plugins/hosts/windows/cap/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostWindows module Cap class NFS def self.nfs_installed(env) false end end end end end ================================================ FILE: plugins/hosts/windows/cap/provider_install_virtualbox.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tempfile" require "vagrant/util/downloader" require "vagrant/util/file_checksum" require "vagrant/util/powershell" 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.10/VirtualBox-5.0.10-104061-Win.exe".freeze VERSION = "5.0.10".freeze SHA256SUM = "3e5ed8fe4ada6eef8dfb4fe6fd79fcab4b242acf799f7d3ab4a17b43838b1e04".freeze def self.provider_install_virtualbox(env) path = Dir::Tmpname.create("vagrant-provider-install-virtualbox") {} # 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, path, ui: ui) dl.download! # Validate that the file checksum matches actual = FileChecksum.new(path, Digest::SHA2).checksum if actual != SHA256SUM raise Vagrant::Errors::ProviderChecksumMismatch, provider: "virtualbox", actual: actual, expected: SHA256SUM end # 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, 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 ================================================ FILE: plugins/hosts/windows/cap/ps.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require "vagrant/util/safe_exec" 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}") args = ["-NoProfile"] args << "-ExecutionPolicy" args << "Bypass" args << "-NoExit" args << "-EncodedCommand" args << encoded(command) if ps_info[:extra_args] args << ps_info[:extra_args] end # Launch it Vagrant::Util::SafeExec.exec("powershell", *args) end def self.encoded(script) encoded_script = script.encode('UTF-16LE', 'UTF-8') Base64.strict_encode64(encoded_script) end end end end end ================================================ FILE: plugins/hosts/windows/cap/rdp.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require "vagrant/util/subprocess" module VagrantPlugins module HostWindows module Cap class RDP def self.rdp_client(env, rdp_info) # Setup password cmdKeyArgs = [ "/add:#{rdp_info[:host]}:#{rdp_info[:port]}", "/user:#{rdp_info[:username]}", "/pass:#{rdp_info[:password]}", ] Vagrant::Util::Subprocess.execute("cmdkey", *cmdKeyArgs) # Build up the args to mstsc args = ["/v:#{rdp_info[:host]}:#{rdp_info[:port]}"] if rdp_info[:extra_args] args = rdp_info[:extra_args] + args end # Launch it Vagrant::Util::Subprocess.execute("mstsc", *args, {:detach => true}) end end end end end ================================================ FILE: plugins/hosts/windows/cap/smb.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostWindows module Cap class SMB # Number of seconds to display UAC warning to user UAC_PROMPT_WAIT = 4 @@logger = Log4r::Logger.new("vagrant::host::windows::smb") def self.smb_installed(env) psv = Vagrant::Util::PowerShell.version.to_i if psv < 3 return false end true end # Required options for mounting a share hosted on Windows # NOTE: Windows deprecated smb 1.0 so a minimum of 2.0 must be enabled def self.smb_mount_options(env) ["vers=2.0"] end def self.smb_validate_password(env, machine, username, password) script_path = File.expand_path("../../scripts/check_credentials.ps1", __FILE__) args = [] args << "-username" << "'#{username.gsub("'", "''")}'" args << "-password" << "'#{password.gsub("'", "''")}'" r = Vagrant::Util::PowerShell.execute(script_path, *args) r.exit_code == 0 end def self.smb_cleanup(env, machine, opts) script_path = File.expand_path("../../scripts/unset_share.ps1", __FILE__) m_id = machine_id(machine) prune_shares = existing_shares.map do |share_name, share_info| if share_info["Description"].to_s.start_with?("vgt-#{m_id}-") @@logger.info("removing smb share name=#{share_name} id=#{m_id}") share_name else @@logger.info("skipping smb share removal, not owned name=#{share_name}") @@logger.debug("smb share ID not present name=#{share_name} id=#{m_id} description=#{share_info["Description"]}") nil end end.compact @@logger.debug("shares to be removed: #{prune_shares}") if prune_shares.size > 0 machine.env.ui.warn("\n" + I18n.t("vagrant_sf_smb.uac.prune_warning") + "\n") sleep UAC_PROMPT_WAIT @@logger.info("remove shares: #{prune_shares}") result = Vagrant::Util::PowerShell.execute(script_path, *prune_shares, sudo: true) if result.exit_code != 0 failed_name = result.stdout.to_s.sub("share name: ", "") raise SyncedFolderSMB::Errors::PruneShareFailed, name: failed_name, stderr: result.stderr, stdout: result.stdout end end end def self.smb_prepare(env, machine, folders, opts) script_path = File.expand_path("../../scripts/set_share.ps1", __FILE__) shares = [] current_shares = existing_shares folders.each do |id, data| hostpath = data[:hostpath].to_s chksum_id = Digest::MD5.hexdigest(id) name = "vgt-#{machine_id(machine)}-#{chksum_id}" data[:smb_id] ||= name # Check if this name is already in use if share_info = current_shares[data[:smb_id]] exist_path = File.expand_path(share_info["Path"]).downcase request_path = File.expand_path(hostpath).downcase if !hostpath.empty? && exist_path != request_path raise SyncedFolderSMB::Errors::SMBNameError, path: hostpath, existing_path: share_info["Path"], name: data[:smb_id] end @@logger.info("skip creation of existing share name=#{name} id=#{data[:smb_id]}") next end @@logger.info("creating new share name=#{name} id=#{data[:smb_id]}") shares << [ "\"#{hostpath.gsub("/", "\\")}\"", name, data[:smb_id] ] end if !shares.empty? uac_notified = false shares.each_slice(10) do |s_shares| if !uac_notified machine.env.ui.warn("\n" + I18n.t("vagrant_sf_smb.uac.create_warning") + "\n") uac_notified = true sleep(UAC_PROMPT_WAIT) end result = Vagrant::Util::PowerShell.execute(script_path, *s_shares, sudo: true) if result.exit_code != 0 share_path = result.stdout.to_s.sub("share path: ", "") raise SyncedFolderSMB::Errors::DefineShareFailed, host: share_path, stderr: result.stderr, stdout: result.stdout end end end end # Generate a list of existing local smb shares # # @return [Hash] def self.existing_shares shares = get_smbshares || get_netshares if shares.nil? raise SyncedFolderSMB::Errors::SMBListFailed end @@logger.debug("local share listing: #{shares}") shares end # Get current SMB share list using Get-SmbShare # # @return [Hash] def self.get_smbshares result = Vagrant::Util::PowerShell.execute_cmd("Get-SmbShare|Format-List|Out-String -Width 4096") if result.nil? return nil end share_data = result.strip.lines shares = {} name = nil until share_data.empty? content = share_data.take_while{|line| !line.strip.empty? } share_name = content[0].strip.split(":", 2).last.strip shares[share_name] = { "Path" => content[-2].strip.split(":", 2).last.strip, "Description" => content[-1].strip.split(":", 2).last.strip } share_data.slice!(0, content.length + 1) end shares end # Get current SMB share list using net.exe # # @return [Hash] def self.get_netshares result = Vagrant::Util::PowerShell.execute_cmd("net share | Out-String -Width 4096") if result.nil? return nil end share_data = result.strip.lines # Remove header information share_data.slice!(0, 2) # Remove footer information share_data.slice!(share_data.size - 1, share_data.size) share_names = share_data.map do |line| line.strip.split(/\s+/).first.strip end shares = {} share_names.each do |share_name| result = Vagrant::Util::PowerShell.execute_cmd("net share #{share_name} | Out-String -Width 4096") next if result.nil? result.strip! share_info = result.lines shares[share_name] = { "Path" => share_info[1].split(/\s+/, 2).last.strip, "Description" => share_info[2].split(/\s+/, 2).last.strip } end shares end # Generates a unique identifier for the given machine # based on the name, provider name, and working directory # of the environment. # # @param [Vagrant::Machine] machine # @return [String] def self.machine_id(machine) @@logger.debug("generating machine ID name=#{machine.name} cwd=#{machine.env.cwd}") Digest::MD5.hexdigest("#{machine.name}-#{machine.provider_name}-#{machine.env.cwd}") end end end end end ================================================ FILE: plugins/hosts/windows/cap/ssh.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HostWindows module Cap class SSH # Set the ownership and permissions for SSH # private key # # @param [Vagrant::Environment] env # @param [Pathname] key_path def self.set_ssh_key_permissions(env, key_path) script_path = Host.scripts_path.join("set_ssh_key_permissions.ps1") result = Vagrant::Util::PowerShell.execute( script_path.to_s, "-KeyPath", key_path.to_s.gsub(' ', '` '), module_path: Host.modules_path.to_s ) if result.exit_code != 0 raise Vagrant::Errors::PowerShellError, script: script_path, stderr: result.stderr end result end end end end end ================================================ FILE: plugins/hosts/windows/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" require 'vagrant/util/platform' module VagrantPlugins module HostWindows class Host < Vagrant.plugin("2", :host) def detect?(env) Vagrant::Util::Platform.windows? end # @return [Pathname] Path to scripts directory def self.scripts_path Pathname.new(File.expand_path("../scripts", __FILE__)) end # @return [Pathname] Path to modules directory def self.modules_path scripts_path.join("utils") end end end end ================================================ FILE: plugins/hosts/windows/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HostWindows class Plugin < Vagrant.plugin("2") name "Windows host" description "Windows host support." host("windows") do require_relative "host" Host end host_capability("windows", "isofs_available") do require_relative "cap/fs_iso" Cap::FsISO end host_capability("windows", "create_iso") do require_relative "cap/fs_iso" Cap::FsISO 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 end host_capability("windows", "rdp_client") do require_relative "cap/rdp" Cap::RDP end host_capability("windows", "ps_client") do require_relative "cap/ps" Cap::PS end host_capability("windows", "smb_installed") do require_relative "cap/smb" Cap::SMB end host_capability("windows", "smb_prepare") do require_relative "cap/smb" Cap::SMB end host_capability("windows", "smb_cleanup") do require_relative "cap/smb" Cap::SMB end host_capability("windows", "smb_mount_options") do require_relative "cap/smb" Cap::SMB end host_capability("windows", "configured_ip_addresses") do require_relative "cap/configured_ip_addresses" Cap::ConfiguredIPAddresses end host_capability("windows", "set_ssh_key_permissions") do require_relative "cap/ssh" Cap::SSH end host_capability("windows", "smb_validate_password") do require_relative "cap/smb" Cap::SMB end end end end ================================================ FILE: plugins/hosts/windows/scripts/check_credentials.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 Param( [Parameter(Mandatory=$true)] [string]$username, [Parameter(Mandatory=$true)] [string]$password ) Add-Type -AssemblyName System.DirectoryServices.AccountManagement $DSContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext( [System.DirectoryServices.AccountManagement.ContextType]::Machine, $env:COMPUTERNAME ) if ( $DSContext.ValidateCredentials( $username, $password ) ) { exit 0 } $DSContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext( [System.DirectoryServices.AccountManagement.ContextType]::Domain, $env:COMPUTERNAME ) if ( $DSContext.ValidateCredentials( $username, $password ) ) { exit 0 } $DSContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext( [System.DirectoryServices.AccountManagement.ContextType]::ApplicationDirectory, $env:COMPUTERNAME ) if ( $DSContext.ValidateCredentials( $username, $password ) ) { exit 0 } exit 1 ================================================ FILE: plugins/hosts/windows/scripts/host_info.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 $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) ================================================ FILE: plugins/hosts/windows/scripts/install_virtualbox.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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" ================================================ FILE: plugins/hosts/windows/scripts/set_share.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # The names of the user are language dependent! $objSID = New-Object System.Security.Principal.SecurityIdentifier("S-1-1-0") $objUser = $objSID.Translate([System.Security.Principal.NTAccount]) $grant = "$objUser,Full" for ($i=0; $i -le $args.length; $i = $i + 3) { $path = $args[$i] $share_name = $args[$i+1] $share_id = $args[$i+2] if ($path -eq $null) { Write-Warning "empty path argument encountered - complete" exit 0 } if ($share_name -eq $null) { Write-Output "share path: ${path}" Write-Error "error - no share name provided" exit 1 } if ($share_id -eq $null) { Write-Output "share path: ${path}" Write-Error "error - no share ID provided" exit 1 } $result = net share $share_id=$path /unlimited /GRANT:$grant /REMARK:"${share_name}" if ($LastExitCode -ne 0) { $host.ui.WriteLine("share path: ${path}") $host.ui.WriteErrorLine("error ${result}") exit 1 } } exit 0 ================================================ FILE: plugins/hosts/windows/scripts/set_ssh_key_permissions.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantSSH param( [Parameter(Mandatory=$true)] [string] $KeyPath, [Parameter(Mandatory=$false)] [string] $Principal=$null ) $ErrorActionPreference = "Stop" try { Set-SSHKeyPermissions -SSHKeyPath $KeyPath -Principal $Principal } catch { Write-Error "Failed to set permissions on key: ${PSItem}" exit 1 } ================================================ FILE: plugins/hosts/windows/scripts/unset_share.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 ForEach ($share_name in $args) { $result = net share $share_name /DELETE /YES if ($LastExitCode -ne 0) { Write-Output "share name: ${share_name}" Write-Error "error - ${result}" exit 1 } } Write-Output "share removal completed" exit 0 ================================================ FILE: plugins/hosts/windows/scripts/utils/VagrantSSH/VagrantSSH.psm1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # Vagrant SSH capability functions function Set-SSHKeyPermissions { param ( [parameter(Mandatory=$true)] [string] $SSHKeyPath, [parameter(Mandatory=$false)] [string] $Principal=$null ) if(!$Principal) { $Principal = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name } # Create the new ACL we want to apply $NewAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( $Principal, "FullControl", "None", "None", "Allow") $ACL = Get-ACL "${SSHKeyPath}" # Disable inherited rules $ACL.SetAccessRuleProtection($true, $false) # Scrub all existing ACLs from the file $ACL.Access | %{$ACL.RemoveAccessRule($_)} # Apply the new ACL $ACL.SetAccessRule($NewAccessRule) Set-ACL "${SSHKeyPath}" $ACL } ================================================ FILE: plugins/kernel_v1/config/nfs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Kernel_V1 class NFSConfig < Vagrant.plugin("1", :config) attr_accessor :map_uid attr_accessor :map_gid def initialize @map_uid = UNSET_VALUE @map_gid = UNSET_VALUE end def upgrade(new) new.nfs.map_uid = @map_uid if @map_uid != UNSET_VALUE new.nfs.map_gid = @map_gid if @map_gid != UNSET_VALUE end end end end ================================================ FILE: plugins/kernel_v1/config/package.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Kernel_V1 class PackageConfig < Vagrant.plugin("1", :config) attr_accessor :name def initialize @name = UNSET_VALUE end def upgrade(new) new.package.name = @name if @name != UNSET_VALUE end end end end ================================================ FILE: plugins/kernel_v1/config/ssh.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Kernel_V1 class SSHConfig < Vagrant.plugin("1", :config) attr_accessor :username attr_accessor :password attr_accessor :host attr_accessor :port attr_accessor :guest_port attr_accessor :max_tries attr_accessor :timeout attr_accessor :private_key_path attr_accessor :forward_agent attr_accessor :forward_x11 attr_accessor :forward_env attr_accessor :shell def initialize @username = UNSET_VALUE @password = UNSET_VALUE @host = UNSET_VALUE @port = UNSET_VALUE @guest_port = UNSET_VALUE @max_tries = UNSET_VALUE @timeout = UNSET_VALUE @private_key_path = UNSET_VALUE @forward_agent = UNSET_VALUE @forward_x11 = UNSET_VALUE @forward_env = UNSET_VALUE @shell = UNSET_VALUE end def upgrade(new) new.ssh.username = @username if @username != UNSET_VALUE new.ssh.host = @host if @host != UNSET_VALUE new.ssh.port = @port if @port != UNSET_VALUE new.ssh.guest_port = @guest_port if @guest_port != UNSET_VALUE new.ssh.private_key_path = @private_key_path if @private_key_path != UNSET_VALUE new.ssh.forward_agent = @forward_agent if @forward_agent != UNSET_VALUE new.ssh.forward_x11 = @forward_x11 if @forward_x11 != UNSET_VALUE new.ssh.forward_env = @forward_env if @forward_env != UNSET_VALUE new.ssh.shell = @shell if @shell != UNSET_VALUE end end end end ================================================ FILE: plugins/kernel_v1/config/vagrant.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Kernel_V1 class VagrantConfig < Vagrant.plugin("1", :config) attr_accessor :dotfile_name attr_accessor :host def initialize @dotfile_name = UNSET_VALUE @host = UNSET_VALUE end def finalize! @dotfile_name = nil if @dotfile_name == UNSET_VALUE @host = nil if @host == UNSET_VALUE end def upgrade(new) new.vagrant.host = @host if @host.nil? warnings = [] if @dotfile_name warnings << "`config.vm.dotfile_name` has no effect anymore." end [warnings, []] end end end end ================================================ FILE: plugins/kernel_v1/config/vm.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Kernel_V1 # This is the Version 1.0.x Vagrant VM configuration. This is # _outdated_ and exists purely to be upgraded over to the new V2 # format. class VMConfig < Vagrant.plugin("1", :config) DEFAULT_VM_NAME = :default attr_accessor :name attr_accessor :auto_port_range attr_accessor :base_mac attr_accessor :boot_mode attr_accessor :box attr_accessor :box_url attr_accessor :guest attr_accessor :host_name attr_reader :customizations attr_reader :networks attr_reader :provisioners attr_reader :shared_folders def initialize @shared_folders = {} @networks = [] @provisioners = [] @customizations = [] @define_calls = [] end def forward_port(guestport, hostport, options=nil) options ||= {} # Build up the network options for V2 network_options = {} network_options[:virtualbox__adapter] = options[:adapter] network_options[:virtualbox__protocol] = options[:protocol] # Just append the forwarded port to the networks @networks << [:forwarded_port, [guestport, hostport, network_options]] end def share_folder(name, guestpath, hostpath, opts=nil) @shared_folders[name] = { guestpath: guestpath.to_s, hostpath: hostpath.to_s, create: false, owner: nil, group: nil, nfs: false, transient: false, extra: nil }.merge(opts || {}) end def network(type, *args) # Convert to symbol so we can allow most anything... type = type.to_sym if type if type == :hostonly @networks << [:private_network, args] elsif type == :bridged @networks << [:public_network, args] else @networks << [:unknown, type] end end def provision(name, options=nil, &block) @provisioners << [name, options, block] end # This argument is nil only because the old style was deprecated and # we didn't want to break Vagrantfiles. This was never removed and # since we've moved onto V2 configuration, we might as well keep this # around forever. def customize(command=nil) @customizations << command if command end def define(name, options=nil, &block) # Force the V1 config on these calls options ||= {} options[:config_version] = "1" @define_calls << [name, options, block] end def finalize! # If we haven't defined a single VM, then we need to define a # default VM which just inherits the rest of the configuration. define(DEFAULT_VM_NAME) if defined_vm_keys.empty? end # Upgrade to a V2 configuration def upgrade(new) warnings = [] new.vm.base_mac = self.base_mac if self.base_mac new.vm.box = self.box if self.box new.vm.box_url = self.box_url if self.box_url new.vm.guest = self.guest if self.guest new.vm.hostname = self.host_name if self.host_name new.vm.usable_port_range = self.auto_port_range if self.auto_port_range if self.boot_mode new.vm.provider :virtualbox do |vb| # Enable the GUI if the boot mode is GUI. vb.gui = (self.boot_mode.to_s == "gui") end end # If we have VM customizations, then we enable them on the # VirtualBox provider on the new VM. if !self.customizations.empty? warnings << "`config.vm.customize` calls are VirtualBox-specific. If you're\n" + "using any other provider, you'll have to use config.vm.provider in a\n" + "v2 configuration block." new.vm.provider :virtualbox do |vb| self.customizations.each do |customization| vb.customize(customization) end end end # Re-define all networks. self.networks.each do |type, args| if type == :unknown warnings << "Unknown network type '#{args}' will be ignored." next end options = {} options = args.pop.dup if args.last.is_a?(Hash) # Determine the extra options we need to set for each type if type == :forwarded_port options[:guest] = args[0] options[:host] = args[1] elsif type == :private_network options[:ip] = args[0] end new.vm.network(type, options) end # Provisioners self.provisioners.each do |name, options, block| options ||= {} new.vm.provision(name, **options, &block) end # Shared folders self.shared_folders.each do |name, sf| options = sf.dup options[:id] = name guestpath = options.delete(:guestpath) hostpath = options.delete(:hostpath) # This was the name of the old default /vagrant shared folder. # We warn the use that this changed, but also silently change # it to try to make things work properly. if options[:id] == "v-root" warnings << "The 'v-root' shared folders have been renamed to 'vagrant-root'.\n" + "Assuming you meant 'vagrant-root'..." options[:id] = "vagrant-root" end new.vm.synced_folder(hostpath, guestpath, options) end # Defined sub-VMs @define_calls.each do |name, options, block| new.vm.define(name, options, &block) end # If name is used, warn that it has no effect anymore if @name warnings << "`config.vm.name` has no effect anymore. Names are derived\n" + "directly from any `config.vm.define` calls." end [warnings, []] end end end end ================================================ FILE: plugins/kernel_v1/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Kernel_V1 # This is the "kernel" of Vagrant and contains the configuration classes # that make up the core of Vagrant. class Plugin < Vagrant.plugin("1") name "kernel" description <<-DESC The kernel of Vagrant. This plugin contains required items for even basic functionality of Vagrant version 1. DESC # Core configuration keys provided by the kernel. Note that all # the kernel configuration classes are marked as _upgrade safe_ (the # true 2nd param). This means that these can be loaded in ANY version # of the core of Vagrant. config("ssh", true) do require File.expand_path("../config/ssh", __FILE__) SSHConfig end config("nfs", true) do require File.expand_path("../config/nfs", __FILE__) NFSConfig end config("package", true) do require File.expand_path("../config/package", __FILE__) PackageConfig end config("vagrant", true) do require File.expand_path("../config/vagrant", __FILE__) VagrantConfig end config("vm", true) do require File.expand_path("../config/vm", __FILE__) VMConfig end end end end ================================================ FILE: plugins/kernel_v2/config/cloud_init.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "securerandom" module VagrantPlugins module Kernel_V2 class VagrantConfigCloudInit < Vagrant.plugin("2", :config) #------------------------------------------------------------------- # Config class for cloud-init #------------------------------------------------------------------- DEFAULT_CONTENT_TYPES = ["text/cloud-boothook", "text/cloud-config", "text/cloud-config-archive", "text/jinja2", "text/part-handler", "text/upstart-job", "text/x-include-once-url", "text/x-include-url", "text/x-shellscript"].map(&:freeze).freeze DEFAULT_CONFIG_TYPE = :user_data # @note This value is for internal use only # # @return [String] attr_reader :id # The 'type' of data being stored. If not defined, # will default to :user_data # # @return [Symbol] attr_accessor :type # @return [String] attr_accessor :content_type # The optional mime-part content-disposition filename. # # @return [String] attr_accessor :content_disposition_filename # @return [String] attr_accessor :path # @return [String] attr_accessor :inline def initialize(type=nil) @logger = Log4r::Logger.new("vagrant::config::vm::cloud_init") @type = type if type @content_type = UNSET_VALUE @content_disposition_filename = UNSET_VALUE @path = UNSET_VALUE @inline = UNSET_VALUE # Internal options @id = SecureRandom.uuid end def finalize! if !@type @type = DEFAULT_CONFIG_TYPE else @type = @type.to_sym end @content_type = nil if @content_type == UNSET_VALUE @content_disposition_filename = nil if @content_disposition_filename == UNSET_VALUE @path = nil if @path == UNSET_VALUE @inline = nil if @inline == UNSET_VALUE end # @return [Array] array of strings of error messages from config option validation def validate(machine) errors = _detected_errors if @type && @type != DEFAULT_CONFIG_TYPE errors << I18n.t("vagrant.cloud_init.incorrect_type_set", type: @type, machine: machine.name, default_type: DEFAULT_CONFIG_TYPE) end if !@content_type errors << I18n.t("vagrant.cloud_init.content_type_not_set", machine: machine.name, accepted_types: DEFAULT_CONTENT_TYPES.join(', ')) elsif !DEFAULT_CONTENT_TYPES.include?(@content_type) errors << I18n.t("vagrant.cloud_init.incorrect_content_type", machine: machine.name, content_type: @content_type, accepted_types: DEFAULT_CONTENT_TYPES.join(', ')) end if @path && @inline errors << I18n.t("vagrant.cloud_init.path_and_inline_set", machine: machine.name) end if @path if !@path.is_a?(String) errors << I18n.t("vagrant.cloud_init.incorrect_path_type", machine: machine.name, path: @path, type: @path.class.name) else expanded_path = Pathname.new(@path).expand_path(machine.env.root_path) if !expanded_path.file? errors << I18n.t("vagrant.cloud_init.path_invalid", path: expanded_path, machine: machine.name) end end end if @inline if !@inline.is_a?(String) errors << I18n.t("vagrant.cloud_init.incorrect_inline_type", machine: machine.name, type: @inline.class.name) end end errors end # The String representation of this config. # # @return [String] def to_s "cloud_init config" end end end end ================================================ FILE: plugins/kernel_v2/config/disk.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "securerandom" require "vagrant/util/numeric" module VagrantPlugins module Kernel_V2 class VagrantConfigDisk < Vagrant.plugin("2", :config) #------------------------------------------------------------------- # Config class for a given Disk #------------------------------------------------------------------- DEFAULT_DISK_TYPES = [:disk, :dvd, :floppy].freeze FILE_CHAR_REGEX = /[^-a-z0-9_]/i.freeze # Note: This value is for internal use only # # @return [String] attr_reader :id # File name for the given disk. Defaults to a generated name that is: # # vagrant__ # # @return [String] attr_accessor :name # Type of disk to create. Defaults to `:disk` # # @return [Symbol] attr_accessor :type # Type of disk extension to create. Defaults to `vdi` # # @return [String] attr_accessor :disk_ext # Size of disk to create # # @return [Integer,String] attr_accessor :size # Path to the location of the disk file (Optional for `:disk` type, # required for `:dvd` type.) # # @return [String] attr_accessor :file # Determines if this disk is the _main_ disk, or an attachment. # Defaults to true. # # @return [Boolean] attr_accessor :primary # Provider specific options # # @return [Hash] attr_accessor :provider_config def initialize(type) @logger = Log4r::Logger.new("vagrant::config::vm::disk") @type = type @provider_config = {} @name = UNSET_VALUE @provider_type = UNSET_VALUE @size = UNSET_VALUE @primary = UNSET_VALUE @file = UNSET_VALUE @disk_ext = UNSET_VALUE # Internal options @id = SecureRandom.uuid end # Helper method for storing provider specific config options # # Expected format is: # # - `provider__diskoption: value` # - `{provider: {diskoption: value, otherdiskoption: value, ...}` # # Duplicates will be overriden # # @param [Hash] options def add_provider_config(**options, &block) current = {} options.each do |k,v| opts = k.to_s.split("__") if opts.size == 2 current[opts[0].to_sym] = {opts[1].to_sym => v} elsif v.is_a?(Hash) current[k] = v else @logger.warn("Disk option '#{k}' found that does not match expected provider disk config schema.") end end current = @provider_config.merge(current) if !@provider_config.empty? if current @provider_config = current else @provider_config = {} end end def finalize! # Ensure all config options are set to nil or default value if untouched # by user @type = :disk if @type == UNSET_VALUE @size = nil if @size == UNSET_VALUE @file = nil if @file == UNSET_VALUE if @primary == UNSET_VALUE @primary = false end if @name.is_a?(String) && @name.match(FILE_CHAR_REGEX) @logger.warn("Vagrant will remove detected invalid characters in '#{@name}' and convert the disk name into something usable for a file") @name.gsub!(FILE_CHAR_REGEX, "_") elsif @name == UNSET_VALUE if @primary @name = "vagrant_primary" else @name = nil end end end # @return [Array] array of strings of error messages from config option validation def validate(machine) errors = _detected_errors # validate type with list of known disk types if !DEFAULT_DISK_TYPES.include?(@type) errors << I18n.t("vagrant.config.disk.invalid_type", type: @type, types: DEFAULT_DISK_TYPES.join(', ')) end if @disk_ext == UNSET_VALUE if machine.provider.capability?(:set_default_disk_ext) @disk_ext = machine.provider.capability(:set_default_disk_ext) else @logger.warn("No provider capability defined to set default 'disk_ext' type. Will use 'vdi' for disk extension.") @disk_ext = "vdi" end elsif @disk_ext @disk_ext = @disk_ext.downcase if machine.provider.capability?(:validate_disk_ext) if !machine.provider.capability(:validate_disk_ext, @disk_ext) if machine.provider.capability?(:default_disk_exts) disk_exts = machine.provider.capability(:default_disk_exts).join(', ') else disk_exts = "not found" end errors << I18n.t("vagrant.config.disk.invalid_ext", ext: @disk_ext, name: @name, exts: disk_exts) end else @logger.warn("No provider capability defined to validate 'disk_ext' type") end end if @size && !@size.is_a?(Integer) if @size.is_a?(String) @size = Vagrant::Util::Numeric.string_to_bytes(@size) end end if !@size && type == :disk errors << I18n.t("vagrant.config.disk.invalid_size", name: @name, machine: machine.name) end if @type == :dvd && !@file errors << I18n.t("vagrant.config.disk.dvd_type_file_required", name: @name, machine: machine.name) end if @type == :dvd && @primary errors << I18n.t("vagrant.config.disk.dvd_type_primary", name: @name, machine: machine.name) end if @file if !@file.is_a?(String) errors << I18n.t("vagrant.config.disk.invalid_file_type", file: @file, machine: machine.name) elsif !File.file?(@file) errors << I18n.t("vagrant.config.disk.missing_file", file_path: @file, name: @name, machine: machine.name) end end if @provider_config if !@provider_config.empty? if !@provider_config.key?(machine.provider_name) machine.env.ui.warn(I18n.t("vagrant.config.disk.missing_provider", machine: machine.name, provider_name: machine.provider_name)) end end end if !@name errors << I18n.t("vagrant.config.disk.no_name_set", machine: machine.name) end errors end # The String representation of this Disk. # # @return [String] def to_s "disk config" end end end end ================================================ FILE: plugins/kernel_v2/config/package.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Kernel_V2 class PackageConfig < Vagrant.plugin("2", :config) attr_accessor :name def initialize @name = UNSET_VALUE end def finalize! @name = nil if @name == UNSET_VALUE end def to_s "Package" end end end end ================================================ FILE: plugins/kernel_v2/config/push.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Kernel_V2 class PushConfig < Vagrant.plugin("2", :config) VALID_OPTIONS = [:strategy].freeze attr_accessor :name def initialize @logger = Log4r::Logger.new("vagrant::config::push") # Internal state @__defined_pushes = {} @__compiled_pushes = {} @__finalized = false end def finalize! @logger.debug("finalizing") # Compile all the provider configurations @__defined_pushes.each do |name, tuples| # Capture the strategy so we can use it later. This will be used in # the block iteration for merging/overwriting strategy = name strategy = tuples[0][0] if tuples[0] # Find the configuration class for this push config_class = Vagrant.plugin("2").manager.push_configs[strategy] config_class ||= Vagrant::Config::V2::DummyConfig # Load it up config = config_class.new begin tuples.each do |s, b| # Update the strategy if it has changed, resetting the current # config object. if s != strategy @logger.warn("duplicate strategy defined, overwriting config") strategy = s config = config_class.new end # If we don't have any blocks, then ignore it next if b.nil? new_config = config_class.new b.call(new_config, Vagrant::Config::V2::DummyConfig.new) config = config.merge(new_config) end rescue Exception => e raise Vagrant::Errors::VagrantfileLoadError, path: "", message: e.message end config.finalize! # It's important that we call _finalize! here also, because pushes get # plucked out of the config in Environment#push without the larger # root.finalize! walk having been done. This means that push configs # were coming out unfinalized, which can cause havoc when they're # passed through functions that attempt to capture keyword arguments, # as they'll cause ruby to call .to_hash on the config, get a # DummyConfig, and then blow up. That havoc was happening in server # mode, and this call fixes it. config._finalize! # Store it for retrieval later @__compiled_pushes[name] = [strategy, config] end @__finalized = true end # Define a new push in the Vagrantfile with the given name. # # @example # vm.push.define "ftp" # # @example # vm.push.define "ftp" do |s| # s.host = "..." # end # # @example # vm.push.define "production", strategy: "docker" do |s| # # ... # end # # @param [#to_sym] name The name of the this strategy. By default, this # is also the name of the strategy, but the `:strategy` key can be given # to customize this behavior # @param [Hash] options The list of options # def define(name, **options, &block) name = name.to_sym strategy = options[:strategy] || name @__defined_pushes[name] ||= [] @__defined_pushes[name] << [strategy.to_sym, block] end # The String representation of this Push. # # @return [String] def to_s "Push" end # Custom merge method def merge(other) super.tap do |result| other_pushes = other.instance_variable_get(:@__defined_pushes) new_pushes = @__defined_pushes.dup other_pushes.each do |key, tuples| new_pushes[key] ||= [] new_pushes[key] += tuples end result.instance_variable_set(:@__defined_pushes, new_pushes) end end # Validate all pushes def validate(machine) errors = { "push" => _detected_errors } __compiled_pushes.each do |_, push| config = push[1] push_errors = config.validate(machine) if push_errors errors = Vagrant::Config::V2::Util.merge_errors(errors, push_errors) end end errors end # This returns the list of compiled pushes as a hash by name. # # @return [Hash>] def __compiled_pushes raise "Must finalize first!" if !@__finalized @__compiled_pushes.dup end end end end ================================================ FILE: plugins/kernel_v2/config/ssh.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" require_relative "ssh_connect" module VagrantPlugins module Kernel_V2 class SSHConfig < SSHConnectConfig attr_accessor :forward_agent attr_accessor :forward_x11 attr_accessor :forward_env attr_accessor :guest_port attr_accessor :keep_alive attr_accessor :shell attr_accessor :proxy_command attr_accessor :ssh_command attr_accessor :pty attr_accessor :sudo_command attr_accessor :export_command_template attr_reader :default def initialize super @forward_agent = UNSET_VALUE @forward_x11 = UNSET_VALUE @forward_env = UNSET_VALUE @guest_port = UNSET_VALUE @keep_alive = UNSET_VALUE @proxy_command = UNSET_VALUE @ssh_command = UNSET_VALUE @pty = UNSET_VALUE @shell = UNSET_VALUE @sudo_command = UNSET_VALUE @export_command_template = UNSET_VALUE @default = SSHConnectConfig.new end def merge(other) super.tap do |result| merged_defaults = @default.merge(other.default) result.instance_variable_set(:@default, merged_defaults) end end def finalize! super @forward_agent = false if @forward_agent == UNSET_VALUE @forward_x11 = false if @forward_x11 == UNSET_VALUE @forward_env = false if @forward_env == UNSET_VALUE @guest_port = 22 if @guest_port == UNSET_VALUE @keep_alive = true if @keep_alive == UNSET_VALUE @proxy_command = nil if @proxy_command == UNSET_VALUE @ssh_command = nil if @ssh_command == UNSET_VALUE @pty = false if @pty == UNSET_VALUE @shell = "bash -l" if @shell == UNSET_VALUE if @export_command_template == UNSET_VALUE @export_command_template = 'export %ENV_KEY%="%ENV_VALUE%"' end if @sudo_command == UNSET_VALUE @sudo_command = "sudo -E -H %c" end @default.username = "vagrant" if @default.username == UNSET_VALUE @default.port = @guest_port if @default.port == UNSET_VALUE @default.finalize! end def to_s "SSH" end def validate(machine) errors = super # Return the errors result = { to_s => errors } # Figure out the errors for the defaults default_errors = @default.validate(machine) result["SSH Defaults"] = default_errors if !default_errors.empty? result end end end end ================================================ FILE: plugins/kernel_v2/config/ssh_connect.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Kernel_V2 class SSHConnectConfig < Vagrant.plugin("2", :config) DEFAULT_SSH_CONNECT_RETRIES = 5 DEFAULT_SSH_CONNECT_RETRY_DELAY = 2 DEFAULT_SSH_CONNECT_TIMEOUT = 15 attr_accessor :host attr_accessor :port attr_accessor :config attr_accessor :connect_retries attr_accessor :connect_retry_delay attr_accessor :connect_timeout attr_accessor :private_key_path attr_accessor :username attr_accessor :password attr_accessor :insert_key attr_accessor :keys_only attr_accessor :key_type attr_accessor :paranoid attr_accessor :verify_host_key attr_accessor :compression attr_accessor :dsa_authentication attr_accessor :extra_args attr_accessor :remote_user attr_accessor :disable_deprecated_algorithms def initialize @host = UNSET_VALUE @port = UNSET_VALUE @config = UNSET_VALUE @connect_retries = UNSET_VALUE @connect_retry_delay = UNSET_VALUE @connect_timeout = UNSET_VALUE @private_key_path = UNSET_VALUE @username = UNSET_VALUE @password = UNSET_VALUE @insert_key = UNSET_VALUE @keys_only = UNSET_VALUE @key_type = UNSET_VALUE @paranoid = UNSET_VALUE @verify_host_key = UNSET_VALUE @compression = UNSET_VALUE @dsa_authentication = UNSET_VALUE @extra_args = UNSET_VALUE @remote_user = UNSET_VALUE @disable_deprecated_algorithms = UNSET_VALUE end def finalize! @host = nil if @host == UNSET_VALUE @port = nil if @port == UNSET_VALUE @private_key_path = nil if @private_key_path == UNSET_VALUE @username = nil if @username == UNSET_VALUE @password = nil if @password == UNSET_VALUE @insert_key = true if @insert_key == UNSET_VALUE @keys_only = true if @keys_only == UNSET_VALUE @key_type = :auto if @key_type == UNSET_VALUE @paranoid = false if @paranoid == UNSET_VALUE @verify_host_key = :never if @verify_host_key == UNSET_VALUE @compression = true if @compression == UNSET_VALUE @dsa_authentication = true if @dsa_authentication == UNSET_VALUE @extra_args = nil if @extra_args == UNSET_VALUE @config = nil if @config == UNSET_VALUE @disable_deprecated_algorithms = false if @disable_deprecated_algorithms == UNSET_VALUE @connect_timeout = DEFAULT_SSH_CONNECT_TIMEOUT if @connect_timeout == UNSET_VALUE @connect_retries = DEFAULT_SSH_CONNECT_RETRIES if @connect_retries == UNSET_VALUE @connect_retry_delay = DEFAULT_SSH_CONNECT_RETRY_DELAY if @connect_retry_delay == UNSET_VALUE if @private_key_path && !@private_key_path.is_a?(Array) @private_key_path = [@private_key_path] end if @remote_user == UNSET_VALUE if @username @remote_user = @username else @remote_user = nil end end if @paranoid @verify_host_key = @paranoid end # Values for verify_host_key changed in 5.0.0 of net-ssh. If old value # detected, update with new value case @verify_host_key when true @verify_host_key = :accepts_new_or_local_tunnel when false @verify_host_key = :never when :very @verify_host_key = :accept_new when :secure @verify_host_key = :always end # Attempt to convert timeout to integer value # If we can't convert the connect timeout into an integer or # if the value is less than 1, set it to the default value begin @connect_timeout = @connect_timeout.to_i rescue # ignore end if @key_type @key_type = @key_type.to_sym end end # NOTE: This is _not_ a valid config validation method, since it # returns an _array_ of strings rather than a Hash. This is meant to # be used with a subclass that handles this. # # @return [Array] def validate(machine) errors = _detected_errors if @private_key_path @private_key_path.each do |raw_path| path = File.expand_path(raw_path, machine.env.root_path) if !File.file?(path) errors << I18n.t( "vagrant.config.ssh.private_key_missing", path: raw_path) end end end if @config config_path = File.expand_path(@config, machine.env.root_path) if !File.file?(config_path) errors << I18n.t( "vagrant.config.ssh.ssh_config_missing", path: @config) end end if @paranoid machine.env.ui.warn(I18n.t("vagrant.config.ssh.paranoid_deprecated")) end if !@connect_timeout.is_a?(Integer) errors << I18n.t( "vagrant.config.ssh.connect_timeout_invalid_type", given: @connect_timeout.class.name) elsif @connect_timeout < 1 errors << I18n.t( "vagrant.config.ssh.connect_timeout_invalid_value", given: @connect_timeout.to_s) end if !@connect_retries.is_a?(Integer) errors << I18n.t( "vagrant.config.ssh.connect_retries_invalid_type", given: @connect_retries.class.name ) elsif @connect_retries < 0 errors << I18n.t( "vagrant.config.ssh.connect_retries_invalid_value", given: @connect_retries.to_s ) end if !@connect_retry_delay.is_a?(Numeric) errors << I18n.t( "vagrant.config.ssh.connect_retry_delay_invalid_type", given: @connect_retry_delay.class.name ) elsif @connect_retry_delay < 0 errors << I18n.t( "vagrant.config.ssh.connect_retry_delay_invalid_value", given: @connect_retry_delay.to_s ) end if @key_type != :auto && !Vagrant::Util::Keypair.valid_type?(@key_type) errors << I18n.t( "vagrant.config.ssh.connect_invalid_key_type", given: @key_type.to_s, supported: Vagrant::Util::Keypair.available_types.join(", ") ) end errors end end end end ================================================ FILE: plugins/kernel_v2/config/trigger.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" require File.expand_path("../vm_trigger", __FILE__) module VagrantPlugins module Kernel_V2 class TriggerConfig < Vagrant.plugin("2", :config) # The TriggerConfig class is what gets called when a user # defines a new trigger in their Vagrantfile. The two entry points are # either `config.trigger.before` or `config.trigger.after`. def initialize @logger = Log4r::Logger.new("vagrant::config::trigger") # Internal State @_before_triggers = [] # An array of VagrantConfigTrigger objects @_after_triggers = [] # An array of VagrantConfigTrigger objects end #------------------------------------------------------------------- # Trigger before/after functions #------------------------------------------------------------------- # # Commands are expected to be ether: # - splat # + config.trigger.before :up, :destroy, :halt do |trigger|.... # - array # + config.trigger.before [:up, :destroy, :halt] do |trigger|.... # # Config is expected to be given as a block, or the last parameter as a hash # # - block # + config.trigger.before :up, :destroy, :halt do |trigger| # trigger.option = "option" # end # - hash # + config.trigger.before :up, :destroy, :halt, options: "option" # Reads in and parses Vagrant command whitelist and settings for a defined # trigger # # @param [Symbol] command Vagrant command to create trigger on # @param [Block] block The defined before block def before(*command, &block) command.flatten! blk = block if command.last.is_a?(Hash) if block_given? extra_cfg = command.pop else # We were given a hash rather than a block, # so the last element should be the "config block" # and the rest are commands for the trigger blk = command.pop end elsif !block_given? raise Vagrant::Errors::TriggersNoBlockGiven, command: command end command.each do |cmd| trigger = create_trigger(cmd, blk, extra_cfg) @_before_triggers << trigger end end # Reads in and parses Vagrant command whitelist and settings for a defined # trigger # # @param [Symbol] command Vagrant command to create trigger on # @param [Block] block The defined after block def after(*command, &block) command.flatten! blk = block if command.last.is_a?(Hash) if block_given? extra_cfg = command.pop else # We were given a hash rather than a block, # so the last element should be the "config block" # and the rest are commands for the trigger blk = command.pop end elsif !block_given? raise Vagrant::Errors::TriggersNoBlockGiven, command: command end command.each do |cmd| trigger = create_trigger(cmd, blk, extra_cfg) @_after_triggers << trigger end end #------------------------------------------------------------------- # Internal methods, don't call these. #------------------------------------------------------------------- # Creates a new trigger config. If a block is given, parse that block # by calling it with the created trigger. Otherwise set the options if it's # a hash. # # @param [Symbol] command Vagrant command to create trigger on # @param [Block] block The defined config block # @param [Hash] extra_cfg Extra configurations for a block defined trigger (Optional) # @return [VagrantConfigTrigger] def create_trigger(command, block, extra_cfg=nil) trigger = VagrantConfigTrigger.new(command) if block.is_a?(Hash) trigger.set_options(block) else block.call(trigger, VagrantConfigTrigger) trigger.set_options(extra_cfg) if extra_cfg end return trigger end def merge(other) super.tap do |result| new_before_triggers = [] new_after_triggers = [] other_defined_before_triggers = other.instance_variable_get(:@_before_triggers) other_defined_after_triggers = other.instance_variable_get(:@_after_triggers) @_before_triggers.each do |bt| other_bft = other_defined_before_triggers.find { |o| bt.id == o.id } if other_bft # Override, take it other_bft = bt.merge(other_bft) # Preserve order, always bt = other_bft other_defined_before_triggers.delete(other_bft) end new_before_triggers << bt.dup end other_defined_before_triggers.each do |obt| new_before_triggers << obt.dup end result.instance_variable_set(:@_before_triggers, new_before_triggers) @_after_triggers.each do |at| other_aft = other_defined_after_triggers.find { |o| at.id == o.id } if other_aft # Override, take it other_aft = at.merge(other_aft) # Preserve order, always at = other_aft other_defined_after_triggers.delete(other_aft) end new_after_triggers << at.dup end other_defined_after_triggers.each do |oat| new_after_triggers << oat.dup end result.instance_variable_set(:@_after_triggers, new_after_triggers) end end # Iterates over all defined triggers and finalizes their config objects def finalize! if !@_before_triggers.empty? @_before_triggers.map { |t| t.finalize! } end if !@_after_triggers.empty? @_after_triggers.map { |t| t.finalize! } end end # Validate Trigger Arrays def validate(machine) errors = _detected_errors @_before_triggers.each do |bt| error = bt.validate(machine) errors.concat error if !error.empty? end @_after_triggers.each do |at| error = at.validate(machine) errors.concat error if !error.empty? end {"trigger" => errors} end # return [Array] def before_triggers @_before_triggers end # return [Array] def after_triggers @_after_triggers end # The String representation of this Trigger. # # @return [String] def to_s "trigger" end end end end ================================================ FILE: plugins/kernel_v2/config/vagrant.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Kernel_V2 class VagrantConfig < Vagrant.plugin("2", :config) attr_accessor :host attr_accessor :sensitive attr_accessor :plugins VALID_PLUGIN_KEYS = ["sources", "version", "entry_point"].map(&:freeze).freeze INVALID_PLUGIN_FORMAT = :invalid_plugin_format def initialize @host = UNSET_VALUE @sensitive = UNSET_VALUE @plugins = UNSET_VALUE end def finalize! @host = :detect if @host == UNSET_VALUE @host = @host.to_sym if @host @sensitive = nil if @sensitive == UNSET_VALUE if @plugins == UNSET_VALUE @plugins = {} else @plugins = format_plugins(@plugins) end if @sensitive.is_a?(Array) || @sensitive.is_a?(String) Array(@sensitive).each do |value| Vagrant::Util::CredentialScrubber.sensitive(value.to_s) end end end # Validate the configuration # # @param [Vagrant::Machine, NilClass] machine Machine instance or nil # @return [Hash] def validate(machine) errors = _detected_errors if @sensitive && (!@sensitive.is_a?(Array) && !@sensitive.is_a?(String)) errors << I18n.t("vagrant.config.root.sensitive_bad_type") end if @plugins == INVALID_PLUGIN_FORMAT errors << I18n.t("vagrant.config.root.plugins_invalid_format") else @plugins.each do |plugin_name, plugin_info| if plugin_info.is_a?(Hash) invalid_keys = plugin_info.keys - VALID_PLUGIN_KEYS if !invalid_keys.empty? errors << I18n.t("vagrant.config.root.plugins_bad_key", plugin_name: plugin_name, plugin_key: invalid_keys.join(", ") ) end else errors << I18n.t("vagrant.config.root.plugins_invalid_format") end end end {"vagrant" => errors} end def to_s "Vagrant" end def format_plugins(val) case val when String {val => Vagrant::Util::HashWithIndifferentAccess.new} when Array val.inject(Vagrant::Util::HashWithIndifferentAccess.new) { |memo, item| memo.merge(format_plugins(item)) } when Hash Vagrant::Util::HashWithIndifferentAccess.new.tap { |h| val.each_pair { |k, v| if v.is_a?(Hash) h[k] = Vagrant::Util::HashWithIndifferentAccess.new(v) else h[k] = v end } } else INVALID_PLUGIN_FORMAT end end end end end ================================================ FILE: plugins/kernel_v2/config/vm.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "securerandom" require "set" require "vagrant" require "vagrant/action/builtin/mixin_synced_folders" require "vagrant/config/v2/util" require "vagrant/util/platform" require "vagrant/util/presence" require "vagrant/util/experimental" require "vagrant/util/map_command_options" require File.expand_path("../vm_provisioner", __FILE__) require File.expand_path("../vm_subvm", __FILE__) require File.expand_path("../disk", __FILE__) require File.expand_path("../cloud_init", __FILE__) module VagrantPlugins module Kernel_V2 class VMConfig < Vagrant.plugin("2", :config) include Vagrant::Util::Presence DEFAULT_VM_NAME = :default attr_accessor :allowed_synced_folder_types attr_accessor :allow_fstab_modification attr_accessor :allow_hosts_modification attr_accessor :base_mac attr_accessor :base_address attr_accessor :boot_timeout attr_accessor :box attr_accessor :box_architecture attr_accessor :ignore_box_vagrantfile attr_accessor :box_check_update attr_accessor :box_url attr_accessor :box_server_url attr_accessor :box_version attr_accessor :box_download_ca_cert attr_accessor :box_download_ca_path attr_accessor :box_download_checksum attr_accessor :box_download_checksum_type attr_accessor :box_download_client_cert attr_accessor :box_download_disable_ssl_revoke_best_effort 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 attr_accessor :hostname attr_accessor :post_up_message attr_accessor :usable_port_range attr_reader :provisioners attr_reader :disks attr_reader :cloud_init_configs attr_reader :box_extra_download_options # This is an experimental feature that isn't public yet. attr_accessor :clone def initialize @logger = Log4r::Logger.new("vagrant::config::vm") @allowed_synced_folder_types = UNSET_VALUE @allow_fstab_modification = UNSET_VALUE @base_mac = UNSET_VALUE @base_address = UNSET_VALUE @boot_timeout = UNSET_VALUE @box = UNSET_VALUE @box_architecture = UNSET_VALUE @ignore_box_vagrantfile = UNSET_VALUE @box_check_update = UNSET_VALUE @box_download_ca_cert = UNSET_VALUE @box_download_ca_path = UNSET_VALUE @box_download_checksum = UNSET_VALUE @box_download_checksum_type = UNSET_VALUE @box_download_client_cert = UNSET_VALUE @box_download_disable_ssl_revoke_best_effort = UNSET_VALUE @box_download_insecure = UNSET_VALUE @box_download_location_trusted = UNSET_VALUE @box_download_options = UNSET_VALUE @box_extra_download_options = UNSET_VALUE @box_url = UNSET_VALUE @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 @hostname = UNSET_VALUE @post_up_message = UNSET_VALUE @provisioners = [] @disks = [] @cloud_init_configs = [] @usable_port_range = UNSET_VALUE # Internal state @__compiled_provider_configs = {} @__defined_vm_keys = [] @__defined_vms = {} @__finalized = false @__networks = {} @__providers = {} @__provider_order = [] @__provider_overrides = {} @__synced_folders = {} end # This was from V1, but we just kept it here as an alias for hostname # because too many people mess this up. def host_name=(value) @hostname = value end # Custom merge method since some keys here are merged differently. def merge(other) super.tap do |result| other_networks = other.instance_variable_get(:@__networks) result.instance_variable_set(:@__networks, @__networks.merge(other_networks)) # Merge defined VMs by first merging the defined VM keys, # preserving the order in which they were defined. other_defined_vm_keys = other.instance_variable_get(:@__defined_vm_keys) other_defined_vm_keys -= @__defined_vm_keys new_defined_vm_keys = @__defined_vm_keys + other_defined_vm_keys # Merge the actual defined VMs. other_defined_vms = other.instance_variable_get(:@__defined_vms) new_defined_vms = {} @__defined_vms.each do |key, subvm| new_defined_vms[key] = subvm.clone end other_defined_vms.each do |key, subvm| if !new_defined_vms.key?(key) new_defined_vms[key] = subvm.clone else new_defined_vms[key].config_procs.concat(subvm.config_procs) new_defined_vms[key].options.merge!(subvm.options) end end # Merge defined disks other_disks = other.instance_variable_get(:@disks) new_disks = [] @disks.each do |p| other_p = other_disks.find { |o| p.id == o.id } if other_p # there is an override. take it. other_p.config = p.config.merge(other_p.config) # Remove duplicate disk config from other p = other_p other_disks.delete(other_p) end # there is an override, merge it into the new_disks << p.dup end other_disks.each do |p| new_disks << p.dup end result.instance_variable_set(:@disks, new_disks) # Merge defined cloud_init_configs other_cloud_init_configs = other.instance_variable_get(:@cloud_init_configs) new_cloud_init_configs = [] @cloud_init_configs.each do |p| other_p = other_cloud_init_configs.find { |o| p.id == o.id } if other_p # there is an override. take it. other_p.config = p.config.merge(other_p.config) # Remove duplicate disk config from other p = other_p other_cloud_init_configs.delete(other_p) end # there is an override, merge it into the new_cloud_init_configs << p.dup end other_cloud_init_configs.each do |p| new_cloud_init_configs << p.dup end result.instance_variable_set(:@cloud_init_configs, new_cloud_init_configs) # Merge the providers by prepending any configuration blocks we # have for providers onto the new configuration. other_providers = other.instance_variable_get(:@__providers) new_providers = @__providers.dup other_providers.each do |key, blocks| new_providers[key] ||= [] new_providers[key] += blocks end # Merge the provider ordering. Anything defined in our CURRENT # scope is before anything else. other_order = other.instance_variable_get(:@__provider_order) new_order = @__provider_order.dup new_order = (new_order + other_order).uniq # Merge the provider overrides by appending them... other_overrides = other.instance_variable_get(:@__provider_overrides) new_overrides = @__provider_overrides.dup other_overrides.each do |key, blocks| new_overrides[key] ||= [] new_overrides[key] += blocks end # Merge provisioners. First we deal with overrides and making # sure the ordering is good there. Then we merge them. new_provs = [] other_provs = other.provisioners.dup @provisioners.each do |p| other_p = other_provs.find { |o| p.id == o.id } if other_p # There is an override. Take it. other_p.config = p.config.merge(other_p.config) other_p.run ||= p.run next if !other_p.preserve_order # We're preserving order, delete from other p = other_p other_provs.delete(other_p) end # There is an override, merge it into the new_provs << p.dup end other_provs.each do |p| new_provs << p.dup end result.instance_variable_set(:@provisioners, new_provs) # Merge synced folders. other_folders = other.instance_variable_get(:@__synced_folders) new_folders = {} @__synced_folders.each do |key, value| new_folders[key] = value.dup end other_folders.each do |id, options| new_folders[id] ||= {} new_folders[id].merge!(options) end result.instance_variable_set(:@__defined_vm_keys, new_defined_vm_keys) result.instance_variable_set(:@__defined_vms, new_defined_vms) result.instance_variable_set(:@__providers, new_providers) result.instance_variable_set(:@__provider_order, new_order) result.instance_variable_set(:@__provider_overrides, new_overrides) result.instance_variable_set(:@__synced_folders, new_folders) end end # Defines a synced folder pair. This pair of folders will be synced # to/from the machine. Note that if the machine you're using doesn't # support multi-directional syncing (perhaps an rsync backed synced # folder) then the host is always synced to the guest but guest data # may not be synced back to the host. # # @param [String] hostpath Path to the host folder to share. If this # is a relative path, it is relative to the location of the # Vagrantfile. # @param [String] guestpath Path on the guest to mount the shared # folder. # @param [Hash] options Additional options. def synced_folder(hostpath, guestpath, options=nil) if Vagrant::Util::Platform.windows? # On Windows, Ruby just uses normal '/' for path seps, so # just replace normal Windows style seps with Unix ones. hostpath = hostpath.to_s.gsub("\\", "/") end if guestpath.is_a?(Hash) options = guestpath guestpath = nil end options ||= {} if options[:nfs] options[:type] = :nfs options.delete(:nfs) end if options.has_key?(:name) synced_folder_name = options.delete(:name) else synced_folder_name = guestpath end options[:guestpath] = guestpath.to_s.gsub(/\/$/, '') if guestpath options[:hostpath] = hostpath options[:disabled] = false if !options.key?(:disabled) options = (@__synced_folders[options[:guestpath]] || {}). merge(options.dup) # Make sure the type is a symbol options[:type] = options[:type].to_sym if options[:type] @__synced_folders[synced_folder_name] = options end # Define a way to access the machine via a network. This exposes a # high-level abstraction for networking that may not directly map # 1-to-1 for every provider. For example, AWS has no equivalent to # "port forwarding." But most providers will attempt to implement this # in a way that behaves similarly. # # `type` can be one of: # # * `:forwarded_port` - A port that is accessible via localhost # that forwards into the machine. # * `:private_network` - The machine gets an IP that is not directly # publicly accessible, but ideally accessible from this machine. # * `:public_network` - The machine gets an IP on a shared network. # # @param [Symbol] type Type of network # @param [Hash] options Options for the network. def network(type, **options) options = options.dup options[:protocol] ||= "tcp" # Convert to symbol to allow strings type = type.to_sym if !options[:id] default_id = nil if type == :forwarded_port # For forwarded ports, set the default ID to be the # concat of host_ip, proto and host_port. This would ensure Vagrant # caters for port forwarding in an IP aliased environment where # different host IP addresses are to be listened on the same port. default_id = "#{options[:host_ip]}#{options[:protocol]}#{options[:host]}" end options[:id] = default_id || SecureRandom.uuid end # Scope the ID by type so that different types can share IDs id = options[:id] id = "#{type}-#{id}" # Merge in the previous settings if we have them. if @__networks.key?(id) options = @__networks[id][1].merge(options) end # Merge in the latest settings and set the internal state @__networks[id] = [type.to_sym, options] end # Configures a provider for this VM. # # @param [Symbol] name The name of the provider. def provider(name, &block) name = name.to_sym @__providers[name] ||= [] @__provider_overrides[name] ||= [] # Add the provider to the ordering list @__provider_order << name if block_given? @__providers[name] << block if block_given? # If this block takes two arguments, then we curry it and store # the configuration override for use later. if block.arity == 2 @__provider_overrides[name] << block.curry[Vagrant::Config::V2::DummyConfig.new] end end end def provision(name, **options, &block) type = name if options.key?(:type) type = options.delete(:type) else name = nil end if options.key?(:id) puts "Setting `id` on a provisioner is deprecated. Please use the" puts "new syntax of `config.vm.provision \"name\", type: \"type\"" puts "where \"name\" is the replacement for `id`. This will be" puts "fully removed in Vagrant 1.8." name = id end prov = nil if name name = name.to_sym prov = @provisioners.find { |p| p.name == name } end if !prov if options.key?(:before) before = options.delete(:before) end if options.key?(:after) after = options.delete(:after) end opts = {before: before, after: after} prov = VagrantConfigProvisioner.new(name, type.to_sym, **opts) @provisioners << prov end prov.preserve_order = !!options.delete(:preserve_order) if \ options.key?(:preserve_order) prov.run = options.delete(:run) if options.key?(:run) prov.communicator_required = options.delete(:communicator_required) if options.key?(:communicator_required) prov.add_config(**options, &block) nil end def defined_vms @__defined_vms end # This returns the keys of the sub-vms in the order they were # defined. def defined_vm_keys @__defined_vm_keys end def define(name, options=nil, &block) name = name.to_sym options ||= {} options = options.dup options[:config_version] ||= "2" # Add the name to the array of VM keys. This array is used to # preserve the order in which VMs are defined. @__defined_vm_keys << name if !@__defined_vm_keys.include?(name) # Add the SubVM to the hash of defined VMs if !@__defined_vms[name] @__defined_vms[name] = VagrantConfigSubVM.new end @__defined_vms[name].options.merge!(options) @__defined_vms[name].config_procs << [options[:config_version], block] if block end # Stores disk config options from Vagrantfile # # @param [Symbol] type # @param [Hash] options # @param [Block] block def disk(type, **options, &block) disk_config = VagrantConfigDisk.new(type) # Remove provider__option options before set_options, otherwise will # show up as missing setting # Extract provider hash options as well provider_options = {} options.delete_if do |p,o| if o.is_a?(Hash) || p.to_s.include?("__") provider_options[p] = o true end end disk_config.set_options(options) # Add provider config disk_config.add_provider_config(**provider_options, &block) @disks << disk_config end # Stores config options for cloud_init # # @param [Symbol] type # @param [Hash] options # @param [Block] block def cloud_init(type=nil, **options, &block) type = type.to_sym if type cloud_init_config = VagrantConfigCloudInit.new(type) if block_given? block.call(cloud_init_config, VagrantConfigCloudInit) else # config is hash cloud_init_config.set_options(options) end @cloud_init_configs << cloud_init_config end #------------------------------------------------------------------- # Internal methods, don't call these. #------------------------------------------------------------------- def finalize! # Defaults @allowed_synced_folder_types = nil if @allowed_synced_folder_types == UNSET_VALUE @base_mac = nil if @base_mac == UNSET_VALUE @base_address = nil if @base_address == UNSET_VALUE @boot_timeout = 300 if @boot_timeout == UNSET_VALUE @box = nil if @box == UNSET_VALUE @box_architecture = :auto if @box_architecture == UNSET_VALUE # If box architecture value was set, force to string if @box_architecture && @box_architecture != :auto @box_architecture = @box_architecture.to_s end @ignore_box_vagrantfile = false if @ignore_box_vagrantfile == UNSET_VALUE if @box_check_update == UNSET_VALUE @box_check_update = !present?(ENV["VAGRANT_BOX_UPDATE_CHECK_DISABLE"]) end @box_download_ca_cert = nil if @box_download_ca_cert == UNSET_VALUE @box_download_ca_path = nil if @box_download_ca_path == UNSET_VALUE @box_download_checksum = nil if @box_download_checksum == UNSET_VALUE @box_download_checksum_type = nil if @box_download_checksum_type == UNSET_VALUE @box_download_client_cert = nil if @box_download_client_cert == UNSET_VALUE @box_download_disable_ssl_revoke_best_effort = false if @box_download_disable_ssl_revoke_best_effort == UNSET_VALUE @box_download_insecure = false if @box_download_insecure == UNSET_VALUE @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 @box_download_options = {} if @box_download_options == UNSET_VALUE @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 @hostname = nil if @hostname == UNSET_VALUE @hostname = @hostname.to_s if @hostname @post_up_message = "" if @post_up_message == UNSET_VALUE if @usable_port_range == UNSET_VALUE @usable_port_range = (2200..2250) end if @allowed_synced_folder_types @allowed_synced_folder_types = Array(@allowed_synced_folder_types).map(&:to_sym) end # Make sure that the download checksum is a string and that # the type is a symbol @box_download_checksum = "" if !@box_download_checksum if @box_download_checksum_type @box_download_checksum_type = @box_download_checksum_type.to_sym end # Make sure the box URL is an array if it is set @box_url = Array(@box_url) if @box_url # Set the communicator properly @communicator = @communicator.to_sym if @communicator # Set the guest properly @guest = @guest.to_sym if @guest # If we haven't defined a single VM, then we need to define a # default VM which just inherits the rest of the configuration. define(DEFAULT_VM_NAME) if defined_vm_keys.empty? # Make sure the SSH forwarding is added if it doesn't exist if @communicator == :winrm if !@__networks["forwarded_port-winrm"] network :forwarded_port, guest: 5985, host: 55985, host_ip: "127.0.0.1", id: "winrm", auto_correct: true end if !@__networks["forwarded_port-winrm-ssl"] network :forwarded_port, guest: 5986, host: 55986, host_ip: "127.0.0.1", id: "winrm-ssl", auto_correct: true end end # forward SSH ports regardless of communicator if !@__networks["forwarded_port-ssh"] network :forwarded_port, guest: 22, host: 2222, host_ip: "127.0.0.1", id: "ssh", auto_correct: true end # Clean up some network configurations @__networks.values.each do |type, opts| if type == :forwarded_port opts[:guest] = opts[:guest].to_i if opts[:guest] opts[:host] = opts[:host].to_i if opts[:host] end end # Compile all the provider configurations @__providers.each do |name, blocks| # TODO(spox): this is a hack that needs to be resolved elsewhere name = name.to_sym # If we don't have any configuration blocks, then ignore it next if blocks.empty? # Find the configuration class for this provider config_class = Vagrant.plugin("2").manager.provider_configs[name] config_class ||= Vagrant::Config::V2::DummyConfig l = Log4r::Logger.new(self.class.name.downcase) l.info("config class lookup for provider #{name.inspect} gave us base class: #{config_class}") # Load it up config = config_class.new begin blocks.each do |b| new_config = config_class.new b.call(new_config, Vagrant::Config::V2::DummyConfig.new) config = config.merge(new_config) end rescue Exception => e @logger.error("Vagrantfile load error: #{e.message}") @logger.error(e.inspect) @logger.error(e.message) @logger.error(e.backtrace.join("\n")) line = "(unknown)" if e.backtrace && e.backtrace[0] line = e.backtrace.first.slice(0, e.backtrace.first.rindex(':')).rpartition(':').last end raise Vagrant::Errors::VagrantfileLoadError, path: "", line: line, exception_class: e.class, message: e.message end config.finalize! # Store it for retrieval later @__compiled_provider_configs[name] = config end # Finalize all the provisioners @provisioners.each do |p| p.config.finalize! if !p.invalid? p.run = p.run.to_sym if p.run end current_dir_shared = false @__synced_folders.each do |id, options| if options[:hostpath] == '.' current_dir_shared = true end end @disks.each do |d| d.finalize! end @cloud_init_configs.each do |c| c.finalize! end if !current_dir_shared && !@__synced_folders["/vagrant"] synced_folder(".", "/vagrant") end # Flag that we finalized @__finalized = true end # This returns the compiled provider-specific configuration for the # given provider. # # @param [Symbol] name Name of the provider. def get_provider_config(name) raise "Must finalize first." if !@__finalized @logger = Log4r::Logger.new(self.class.name.downcase) @logger.info("looking up provider config for: #{name.inspect}") result = @__compiled_provider_configs[name] @logger.info("provider config value that was stored: #{result.inspect}") # If no compiled configuration was found, then we try to just # use the default configuration from the plugin. if !result @logger.info("no result so doing plugin config lookup using name: #{name.inspect}") config_class = Vagrant.plugin("2").manager.provider_configs[name] @logger.info("config class that we got for the lookup: #{config_class}") if config_class result = config_class.new result.finalize! end end return result end # This returns a list of VM configurations that are overrides # for this provider. # # @param [Symbol] name Name of the provider # @return [Array] def get_provider_overrides(name) (@__provider_overrides[name] || []).map do |p| ["2", p] end end # This returns the list of networks configured. def networks @__networks.values end # This returns the list of synced folders def synced_folders @__synced_folders end def validate(machine, ignore_provider=nil) errors = _detected_errors if @allow_fstab_modification == UNSET_VALUE machine.synced_folders.types.each do |impl_name| inst = machine.synced_folders.type(impl_name) if inst.capability?(:default_fstab_modification) && inst.capability(:default_fstab_modification) == false @allow_fstab_modification = false break end end @allow_fstab_modification = true if @allow_fstab_modification == UNSET_VALUE end if !box && !clone && !machine.provider_options[:box_optional] errors << I18n.t("vagrant.config.vm.box_missing") end if box && clone errors << I18n.t("vagrant.config.vm.clone_and_box") end if box && box.empty? errors << I18n.t("vagrant.config.vm.box_empty", machine_name: machine.name) end errors << I18n.t("vagrant.config.vm.hostname_invalid_characters", name: machine.name) if \ @hostname && @hostname !~ /^[a-z0-9][-.a-z0-9]*$/i if @box_version @box_version.to_s.split(",").each do |v| begin Gem::Requirement.new(v.strip) rescue Gem::Requirement::BadRequirementError errors << I18n.t( "vagrant.config.vm.bad_version", version: v) end end end if box_download_ca_cert path = Pathname.new(box_download_ca_cert). expand_path(machine.env.root_path) if !path.file? errors << I18n.t( "vagrant.config.vm.box_download_ca_cert_not_found", path: box_download_ca_cert) end end if box_download_ca_path path = Pathname.new(box_download_ca_path). expand_path(machine.env.root_path) if !path.directory? errors << I18n.t( "vagrant.config.vm.box_download_ca_path_not_found", path: box_download_ca_path) end end if box_download_checksum_type if box_download_checksum == "" errors << I18n.t("vagrant.config.vm.box_download_checksum_blank") end else if box_download_checksum != "" errors << I18n.t("vagrant.config.vm.box_download_checksum_notblank") end end if !box_download_options.is_a?(Hash) errors << I18n.t("vagrant.config.vm.box_download_options_type", type: box_download_options.class.to_s) end box_download_options.each do |k, v| # If the value is truthy and # if `box_extra_download_options` does not include the key # then the conversion to extra download options produced an error if v && !box_extra_download_options.include?("--#{k}") errors << I18n.t("vagrant.config.vm.box_download_options_not_converted", missing_key: k) end end used_guest_paths = Set.new @__synced_folders.each do |id, options| # If the shared folder is disabled then don't worry about validating it next if options[:disabled] guestpath = Pathname.new(options[:guestpath]) if options[:guestpath] hostpath = Pathname.new(options[:hostpath]).expand_path(machine.env.root_path) if guestpath.to_s == "" && id.to_s == "" errors << I18n.t("vagrant.config.vm.shared_folder_requires_guestpath_or_name") elsif guestpath.to_s != "" if guestpath.relative? && guestpath.to_s !~ /^\w+:/ errors << I18n.t("vagrant.config.vm.shared_folder_guestpath_relative", path: options[:guestpath]) else if used_guest_paths.include?(options[:guestpath]) errors << I18n.t("vagrant.config.vm.shared_folder_guestpath_duplicate", path: options[:guestpath]) end used_guest_paths.add(options[:guestpath]) end end if !hostpath.directory? && !options[:create] errors << I18n.t("vagrant.config.vm.shared_folder_hostpath_missing", path: options[:hostpath]) end if options[:type] == :nfs && !options[:nfs__quiet] if options[:owner] || options[:group] # Owner/group don't work with NFS errors << I18n.t("vagrant.config.vm.shared_folder_nfs_owner_group", path: options[:hostpath]) end end if options[:mount_options] && !options[:mount_options].is_a?(Array) errors << I18n.t("vagrant.config.vm.shared_folder_mount_options_array") end if options[:type] plugins = Vagrant.plugin("2").manager.synced_folders impl_class = plugins[options[:type]] if !impl_class errors << I18n.t("vagrant.config.vm.shared_folder_invalid_option_type", type: options[:type], options: plugins.keys.join(', ')) end end end # Validate networks has_fp_port_error = false fp_used = Set.new valid_network_types = [:forwarded_port, :private_network, :public_network] port_range=(1..65535) has_hostname_config = false networks.each do |type, options| if options[:hostname] if has_hostname_config errors << I18n.t("vagrant.config.vm.multiple_networks_set_hostname") end if options[:ip] == nil errors << I18n.t("vagrant.config.vm.network_with_hostname_must_set_ip") end has_hostname_config = true end if !valid_network_types.include?(type) errors << I18n.t("vagrant.config.vm.network_type_invalid", type: type.to_s) end if type == :forwarded_port if !has_fp_port_error && (!options[:guest] || !options[:host]) errors << I18n.t("vagrant.config.vm.network_fp_requires_ports") has_fp_port_error = true end if options[:host] key = "#{options[:host_ip]}#{options[:protocol]}#{options[:host]}" if fp_used.include?(key) errors << I18n.t("vagrant.config.vm.network_fp_host_not_unique", host: options[:host].to_s, protocol: options[:protocol].to_s) end fp_used.add(key) end if !port_range.include?(options[:host]) || !port_range.include?(options[:guest]) errors << I18n.t("vagrant.config.vm.network_fp_invalid_port") end end if type == :private_network if options[:type] && options[:type].to_sym != :dhcp if !options[:ip] errors << I18n.t("vagrant.config.vm.network_ip_required") end end if options[:ip] && (options[:ip].end_with?(".1") || options[:ip].end_with?(":1")) && (options[:type] || "").to_sym != :dhcp machine.ui.warn(I18n.t( "vagrant.config.vm.network_ip_ends_in_one")) end end end # Validate disks # Check if there is more than one primary disk defined and throw an error primary_disks = @disks.select { |d| d.primary && d.type == :disk } if primary_disks.size > 1 errors << I18n.t("vagrant.config.vm.multiple_primary_disks_error", name: machine.name) end disk_names = @disks.map { |d| d.name } duplicate_names = disk_names.find_all { |d| disk_names.count(d) > 1 } if duplicate_names.any? errors << I18n.t("vagrant.config.vm.multiple_disk_names_error", name: machine.name, disk_names: duplicate_names.uniq.join("\n")) end disk_files = @disks.map { |d| d.file } duplicate_files = disk_files.find_all { |d| d && disk_files.count(d) > 1 } if duplicate_files.any? errors << I18n.t("vagrant.config.vm.multiple_disk_files_error", name: machine.name, disk_files: duplicate_files.uniq.join("\n")) end @disks.each do |d| error = d.validate(machine) errors.concat(error) if !error.empty? end # Validate clout_init_configs @cloud_init_configs.each do |c| error = c.validate(machine) errors.concat error if !error.empty? end # We're done with VM level errors so prepare the section errors = { "vm" => errors } # Validate only the _active_ provider if machine.provider_config if !ignore_provider provider_errors = machine.provider_config.validate(machine) if provider_errors errors = Vagrant::Config::V2::Util.merge_errors(errors, provider_errors) end else machine.ui.warn(I18n.t("vagrant.config.vm.ignore_provider_config")) end end # Validate provisioners @provisioners.each do |vm_provisioner| if vm_provisioner.invalid? name = vm_provisioner.name.to_s name = vm_provisioner.type.to_s if name.empty? errors["vm"] << I18n.t("vagrant.config.vm.provisioner_not_found", name: name) next end provisioner_errors = vm_provisioner.validate(machine, @provisioners) if provisioner_errors errors = Vagrant::Config::V2::Util.merge_errors(errors, provisioner_errors) end if vm_provisioner.config provisioner_errors = vm_provisioner.config.validate(machine) if provisioner_errors errors = Vagrant::Config::V2::Util.merge_errors(errors, provisioner_errors) end end end # If running from the Windows Subsystem for Linux, validate that configured # hostpaths for synced folders are on DrvFs file systems, or the synced # folder implementation explicitly supports non-DrvFs file system types # within the WSL if Vagrant::Util::Platform.wsl? # Create a helper that will with the synced folders mixin # from the builtin action to get the correct implementation # to be used for each folder sf_helper = Class.new do include Vagrant::Action::Builtin::MixinSyncedFolders end.new folders = sf_helper.synced_folders(machine, config: self) folders.each do |impl_name, data| data.each do |_, fs| hostpath = File.expand_path(fs[:hostpath], machine.env.root_path) if !Vagrant::Util::Platform.wsl_drvfs_path?(hostpath) sf_klass = sf_helper.plugins[impl_name.to_sym].first if sf_klass.respond_to?(:wsl_allow_non_drvfs?) && sf_klass.wsl_allow_non_drvfs? next end errors["vm"] << I18n.t("vagrant.config.vm.shared_folder_wsl_not_drvfs", path: fs[:hostpath]) end end end end # Validate sub-VMs if there are any @__defined_vms.each do |name, _| if name =~ /[\[\]\{\}\/]/ errors["vm"] << I18n.t( "vagrant.config.vm.name_invalid", name: name) end end if ![TrueClass, FalseClass].include?(@allow_fstab_modification.class) errors["vm"] << I18n.t("vagrant.config.vm.config_type", option: "allow_fstab_modification", given: @allow_fstab_modification.class, required: "Boolean" ) end if ![TrueClass, FalseClass].include?(@allow_hosts_modification.class) errors["vm"] << I18n.t("vagrant.config.vm.config_type", option: "allow_hosts_modification", given: @allow_hosts_modification.class, required: "Boolean" ) end errors end def __providers @__provider_order end end end end ================================================ FILE: plugins/kernel_v2/config/vm_provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' module VagrantPlugins module Kernel_V2 # Represents a single configured provisioner for a VM. class VagrantConfigProvisioner < Vagrant.plugin("2", :config) # Defaults VALID_BEFORE_AFTER_TYPES = [:each, :all].freeze # Unique name for this provisioner # # Accepts a string, but is ultimately forced into a symbol in the top level method inside # #Config::VM.provision method while being parsed from a Vagrantfile # # @return [Symbol] attr_reader :name # Internal unique name for this provisioner # Set to the given :name if exists, otherwise # it's set as a UUID. # # Note: This is for internal use only. # # @return [String] attr_reader :id # The type of the provisioner that should be registered # as a plugin. # # @return [Symbol] attr_reader :type # The configuration associated with the provisioner, if there is any. # # @return [Object] attr_accessor :config # When to run this provisioner. Either "once", "always", or "never" # # @return [String] attr_accessor :run # Whether or not to preserve the order when merging this with a # parent scope. # # @return [Boolean] attr_accessor :preserve_order # The name of a provisioner to run before it has started # # @return [String, Symbol] attr_accessor :before # The name of a provisioner to run after it is finished # # @return [String, Symbol] attr_accessor :after # Boolean, when true signifies that some communicator must # be available in order for the provisioner to run. # # @return [Boolean] attr_accessor :communicator_required def initialize(name, type, **options) @logger = Log4r::Logger.new("vagrant::config::vm::provisioner") @logger.debug("Provisioner defined: #{name}") @id = name || SecureRandom.uuid @config = nil @invalid = false @name = name @preserve_order = false @run = nil @type = type @before = options[:before] @after = options[:after] @communicator_required = options.fetch(:communicator_required, true) # Attempt to find the provisioner... if !Vagrant.plugin("2").manager.provisioners[type] @logger.warn("Provisioner '#{type}' not found.") @invalid = true end # Attempt to find the configuration class for this provider # if it exists and load the configuration. @config_class = Vagrant.plugin("2").manager. provisioner_configs[@type] if !@config_class @logger.info( "Provisioner config for '#{@type}' not found. Ignoring config.") @config_class = Vagrant::Config::V2::DummyConfig end end def initialize_copy(orig) super @config = @config.dup if @config end def add_config(**options, &block) # Don't skip if config is invalid. It might be a valid non-Ruby plugin current = @config_class.new current.set_options(options) if options block.call(current) if block current = @config.merge(current) if @config @config = current end def finalize! return if invalid? @config.finalize! end # Validates the before/after options # # @param [Vagrant::Machine] machine - machine to validate against # @param [Array] provisioners - Array of defined provisioners for the guest machine # @return [Array] array of strings of error messages from config option validation def validate(machine, provisioners) errors = _detected_errors provisioner_names = provisioners.map { |i| i.name.to_s if i.name != name }.compact if ![TrueClass, FalseClass].include?(@communicator_required.class) errors << I18n.t("vagrant.provisioners.base.wrong_type", opt: "communicator_required", type: "boolean") end if @before && @after errors << I18n.t("vagrant.provisioners.base.both_before_after_set") end if @before if !VALID_BEFORE_AFTER_TYPES.include?(@before) if @before.is_a?(Symbol) && !VALID_BEFORE_AFTER_TYPES.include?(@before) errors << I18n.t("vagrant.provisioners.base.invalid_alias_value", opt: "before", alias: VALID_BEFORE_AFTER_TYPES.join(", ")) elsif !@before.is_a?(String) && !VALID_BEFORE_AFTER_TYPES.include?(@before) errors << I18n.t("vagrant.provisioners.base.wrong_type", opt: "before", type: "string") end if !provisioner_names.include?(@before) errors << I18n.t("vagrant.provisioners.base.missing_provisioner_name", name: @before, machine_name: machine.name, action: "before", provisioner_name: @name) end dep_prov = provisioners.find_all { |i| i.name.to_s == @before && (i.before || i.after) } if !dep_prov.empty? errors << I18n.t("vagrant.provisioners.base.dependency_provisioner_dependency", name: @name, dep_name: dep_prov.first.name.to_s) end end end if @after if !VALID_BEFORE_AFTER_TYPES.include?(@after) if @after.is_a?(Symbol) errors << I18n.t("vagrant.provisioners.base.invalid_alias_value", opt: "after", alias: VALID_BEFORE_AFTER_TYPES.join(", ")) elsif !@after.is_a?(String) errors << I18n.t("vagrant.provisioners.base.wrong_type", opt: "after", type: "string") end if !provisioner_names.include?(@after) errors << I18n.t("vagrant.provisioners.base.missing_provisioner_name", name: @after, machine_name: machine.name, action: "after", provisioner_name: @name) end dep_prov = provisioners.find_all { |i| i.name.to_s == @after && (i.before || i.after) } if !dep_prov.empty? errors << I18n.t("vagrant.provisioners.base.dependency_provisioner_dependency", name: @name, dep_name: dep_prov.first.name.to_s) end end end {"provisioner" => errors} end # Returns whether the provisioner used was invalid or not. A provisioner # is invalid if it can't be found. # # @return [Boolean] def invalid? @invalid end end end end ================================================ FILE: plugins/kernel_v2/config/vm_subvm.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/stacked_proc_runner" module VagrantPlugins module Kernel_V2 # Represents a single sub-VM in a multi-VM environment. class VagrantConfigSubVM include Vagrant::Util::StackedProcRunner # Returns an array of the configuration procs in [version, proc] # format. # # @return [Array] attr_reader :config_procs attr_reader :options def initialize @config_procs = [] @options = {} end def initialize_copy(other) super @config_procs = other.config_procs.clone @options = other.options.clone end end end end ================================================ FILE: plugins/kernel_v2/config/vm_trigger.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "securerandom" require Vagrant.source_root.join("plugins/provisioners/shell/config") module VagrantPlugins module Kernel_V2 # Represents a single configured provisioner for a VM. class VagrantConfigTrigger < Vagrant.plugin("2", :config) # Defaults DEFAULT_ON_ERROR = :halt DEFAULT_EXIT_CODE = 0 VALID_TRIGGER_TYPES = [:command, :action, :hook].freeze #------------------------------------------------------------------- # Config class for a given Trigger #------------------------------------------------------------------- # Internal unique name for this trigger # # Note: This is for internal use only. # # @return [String] attr_reader :id # Name for the given Trigger. Defaults to nil. # # @return [String] attr_accessor :name # Command to fire the trigger on # # @return [Symbol] attr_reader :command # A string to print at the WARN level # # @return [String] attr_accessor :info # A string to print at the WARN level # # @return [String] attr_accessor :warn # Determines what how a Trigger should behave if it runs into an error. # Defaults to :halt, otherwise can only be set to :continue. # # @return [Symbol] attr_accessor :on_error # If set, will not run trigger for the configured Vagrant commands. # # @return [Symbol, Array] attr_accessor :ignore # If set, will only run trigger for guests that match keys for this parameter. # # @return [String, Regex, Array] attr_accessor :only_on # A local inline or file script to execute for the trigger # # @return [Hash] attr_accessor :run # A remote inline or file script to execute for the trigger # # @return [Hash] attr_accessor :run_remote # If set, will not run trigger for the configured Vagrant commands. # # @return [Integer, Array] attr_accessor :exit_codes # If set to true, trigger will halt Vagrant immediately and exit 0 # Can also be configured to have a custom exit code # # @return [Integer] attr_accessor :abort # Internal reader for the internal variable ruby_block # # @return [Proc] attr_reader :ruby_block # Variable used to store ruby proc when defining a ruby trigger # with the "hash" syntax # # @return [Proc] attr_accessor :ruby # The type of trigger, which defines where it will fire. If not defined, # the option will default to `:action` # # @return [Symbol] attr_accessor :type def initialize(command) @logger = Log4r::Logger.new("vagrant::config::vm::trigger::config") @name = UNSET_VALUE @info = UNSET_VALUE @warn = UNSET_VALUE @on_error = UNSET_VALUE @ignore = UNSET_VALUE @only_on = UNSET_VALUE @run = UNSET_VALUE @run_remote = UNSET_VALUE @exit_codes = UNSET_VALUE @abort = UNSET_VALUE @ruby = UNSET_VALUE @type = UNSET_VALUE # Internal options @id = SecureRandom.uuid if command.respond_to?(:to_sym) @command = command.to_sym else @command = command end @ruby_block = UNSET_VALUE @logger.debug("Trigger defined for: #{command}") end # Config option `ruby` for a trigger which reads in a ruby block and sets # it to be evaluated when the configured trigger fires. This method is only # invoked when the regular "block" syntax is used. Otherwise the proc is # set through the attr_accessor if the hash syntax is used. # # @param [Proc] block def ruby(&block) @ruby_block = block end def finalize! # Ensure all config options are set to nil or default value if untouched # by user @name = nil if @name == UNSET_VALUE @info = nil if @info == UNSET_VALUE @warn = nil if @warn == UNSET_VALUE @on_error = DEFAULT_ON_ERROR if @on_error == UNSET_VALUE @ignore = [] if @ignore == UNSET_VALUE @run = nil if @run == UNSET_VALUE @run_remote = nil if @run_remote == UNSET_VALUE @only_on = nil if @only_on == UNSET_VALUE @exit_codes = DEFAULT_EXIT_CODE if @exit_codes == UNSET_VALUE @abort = nil if @abort == UNSET_VALUE @type = :action if @type == UNSET_VALUE @ruby_block = nil if @ruby_block == UNSET_VALUE @ruby = nil if @ruby == UNSET_VALUE @ruby_block = @ruby if @ruby # These values are expected to always be an Array internally, # but can be set as a single String or Symbol # # Guests are stored internally as strings if @only_on @only_on = Array(@only_on) end # Commands must be stored internally as symbols if @ignore @ignore = Array(@ignore) @ignore.map! { |i| i.to_sym } end if @exit_codes @exit_codes = Array(@exit_codes) end # Convert @run and @run_remote to be a "Shell provisioner" config if @run && @run.is_a?(Hash) # Powershell args and privileged for run commands is currently not supported # so by default use empty string or false if unset. This helps the validate # function determine if the setting was purposefully set, to print a warning if !@run.key?(:powershell_args) @run[:powershell_args] = "" end if !@run.key?(:privileged) @run[:privileged] = false end new_run = VagrantPlugins::Shell::Config.new new_run.set_options(@run) new_run.finalize! @run = new_run end if @run_remote && @run_remote.is_a?(Hash) new_run = VagrantPlugins::Shell::Config.new new_run.set_options(@run_remote) new_run.finalize! @run_remote = new_run end if @abort == true @abort = 1 end if @type @type = @type.to_sym end end # @return [Array] array of strings of error messages from config option validation def validate(machine) errors = _detected_errors if @type && !VALID_TRIGGER_TYPES.include?(@type) errors << I18n.t("vagrant.config.triggers.bad_trigger_type", type: @type, trigger: @command, types: VALID_TRIGGER_TYPES.join(', ')) end if @type == :command || !@type commands = Vagrant.plugin("2").manager.commands.keys.map(&:to_s) if !commands.include?(@command) && @command != :all machine.ui.warn(I18n.t("vagrant.config.triggers.bad_command_warning", cmd: @command)) end end if @run errorz = @run.validate(machine) errors.concat errorz["shell provisioner"] if !errorz.empty? if @run.privileged == true machine.ui.warn(I18n.t("vagrant.config.triggers.privileged_ignored", command: @command)) end if @run.powershell_args != "" machine.ui.warn(I18n.t("vagrant.config.triggers.powershell_args_ignored")) end end if @run_remote errorz = @run_remote.validate(machine) errors.concat errorz["shell provisioner"] if !errorz.empty? end if @name && !@name.is_a?(String) errors << I18n.t("vagrant.config.triggers.name_bad_type", cmd: @command) end if @info && !@info.is_a?(String) errors << I18n.t("vagrant.config.triggers.info_bad_type", cmd: @command) end if @warn && !@warn.is_a?(String) errors << I18n.t("vagrant.config.triggers.warn_bad_type", cmd: @command) end if @on_error != :halt if @on_error != :continue errors << I18n.t("vagrant.config.triggers.on_error_bad_type", cmd: @command) end end if @exit_codes if !@exit_codes.all? {|i| i.is_a?(Integer)} errors << I18n.t("vagrant.config.triggers.exit_codes_bad_type", cmd: @command) end end if @abort && !@abort.is_a?(Integer) errors << I18n.t("vagrant.config.triggers.abort_bad_type", cmd: @command) elsif @abort == false machine.ui.warn(I18n.t("vagrant.config.triggers.abort_false_type")) end if @ruby_block && !ruby_block.is_a?(Proc) errors << I18n.t("vagrant.config.triggers.ruby_bad_type", cmd: @command) end errors end # The String representation of this Trigger. # # @return [String] def to_s "trigger config" end end end end ================================================ FILE: plugins/kernel_v2/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Kernel_V2 # This is the "kernel" of Vagrant and contains the configuration classes # that make up the core of Vagrant for V2. class Plugin < Vagrant.plugin("2") name "kernel" description <<-DESC The kernel of Vagrant. This plugin contains required items for even basic functionality of Vagrant version 2. DESC # Core configuration keys provided by the kernel. Note that unlike # "kernel_v1", none of these configuration classes are upgradable. # This is by design, since we can't be sure if they're upgradable # until another version is available. config("ssh") do require File.expand_path("../config/ssh", __FILE__) SSHConfig end config("package") do require File.expand_path("../config/package", __FILE__) PackageConfig end config("push") do require File.expand_path("../config/push", __FILE__) PushConfig end config("vagrant") do require File.expand_path("../config/vagrant", __FILE__) VagrantConfig end config("vm") do require File.expand_path("../config/vm", __FILE__) VMConfig end plugins = Vagrant::Plugin::Manager.instance.installed_plugins if !plugins.keys.include?("vagrant-triggers") config("trigger") do require File.expand_path("../config/trigger", __FILE__) TriggerConfig end else if !ENV["VAGRANT_USE_VAGRANT_TRIGGERS"] $stderr.puts <<-EOF WARNING: Vagrant has detected the `vagrant-triggers` plugin. This plugin conflicts with the internal triggers implementation. Please uninstall the `vagrant-triggers` plugin and run the command again if you wish to use the core trigger feature. To uninstall the plugin, run the command shown below: vagrant plugin uninstall vagrant-triggers Note that the community plugin `vagrant-triggers` and the core trigger feature in Vagrant do not have compatible syntax. To disable this warning, set the environment variable `VAGRANT_USE_VAGRANT_TRIGGERS`. EOF end end end end end ================================================ FILE: plugins/providers/docker/action/build.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/util/ansi_escape_code_remover" module VagrantPlugins module DockerProvider module Action class Build include Vagrant::Util::ANSIEscapeCodeRemover def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::docker::build") end def call(env) machine = env[:machine] build_dir = env[:build_dir] build_dir ||= machine.provider_config.build_dir git_repo = env[:git_repo] git_repo ||= machine.provider_config.git_repo # If we're not building a container, then just skip this step return @app.call(env) if (!build_dir && !git_repo) # Try to read the image ID from the cache file if we've # already built it. image_file = machine.data_dir.join("docker_build_image") image = nil if image_file.file? image = image_file.read.chomp end # Verify the image exists if we have one if image && !machine.provider.driver.image?(image) machine.ui.output(I18n.t("docker_provider.build_image_invalid")) image = nil end # If we have no image or we're rebuilding, we rebuild if !image || env[:build_rebuild] # Build it args = machine.provider_config.build_args.clone if machine.provider_config.dockerfile dockerfile = machine.provider_config.dockerfile dockerfile_path = build_dir ? File.join(build_dir, dockerfile) : dockerfile args.push("--file").push(dockerfile_path) if build_dir machine.ui.output( I18n.t("docker_provider.building_named_dockerfile", file: machine.provider_config.dockerfile)) else machine.ui.output( I18n.t("docker_provider.building_git_repo_named_dockerfile", file: machine.provider_config.dockerfile, repo: git_repo)) end else if build_dir machine.ui.output(I18n.t("docker_provider.building")) else machine.ui.output( I18n.t("docker_provider.building_git_repo", repo: git_repo)) end end image = machine.provider.driver.build( build_dir || git_repo, extra_args: args) do |type, data| data = remove_ansi_escape_codes(data.chomp).chomp env[:ui].detail(data) if data != "" end # Output the final image machine.ui.detail("\nImage: #{image}") # Store the image ID image_file.open("w") do |f| f.binmode f.write("#{image}\n") end else machine.ui.output(I18n.t("docker_provider.already_built")) end # Set the image for creation env[:create_image] = image @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/compare_synced_folders.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/action/builtin/mixin_synced_folders" module VagrantPlugins module DockerProvider module Action class CompareSyncedFolders include Vagrant::Action::Builtin::MixinSyncedFolders def initialize(app, env) @app = app end def call(env) machine = env[:machine] # Get the synced folders that are cached, and those that aren't cached = synced_folders(machine, cached: true) fresh = synced_folders(machine) # Build up a mapping of existing setup synced folders existing = {} cached.each do |_, fs| fs.each do |_, data| existing[data[:guestpath]] = data[:hostpath] end end # Remove the matching folders, and build up non-matching or # new synced folders. invalids = {} fresh.each do |_, fs| fs.each do |_, data| invalid = false old = existing.delete(data[:guestpath]) if !old invalid = true else old = File.expand_path(old) end if !invalid && old invalid = true if old != File.expand_path(data[:hostpath]) end if invalid invalids[File.expand_path(data[:guestpath])] = File.expand_path(data[:hostpath]) end end end # If we have invalid entries, these are changed or new entries. # If we have existing entries, then we removed some entries. if !invalids.empty? || !existing.empty? machine.ui.warn(I18n.t("docker_provider.synced_folders_changed")) end @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/connect_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'ipaddr' require 'log4r' require 'vagrant/util/scoped_hash_override' module VagrantPlugins module DockerProvider module Action class ConnectNetworks include Vagrant::Util::ScopedHashOverride def initialize(app, env) @app = app @logger = Log4r::Logger.new('vagrant::plugins::docker::connectnetworks') end # Generate CLI arguments for creating the docker network. # # @param [Hash] options Options from the network config # @returns[Array Network create arguments def generate_connect_cli_arguments(options) options.map do |key, value| # If value is false, option is not set next if value.to_s == "false" # If value is true, consider feature flag with no value opt = value.to_s == "true" ? [] : [value] opt.unshift("--#{key.to_s.tr("_", "-")}") end.flatten.compact end # Execute the action def call(env) # If we are using a host VM, then don't worry about it machine = env[:machine] if machine.provider.host_vm? @logger.debug("Not setting up networks because docker host_vm is in use") return @app.call(env) end env[:ui].info(I18n.t("docker_provider.network_connect")) connections = env[:docker_connects] || {} machine.config.vm.networks.each_with_index do |args, idx| type, options = args next if type != :private_network && type != :public_network network_options = scoped_hash_override(options, :docker_connect) network_options.delete_if{|k,_| options.key?(k)} network_name = connections[idx] if !network_name raise Errors::NetworkNameMissing, index: idx, container: machine.name end @logger.debug("Connecting network #{network_name} to container guest #{machine.name}") if options[:ip] && options[:type] != "dhcp" if IPAddr.new(options[:ip]).ipv4? network_options[:ip] = options[:ip] else network_options[:ip6] = options[:ip] end end network_options[:alias] = options[:alias] if options[:alias] connect_opts = generate_connect_cli_arguments(network_options) machine.provider.driver.connect_network(network_name, machine.id, connect_opts) end @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/create.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class Create def initialize(app, env) @app = app end def call(env) @env = env @machine = env[:machine] @provider_config = @machine.provider_config @machine_config = @machine.config @driver = @machine.provider.driver params = create_params # If we're running a single command, we modify the params a bit if env[:machine_action] == :run_command # Use the command that is given to us params[:cmd] = env[:run_command] # Don't detach, we want to watch the command run params[:detach] = false # No ports should be shared to the host params[:ports] = [] # Allocate a pty if it was requested params[:pty] = true if env[:run_pty] # Remove container after execution params[:rm] = true if env[:run_rm] # Name should be unique params[:name] = "#{params[:name]}_#{Time.now.to_i}" # We link to our original container # TODO end env[:ui].output(I18n.t("docker_provider.creating")) env[:ui].detail(" Name: #{params[:name]}") env[:ui].detail(" Image: #{params[:image]}") if params[:cmd] && !params[:cmd].empty? env[:ui].detail(" Cmd: #{params[:cmd].join(" ")}") end params[:volumes].each do |volume| env[:ui].detail("Volume: #{volume}") end params[:ports].each do |pair| env[:ui].detail(" Port: #{pair}") end params[:links].each do |name, other| env[:ui].detail(" Link: #{name}:#{other}") end if env[:machine_action] != :run_command # For regular "ups" create it and get the CID cid = @driver.create(params) env[:ui].detail(" \n"+I18n.t( "docker_provider.created", id: cid[0...16])) @machine.id = cid elsif params[:detach] env[:ui].detail(" \n"+I18n.t("docker_provider.running_detached")) else ui_opts = {} # If we're running with a pty, we want the output to look as # authentic as possible. We don't prefix things and we don't # output a newline. if env[:run_pty] ui_opts[:prefix] = false ui_opts[:new_line] = false end # For run commands, we run it and stream back the output env[:ui].detail(" \n"+I18n.t("docker_provider.running")+"\n ") @driver.create(params, stdin: env[:run_pty]) do |type, data| env[:ui].detail(data.chomp, **ui_opts) end end @app.call(env) end def create_params container_name = @provider_config.name if !container_name container_name = generate_container_name end image = @env[:create_image] image ||= @provider_config.image links = [] @provider_config._links.each do |link| parts = link.split(":", 2) links << parts end { cmd: @provider_config.cmd, detach: true, env: @provider_config.env, expose: @provider_config.expose, extra_args: @provider_config.create_args, hostname: @machine_config.vm.hostname, image: image, links: links, name: container_name, ports: forwarded_ports(@provider_config.has_ssh), privileged: @provider_config.privileged, pty: false, volumes: @provider_config.volumes, } end def forwarded_ports(include_ssh=false) mappings = {} random = [] @machine.config.vm.networks.each do |type, options| next if type != :forwarded_port # Don't include SSH if we've explicitly asked not to next if options[:id] == "ssh" && !include_ssh # Skip port if it is disabled next if options[:disabled] # If the guest port is 0, put it in the random group if options[:guest] == 0 random << options[:host] next end mappings["#{options[:host]}/#{options[:protocol]}"] = options end # Build the results result = random.map(&:to_s) result += mappings.values.map do |fp| protocol = "" protocol = "/udp" if fp[:protocol].to_s == "udp" host_ip = "" host_ip = "#{fp[:host_ip]}:" if fp[:host_ip] "#{host_ip}#{fp[:host]}:#{fp[:guest]}#{protocol}" end.compact result end def generate_container_name container_name = "#{@env[:root_path].basename.to_s}_#{@machine.name}" # Remove leading -/_ and any non-alphanumeric, non-hyphen/underscore characters container_name.gsub!(/\A[^a-zA-Z0-9]+|[^-a-z0-9_]/i, "") container_name << "_#{Time.now.to_i}" container_name end end end end end ================================================ FILE: plugins/providers/docker/action/destroy.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class Destroy def initialize(app, env) @app = app end def call(env) env[:ui].info I18n.t("docker_provider.messages.destroying") machine = env[:machine] driver = machine.provider.driver # If we have a build image, store that image_file = machine.data_dir.join("docker_build_image") image = nil if image_file.file? image = image_file.read.chomp end env[:build_image] = image driver.rm(machine.id) machine.id = nil @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/destroy_build_image.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module DockerProvider module Action class DestroyBuildImage def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::docker::destroybuildimage") end def call(env) machine = env[:machine] image = env[:build_image] image_file = nil if !image # Try to read the image ID from the cache file if we've # already built it. image_file = machine.data_dir.join("docker_build_image") image = nil if image_file.file? image = image_file.read.chomp end end if image machine.ui.output(I18n.t("docker_provider.build_image_destroy")) if !machine.provider.driver.rmi(image) machine.ui.detail(I18n.t( "docker_provider.build_image_destroy_in_use")) end end if image_file && image_file.file? begin image_file.delete rescue Errno::ENOENT # Its okay end end @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/destroy_network.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' module VagrantPlugins module DockerProvider module Action class DestroyNetwork @@lock = Mutex.new def initialize(app, env) @app = app @logger = Log4r::Logger.new('vagrant::plugins::docker::network') end def call(env) # If we are using a host VM, then don't worry about it machine = env[:machine] if machine.provider.host_vm? @logger.debug("Not setting up networks because docker host_vm is in use") return @app.call(env) end @@lock.synchronize do machine.env.lock("docker-network-destroy", retry: true) do machine.config.vm.networks.each do |type, options| next if type != :private_network && type != :public_network vagrant_networks = machine.provider.driver.list_network_names.find_all do |n| n.start_with?("vagrant_network") end vagrant_networks.each do |network_name| if machine.provider.driver.existing_named_network?(network_name) && !machine.provider.driver.network_used?(network_name) env[:ui].info(I18n.t("docker_provider.network_destroy", network_name: network_name)) machine.provider.driver.rm_network(network_name) else @logger.debug("Network #{network_name} not found or in use") end end end end end @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/forwarded_ports.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class ForwardedPorts def initialize(app, env) @app = app end # Converts the `ports` docker provider param into proper network configs # of type :forwarded_port def call(env) env[:machine].provider_config.ports.each do |p| host_ip = nil protocol = "tcp" host, guest = p.split(":", 2) if guest.include?(":") host_ip = host host, guest = guest.split(":", 2) end guest, protocol = guest.split("/", 2) if guest.include?("/") env[:machine].config.vm.network "forwarded_port", host: host.to_i, guest: guest.to_i, host_ip: host_ip, protocol: protocol end @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/has_ssh.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action # This middleware is used with Call to test if this machine supports # SSH. class HasSSH def initialize(app, env) @app = app end def call(env) env[:result] = env[:machine].provider_config.has_ssh @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/host_machine.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module DockerProvider module Action # This action is responsible for creating the host machine if # we need to. The host machine is where Docker containers will # live. class HostMachine def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::docker::hostmachine") end def call(env) if !env[:machine].provider.host_vm? @logger.info("No host machine needed.") return @app.call(env) end env[:machine].ui.output(I18n.t( "docker_provider.host_machine_needed")) host_machine = env[:machine].provider.host_vm begin env[:machine].provider.host_vm_lock do setup_host_machine(host_machine, env) end rescue Vagrant::Errors::EnvironmentLockedError sleep 1 retry end @app.call(env) end protected def setup_host_machine(host_machine, env) # Create a UI for this machine that stays at the detail level proxy_ui = host_machine.ui.dup proxy_ui.opts[:bold] = false proxy_ui.opts[:prefix_spaces] = true proxy_ui.opts[:target] = env[:machine].name.to_s # Reload the machine so that if it was created while we didn't # hold the lock, we'll see the updated state. host_machine.reload # See if the machine is ready already. If not, start it. if host_machine.communicate.ready? env[:machine].ui.detail(I18n.t("docker_provider.host_machine_ready")) else env[:machine].ui.detail( I18n.t("docker_provider.host_machine_starting")) env[:machine].ui.detail(" ") host_machine.with_ui(proxy_ui) do host_machine.action(:up) end # Verify communication is ready. If not, we have a problem. if !host_machine.communicate.ready? raise Errors::HostVMCommunicatorNotReady, id: host_machine.id end end end end end end end ================================================ FILE: plugins/providers/docker/action/host_machine_build_dir.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/md5" require "log4r" module VagrantPlugins module DockerProvider module Action class HostMachineBuildDir def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::docker::hostmachinebuilddir") end def call(env) machine = env[:machine] build_dir = machine.provider_config.build_dir # If we're not building a Dockerfile, ignore return @app.call(env) if !build_dir # If we're building a docker file, expand the directory build_dir = File.expand_path(build_dir, env[:machine].env.root_path) env[:build_dir] = build_dir # If we're not on a host VM, we're done return @app.call(env) if !machine.provider.host_vm? # We're on a host VM, so we need to move our build dir to # that machine. We do this by putting the synced folder on # ourself and letting HostMachineSyncFolders handle it. new_build_dir = "/var/lib/docker/docker_build_#{Digest::MD5.hexdigest(build_dir)}" options = { docker__ignore: true, docker__exact: true, }.merge(machine.provider_config.host_vm_build_dir_options || {}) machine.config.vm.synced_folder(build_dir, new_build_dir, options) # Set the build dir to be the correct one env[:build_dir] = new_build_dir @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/host_machine_port_checker.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module DockerProvider module Action # This sets up the middleware env var to check for ports in use. class HostMachinePortChecker def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::docker::hostmachineportchecker") end def call(env) return @app.call(env) if !env[:machine].provider.host_vm? @machine = env[:machine] env[:port_collision_port_check] = method(:port_check) @app.call(env) end protected def port_check(port) host_machine = @machine.provider.host_vm host_machine.guest.capability(:port_open_check, port) end end end end end ================================================ FILE: plugins/providers/docker/action/host_machine_port_warning.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class HostMachinePortWarning def initialize(app, env) @app = app end def call(env) if !env[:machine].provider.host_vm? return @app.call(env) end # If we have forwarded ports, then notify the user that they # won't be immediately available unless a private network # is created. if has_forwarded_ports?(env[:machine]) env[:machine].ui.warn(I18n.t( "docker_provider.host_machine_forwarded_ports")) end @app.call(env) end protected def has_forwarded_ports?(machine) machine.config.vm.networks.each do |type, _| return true if type == :forwarded_port end false end end end end end ================================================ FILE: plugins/providers/docker/action/host_machine_required.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action # This middleware is used with Call to test if we're using a host VM. class HostMachineRequired def initialize(app, env) @app = app end def call(env) env[:result] = env[:machine].provider.host_vm? @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/host_machine_sync_folders.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/md5" require "securerandom" require "log4r" require "vagrant/action/builtin/mixin_synced_folders" module VagrantPlugins module DockerProvider module Action # This action is responsible for creating the host machine if # we need to. The host machine is where Docker containers will # live. class HostMachineSyncFolders include Vagrant::Action::Builtin::MixinSyncedFolders def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::docker::hostmachine") end def call(env) return @app.call(env) if !env[:machine].provider.host_vm? if !env.key?(:host_machine_sync_folders) env[:host_machine_sync_folders] = true end host_machine = env[:machine].provider.host_vm # Lock while we make changes begin env[:machine].provider.host_vm_lock do setup_synced_folders(host_machine, env) end rescue Vagrant::Errors::EnvironmentLockedError sleep 1 retry end @app.call(env) end protected def setup_synced_folders(host_machine, env) # Write the host machine SFID if we have one id_path = env[:machine].data_dir.join("host_machine_sfid") if !id_path.file? host_sfid = SecureRandom.uuid id_path.open("w") do |f| f.binmode f.write("#{host_sfid}\n") end else host_sfid = id_path.read.chomp end # Create a UI for this machine that stays at the detail level proxy_ui = host_machine.ui.dup proxy_ui.opts[:bold] = false proxy_ui.opts[:prefix_spaces] = true proxy_ui.opts[:target] = env[:machine].name.to_s # Read the existing folders that are setup existing_folders = synced_folders(host_machine, cached: true) existing_ids = {} if existing_folders existing_folders.each do |impl, fs| fs.each do |_name, data| if data[:docker_sfid] && data[:docker_host_sfid] == host_sfid existing_ids[data[:docker_sfid]] = data end end end end # Sync some folders so that our volumes work later. new_config = VagrantPlugins::Kernel_V2::VMConfig.new our_folders = synced_folders(env[:machine]) our_folders.each do |type, folders| folders.each do |id, data| data = data.dup if type == :docker # We don't use the Docker type explicitly on the host VM data.delete(:type) end # Expand the hostpath relative to _our_ root path. Otherwise, # it expands it relative to the proxy VM, which is not what # we want. data[:hostpath] = File.expand_path( data[:hostpath], env[:machine].env.root_path) # Generate an ID that is deterministic based on our machine # and Vagrantfile path... id = Digest::MD5.hexdigest( "#{env[:machine].env.root_path}" + "#{data[:hostpath]}" + "#{data[:guestpath]}" + "#{env[:machine].name}") # Generate a new guestpath data[:docker_guestpath] = data[:guestpath] data[:docker_sfid] = id data[:docker_host_sfid] = host_sfid data[:id] = id[0...6] + rand(10000).to_s # If we specify exact then we know what we're doing if !data[:docker__exact] data[:guestpath] = "/var/lib/docker/docker_#{id}" end # Add this synced folder onto the new config if we haven't # already shared it before. if !existing_ids.key?(id) # A bit of a hack for VirtualBox to mount our # folder as transient. This can be removed once # the VirtualBox synced folder mechanism is smarter. data[:virtualbox__transient] = true new_config.synced_folder( data[:hostpath], data[:guestpath], data) else # We already have the folder, so just load its data data = existing_ids[id] end # Remove from our machine env[:machine].config.vm.synced_folders.delete(id) # Add the "fixed" folder to our machine data = data.merge({ hostpath_exact: true, type: :docker, }) env[:machine].config.vm.synced_folder( data[:guestpath], data[:docker_guestpath], data) end end if !env[:host_machine_sync_folders] @logger.info("Not syncing folders because container created.") end if !new_config.synced_folders.empty? # Sync the folders! env[:machine].ui.output(I18n.t( "docker_provider.host_machine_syncing_folders")) host_machine.with_ui(proxy_ui) do action_env = { synced_folders_config: new_config } begin host_machine.action(:sync_folders, action_env) rescue Vagrant::Errors::MachineActionLockedError sleep 1 retry rescue Vagrant::Errors::UnimplementedProviderAction callable = Vagrant::Action::Builder.new callable.use Vagrant::Action::Builtin::SyncedFolders host_machine.action_raw(:sync_folders, callable, action_env) end end end end end end end end ================================================ FILE: plugins/providers/docker/action/host_machine_sync_folders_disable.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/action/builtin/mixin_synced_folders" module VagrantPlugins module DockerProvider module Action # This action disables the synced folders we created. class HostMachineSyncFoldersDisable include Vagrant::Action::Builtin::MixinSyncedFolders def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::docker::hostmachine") end def call(env) return @app.call(env) if !env[:machine].provider.host_vm? # Read our random ID for this instance id_path = env[:machine].data_dir.join("host_machine_sfid") return @app.call(env) if !id_path.file? host_sfid = id_path.read.chomp host_machine = env[:machine].provider.host_vm @app.call(env) begin env[:machine].provider.host_vm_lock do setup_synced_folders(host_machine, host_sfid, env) end rescue Vagrant::Errors::EnvironmentLockedError sleep 1 retry end end protected def setup_synced_folders(host_machine, host_sfid, env) to_disable = [] # Read the existing folders that are setup existing_folders = synced_folders(host_machine, cached: true) if existing_folders existing_folders.each do |impl, fs| fs.each do |id, data| if data[:docker_host_sfid] == host_sfid to_disable << id end end end end # Nothing to do if we have no bad folders return if to_disable.empty? # Create a UI for this machine that stays at the detail level proxy_ui = host_machine.ui.dup proxy_ui.opts[:bold] = false proxy_ui.opts[:prefix_spaces] = true proxy_ui.opts[:target] = env[:machine].name.to_s env[:machine].ui.output(I18n.t( "docker_provider.host_machine_disabling_folders")) host_machine.with_ui(proxy_ui) do action_env = { synced_folders_cached: true, synced_folders_disable: to_disable, } begin host_machine.action(:sync_folders, action_env) rescue Vagrant::Errors::MachineActionLockedError sleep 1 retry rescue Vagrant::Errors::UnimplementedProviderAction callable = Vagrant::Action::Builder.new callable.use Vagrant::Action::Builtin::SyncedFolders host_machine.action_raw(:sync_folders, callable, action_env) end end end end end end end ================================================ FILE: plugins/providers/docker/action/init_state.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class InitState def initialize(app, env) @app = app end def call(env) # We set the ID of the machine to "preparing" so that we can use # the data dir without it being deleted with the not_created state. env[:machine].id = nil env[:machine].id = "preparing" @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/is_build.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class IsBuild def initialize(app, env) @app = app end def call(env) env[:result] = (!!env[:machine].provider_config.build_dir || !!env[:machine].provider_config.git_repo) @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/is_host_machine_created.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class IsHostMachineCreated def initialize(app, env) @app = app end def call(env) if !env[:machine].provider.host_vm? env[:result] = true return @app.call(env) end host_machine = env[:machine].provider.host_vm env[:result] = host_machine.state.id != Vagrant::MachineState::NOT_CREATED_ID # If the host machine isn't created, neither are we. It is # important we set this to nil here so that global-status # sees the right thing. env[:machine].id = nil if !env[:result] @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/login.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module DockerProvider module Action class Login def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::docker::login") end def login(env, config, driver) # Login! env[:ui].output(I18n.t("docker_provider.logging_in")) driver.login( config.email, config.username, config.password, config.auth_server) # Continue, so that the auth is protected # from meddling. @app.call(env) # Log out driver.logout(config.auth_server) end def call(env) config = env[:machine].provider_config driver = env[:machine].provider.driver # If we don't have a password set, don't auth return @app.call(env) if config.password == "" if !env[:machine].provider.host_vm? # no host vm in use, using docker directly login(env, config, driver) else # Grab a host VM lock to do the login so that we only login # once per container for the rest of this process. env[:machine].provider.host_vm_lock do login(env, config, driver) end end end end end end end ================================================ FILE: plugins/providers/docker/action/prepare_forwarded_port_collision_params.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class PrepareForwardedPortCollisionParams def initialize(app, env) @app = app end def call(env) machine = env[:machine] # Get the forwarded ports used by other containers and # consider those in use as well. other_used_ports = machine.provider.driver.read_used_ports env[:port_collision_extra_in_use] = other_used_ports # Build the remap for any existing collision detections # # Note: This remap might not be required yet (as it is with the virtualbox provider) # so for now we leave the remap hash empty. remap = {} env[:port_collision_remap] = remap # This port checker method calls the custom port_check method # defined below. If its false, it will go ahead and use the built-in # port_check method to see if there are any live containers with bound # ports docker_port_check = proc { |host_ip, host_port| result = port_check(env, host_port) if !result result = Vagrant::Action::Builtin::HandleForwardedPortCollisions.port_check(machine, host_ip, host_port) end result} env[:port_collision_port_check] = docker_port_check @app.call(env) end protected # This check is required the docker provider. Containers # can bind ports but be halted. We don't want new containers to # grab these bound ports, so this check is here for that since # the checks above won't detect it # # @param [Vagrant::Environment] env # @param [String] host_port # @returns [Bool] def port_check(env, host_port) extra_in_use = env[:port_collision_extra_in_use] if extra_in_use return extra_in_use.include?(host_port.to_s) else return false end end end end end end ================================================ FILE: plugins/providers/docker/action/prepare_networks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'ipaddr' require 'log4r' require 'vagrant/util/scoped_hash_override' module VagrantPlugins module DockerProvider module Action class PrepareNetworks include Vagrant::Util::ScopedHashOverride @@lock = Mutex.new def initialize(app, env) @app = app @logger = Log4r::Logger.new('vagrant::plugins::docker::preparenetworks') end # Generate CLI arguments for creating the docker network. # # @param [Hash] options Options from the network config # @returns[Array] Network create arguments def generate_create_cli_arguments(options) options.map do |key, value| # If value is false, option is not set next if value.to_s == "false" # If value is true, consider feature flag with no value opt = value.to_s == "true" ? [] : [value] opt.unshift("--#{key.to_s.tr("_", "-")}") end.flatten.compact end # @return [Array] interface list def list_interfaces Socket.getifaddrs.find_all do |i| !i.addr.nil? && i.addr.ip? && !i.addr.ipv4_loopback? && !i.addr.ipv6_loopback? && !i.addr.ipv6_linklocal? end end # Validates that a network name exists. If it does not # exist, an exception is raised. # # @param [String] network_name Name of existing network # @param [Hash] env Local call env # @return [Boolean] def validate_network_name!(network_name, env) if !env[:machine].provider.driver.existing_named_network?(network_name) raise Errors::NetworkNameUndefined, network_name: network_name end true end # Validates that the provided options are compatible with a # pre-existing network. Raises exceptions on invalid configurations # # @param [String] network_name Name of the network # @param [Hash] root_options Root networking options # @param [Hash] network_options Docker scoped networking options # @param [Driver] driver Docker driver # @return [Boolean] def validate_network_configuration!(network_name, root_options, network_options, driver) if root_options[:ip] && driver.network_containing_address(root_options[:ip]) != network_name raise Errors::NetworkAddressInvalid, address: root_options[:ip], network_name: network_name end if network_options[:subnet] && driver.network_containing_address(network_options[:subnet]) != network_name raise Errors::NetworkSubnetInvalid, subnet: network_options[:subnet], network_name: network_name end true end # Generate configuration for private network # # @param [Hash] root_options Root networking options # @param [Hash] net_options Docker scoped networking options # @param [Hash] env Local call env # @return [String, Hash] Network name and updated network_options def process_private_network(root_options, network_options, env) if root_options[:name] && validate_network_name!(root_options[:name], env) network_name = root_options[:name] end if root_options[:type].to_s == "dhcp" if !root_options[:ip] && !root_options[:subnet] network_name = "vagrant_network" if !network_name return [network_name, network_options] end if root_options[:subnet] addr = IPAddr.new(root_options[:subnet]) root_options[:netmask] = addr.prefix end end if root_options[:ip] addr = IPAddr.new(root_options[:ip]) elsif addr.nil? raise Errors::NetworkIPAddressRequired end # If address is ipv6, enable ipv6 support network_options[:ipv6] = addr.ipv6? # If no mask is provided, attempt to locate any existing # network which contains the assigned IP address if !root_options[:netmask] && !network_name network_name = env[:machine].provider.driver. network_containing_address(root_options[:ip]) # When no existing network is found, we are creating # a new network. Since no mask was provided, default # to /24 for ipv4 and /64 for ipv6 if !network_name root_options[:netmask] = addr.ipv4? ? 24 : 64 end end # With no network name, process options to find or determine # name for new network if !network_name if !root_options[:subnet] # Only generate a subnet if not given one subnet = IPAddr.new("#{addr}/#{root_options[:netmask]}") network = "#{subnet}/#{root_options[:netmask]}" else network = root_options[:subnet] end network_options[:subnet] = network existing_network = env[:machine].provider.driver. network_defined?(network) if !existing_network network_name = "vagrant_network_#{network}" else if !existing_network.to_s.start_with?("vagrant_network") env[:ui].warn(I18n.t("docker_provider.subnet_exists", network_name: existing_network, subnet: network)) end network_name = existing_network end end [network_name, network_options] end # Generate configuration for public network # # TODO: When the Vagrant installer upgrades to Ruby 2.5.x, # remove all instances of the roundabout way of determining a prefix # and instead just use the built-in `.prefix` method # # @param [Hash] root_options Root networking options # @param [Hash] net_options Docker scoped networking options # @param [Hash] env Local call env # @return [String, Hash] Network name and updated network_options def process_public_network(root_options, net_options, env) if root_options[:name] && validate_network_name!(root_options[:name], env) network_name = root_options[:name] end if !network_name valid_interfaces = list_interfaces if valid_interfaces.empty? raise Errors::NetworkNoInterfaces elsif valid_interfaces.size == 1 bridge_interface = valid_interfaces.first elsif idx = valid_interfaces.detect{|i| Array(root_options[:bridge]).include?(i.name) } bridge_interface = idx end if !bridge_interface env[:ui].info(I18n.t("vagrant.actions.vm.bridged_networking.available"), prefix: false) valid_interfaces.each_with_index do |int, i| env[:ui].info("#{i + 1}) #{int.name}", prefix: false) end env[:ui].info(I18n.t( "vagrant.actions.vm.bridged_networking.choice_help") + "\n", prefix: false ) end while !bridge_interface choice = env[:ui].ask( I18n.t("vagrant.actions.vm.bridged_networking.select_interface") + " ", prefix: false) bridge_interface = valid_interfaces[choice.to_i - 1] end base_opts = Vagrant::Util::HashWithIndifferentAccess.new base_opts[:opt] = "parent=#{bridge_interface.name}" subnet = IPAddr.new(bridge_interface.addr.ip_address << "/" << bridge_interface.netmask.ip_unpack.first) netmask = bridge_interface.netmask.ip_unpack.first prefix = IPAddr.new("255.255.255.255/#{netmask}").to_i.to_s(2).count("1") base_opts[:subnet] = "#{subnet}/#{prefix}" subnet_addr = IPAddr.new(base_opts[:subnet]) base_opts[:driver] = "macvlan" base_opts[:gateway] = subnet_addr.succ.to_s base_opts[:ipv6] = subnet_addr.ipv6? network_options = base_opts.merge(net_options) # Check if network already exists for this subnet network_name = env[:machine].provider.driver. network_containing_address(network_options[:gateway]) if !network_name network_name = "vagrant_network_public_#{bridge_interface.name}" end # If the network doesn't already exist, gather available address range # within subnet which docker can provide addressing if !env[:machine].provider.driver.existing_named_network?(network_name) if !net_options[:gateway] network_options[:gateway] = request_public_gateway( network_options, bridge_interface.name, env) end network_options[:ip_range] = request_public_iprange( network_options, bridge_interface, env) end end [network_name, network_options] end # Request the gateway address for the public network # # @param [Hash] network_options Docker scoped networking options # @param [String] interface The bridge interface used # @param [Hash] env Local call env # @return [String] Gateway address def request_public_gateway(network_options, interface, env) subnet = IPAddr.new(network_options[:subnet]) gateway = nil while !gateway gateway = env[:ui].ask(I18n.t( "docker_provider.network_bridge_gateway_request", interface: interface, default_gateway: network_options[:gateway]) + " ", prefix: false ).strip if gateway.empty? gateway = network_options[:gateway] end begin gateway = IPAddr.new(gateway) if !subnet.include?(gateway) env[:ui].warn(I18n.t("docker_provider.network_bridge_gateway_outofbounds", gateway: gateway, subnet: network_options[:subnet]) + "\n", prefix: false) end rescue IPAddr::InvalidAddressError env[:ui].warn(I18n.t("docker_provider.network_bridge_gateway_invalid", gateway: gateway) + "\n", prefix: false) gateway = nil end end gateway.to_s end # Request the IP range allowed for use by docker when creating a new # public network # # TODO: When the Vagrant installer upgrades to Ruby 2.5.x, # remove all instances of the roundabout way of determining a prefix # and instead just use the built-in `.prefix` method # # @param [Hash] network_options Docker scoped networking options # @param [Socket::Ifaddr] interface The bridge interface used # @param [Hash] env Local call env # @return [String] Address range def request_public_iprange(network_options, interface, env) return network_options[:ip_range] if network_options[:ip_range] subnet = IPAddr.new(network_options[:subnet]) env[:ui].info(I18n.t( "docker_provider.network_bridge_iprange_info") + "\n", prefix: false ) range = nil while !range range = env[:ui].ask(I18n.t( "docker_provider.network_bridge_iprange_request", interface: interface.name, default_range: network_options[:subnet]) + " ", prefix: false ).strip if range.empty? range = network_options[:subnet] end begin range = IPAddr.new(range) if !subnet.include?(range) netmask = interface.netmask.ip_unpack.first prefix = IPAddr.new("255.255.255.255/#{netmask}").to_i.to_s(2).count("1") env[:ui].warn(I18n.t( "docker_provider.network_bridge_iprange_outofbounds", subnet: network_options[:subnet], range: "#{range}/#{prefix}" ) + "\n", prefix: false) range = nil end rescue IPAddr::InvalidAddressError env[:ui].warn(I18n.t( "docker_provider.network_bridge_iprange_invalid", range: range) + "\n", prefix: false) range = nil end end netmask = interface.netmask.ip_unpack.first prefix = IPAddr.new("255.255.255.255/#{netmask}").to_i.to_s(2).count("1") "#{range}/#{prefix}" end # Execute the action def call(env) # If we are using a host VM, then don't worry about it machine = env[:machine] if machine.provider.host_vm? @logger.debug("Not setting up networks because docker host_vm is in use") return @app.call(env) end connections = {} @@lock.synchronize do machine.env.lock("docker-network-create", retry: true) do env[:ui].info(I18n.t("docker_provider.network_create")) machine.config.vm.networks.each_with_index do |net_info, net_idx| type, options = net_info network_options = scoped_hash_override(options, :docker_network) network_options.delete_if{|k,_| options.key?(k)} case type when :public_network network_name, network_options = process_public_network( options, network_options, env) when :private_network network_name, network_options = process_private_network( options, network_options, env) else next # unsupported type so ignore end if !network_name raise Errors::NetworkInvalidOption, container: machine.name end if !machine.provider.driver.existing_named_network?(network_name) @logger.debug("Creating network #{network_name}") cli_opts = generate_create_cli_arguments(network_options) machine.provider.driver.create_network(network_name, cli_opts) else @logger.debug("Network #{network_name} already created") validate_network_configuration!(network_name, options, network_options, machine.provider.driver) end connections[net_idx] = network_name end end end env[:docker_connects] = connections @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/prepare_nfs_settings.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class PrepareNFSSettings include Vagrant::Util::Retryable def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::vm::nfs") end def call(env) @machine = env[:machine] @app.call(env) if using_nfs? && !privileged_container? raise Errors::NfsWithoutPrivilegedError end if using_nfs? @logger.info("Using NFS, preparing NFS settings by reading host IP and machine IP") add_ips_to_env!(env) end end # We're using NFS if we have any synced folder with NFS configured. If # we are not using NFS we don't need to do the extra work to # populate these fields in the environment. def using_nfs? @machine.config.vm.synced_folders.any? { |_, opts| opts[:type] == :nfs } end def privileged_container? @machine.provider.driver.privileged?(@machine.id) end # Extracts the proper host and guest IPs for NFS mounts and stores them # in the environment for the SyncedFolder action to use them in # mounting. # # The ! indicates that this method modifies its argument. def add_ips_to_env!(env) provider = env[:machine].provider host_ip = provider.driver.docker_bridge_ip machine_ip = provider.ssh_info[:host] raise Vagrant::Errors::NFSNoHostonlyNetwork if !host_ip || !machine_ip env[:nfs_host_ip] = host_ip env[:nfs_machine_ip] = machine_ip end end end end end ================================================ FILE: plugins/providers/docker/action/prepare_nfs_valid_ids.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class PrepareNFSValidIds def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::vm::nfs") end def call(env) machine = env[:machine] env[:nfs_valid_ids] = machine.provider.driver.all_containers @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/prepare_ssh.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class PrepareSSH def initialize(app, env) @app = app end def call(env) # If we aren't using a host VM, then don't worry about it return @app.call(env) if !env[:machine].provider.host_vm? env[:machine].ui.output(I18n.t( "docker_provider.ssh_through_host_vm")) # Modify the SSH info to be the host VM's info env[:ssh_info] = env[:machine].provider.host_vm.ssh_info # Modify the SSH options for when we `vagrant ssh`... ssh_opts = env[:ssh_opts] || {} # Build the command we'll execute within the Docker host machine: ssh_command = env[:machine].communicate.container_ssh_command if !Array(ssh_opts[:extra_args]).empty? ssh_command << " #{Array(ssh_opts[:extra_args]).join(" ")}" end # Modify the SSH options for the original command: # Append "-t" to force a TTY allocation ssh_opts[:extra_args] = ["-t"] # Enable Agent forwarding when requested for the target VM if env[:machine].ssh_info[:forward_agent] ssh_opts[:extra_args] << "-o ForwardAgent=yes" end ssh_opts[:extra_args] << ssh_command # Set the opts env[:ssh_opts] = ssh_opts @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/pull.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class Pull def initialize(app, env) @app = app end def call(env) @env = env @machine = env[:machine] @provider_config = @machine.provider_config @driver = @machine.provider.driver # Skip pulling if the image is built return @app.call(env) if @env[:create_image] || !@provider_config.pull image = @provider_config.image env[:ui].output(I18n.t("docker_provider.pull", image: image)) @driver.pull(image) @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/start.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class Start def initialize(app, env) @app = app end def call(env) machine = env[:machine] driver = machine.provider.driver machine.ui.output(I18n.t("docker_provider.messages.starting")) driver.start(machine.id) @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/stop.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action class Stop def initialize(app, env) @app = app end def call(env) machine = env[:machine] driver = machine.provider.driver if driver.running?(machine.id) env[:ui].info I18n.t("docker_provider.messages.stopping") driver.stop(machine.id, machine.provider_config.stop_timeout) end @app.call(env) end end end end end ================================================ FILE: plugins/providers/docker/action/wait_for_running.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "thread" require "log4r" module VagrantPlugins module DockerProvider module Action class WaitForRunning def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::docker::waitforrunning") end def call(env) machine = env[:machine] wait = true if !machine.provider_config.remains_running @logger.debug("remains_running is false") wait = false elsif machine.state.id == :running @logger.debug("container is already running") wait = false end # If we're not waiting, just return return @app.call(env) if !wait machine.ui.output(I18n.t("docker_provider.waiting_for_running")) # First, make sure it leaves the stopped state if its supposed to. after = sleeper(5) while machine.state.id == :stopped if after[:done] raise Errors::StateStopped end sleep 0.2 end # Then, wait for it to become running after = sleeper(30) while true state = machine.state break if state.id == :running @logger.info("Waiting for container to run. State: #{state.id}") if after[:done] raise Errors::StateNotRunning end sleep 0.2 end @app.call(env) end protected def sleeper(duration) Thread.new(duration) do |d| sleep(d) Thread.current[:done] = true end end end end end end ================================================ FILE: plugins/providers/docker/action.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Action # Include the built-in modules so we can use them as top-level things. include Vagrant::Action::Builtin # This action starts another container just like the real one running # but only for the purpose of running a single command rather than # to exist long-running. def self.action_run_command Vagrant::Action::Builder.new.tap do |b| # We just call the "up" action. We create a separate action # to hold this though in case we modify it in the future, and # so that we can switch on the "machine_action" env var. b.use action_up end end # This action brings the "machine" up from nothing, including creating the # container, configuring metadata, and booting. def self.action_up Vagrant::Action::Builder.new.tap do |b| b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use HandleBox end end b.use ConfigValidate b.use HostMachine # Yeah, this is supposed to be here twice (once more above). This # catches the case when the container was supposed to be created, # but the host state was unknown, and now we know its not actually # created. b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use HandleBox b2.use DestroyBuildImage end end b.use action_start end end def self.action_package lambda do |env| raise Errors::PackageNotSupported end end # This action just runs the provisioners on the machine. def self.action_provision Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("docker_provider.messages.not_created") next end b2.use Call, IsState, :running do |env2, b3| if !env2[:result] b3.use Message, I18n.t("docker_provider.messages.not_running") next end b3.use Call, HasProvisioner do |env3, b4| b4.use Provision end end end end end # This is the action that is primarily responsible for halting # the virtual machine, gracefully or by force. def self.action_halt Vagrant::Action::Builder.new.tap do |b| b.use Call, IsState, :host_state_unknown do |env, b2| if env[:result] b2.use HostMachine end end b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("docker_provider.messages.not_created") next end b2.use Stop end end end # This action is responsible for reloading the machine, which # brings it down, sucks in new configuration, and brings the # machine back up with the new configuration. def self.action_reload Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("docker_provider.messages.not_created") next end b2.use action_halt b2.use Call, IsBuild do |env2, b3| if env2[:result] b3.use EnvSet, force_halt: true b3.use action_halt b3.use HostMachineSyncFoldersDisable b3.use Destroy b3.use ProvisionerCleanup end end b2.use action_start end end end # This is the action that is primarily responsible for completely # freeing the resources of the underlying virtual machine. def self.action_destroy Vagrant::Action::Builder.new.tap do |b| b.use Call, IsHostMachineCreated do |env, b2| if !env[:result] b2.use Message, I18n.t("docker_provider.messages.not_created") next end b2.use Call, IsState, :host_state_unknown do |env2, b3| if env2[:result] b3.use HostMachine end end b2.use Call, IsState, :not_created do |env2, b3| if env2[:result] b3.use Message, I18n.t("docker_provider.messages.not_created") next end b3.use Call, DestroyConfirm do |env3, b4| if env3[:result] b4.use ConfigValidate b4.use ProvisionerCleanup, :before b4.use EnvSet, force_halt: true b4.use action_halt b4.use HostMachineSyncFoldersDisable b4.use Destroy b4.use DestroyNetwork b4.use DestroyBuildImage else b4.use Message, I18n.t("docker_provider.messages.will_not_destroy") end end 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| b.use Call, IsState, :not_created do |env, b2| if env[:result] raise Errors::ContainerNotCreatedError end b2.use Call, IsState, :running do |env2, b3| if !env2[:result] raise Errors::ContainerNotRunningError end b3.use PrepareSSH b3.use SSHExec end end end end # This is the action that will run a single SSH command. def self.action_ssh_run Vagrant::Action::Builder.new.tap do |b| b.use Call, IsState, :not_created do |env, b2| if env[:result] raise Errors::ContainerNotCreatedError end b2.use Call, IsState, :running do |env2, b3| if !env2[:result] raise Errors::ContainerNotRunningError end b3.use SSHRun end end end end def self.action_start Vagrant::Action::Builder.new.tap do |b| b.use Call, IsState, :running do |env, b2| if env[:machine_action] != :run_command b2.use Call, HasProvisioner do |env2, b3| b3.use Provision end end # If the container is running and we're doing a run, we're done next if env[:result] && env[:machine_action] != :run_command b2.use Call, IsState, :not_created do |env2, b3| if env2[:result] # First time making this thing, set to the "preparing" state b3.use InitState else b3.use EnvSet, host_machine_sync_folders: false end end b2.use HostMachineBuildDir b2.use HostMachineSyncFolders b2.use PrepareNFSValidIds b2.use SyncedFolderCleanup b2.use SyncedFolders b2.use PrepareNFSSettings b2.use PrepareNetworks b2.use Login b2.use Build if env[:machine_action] != :run_command # If the container is NOT created yet, then do some setup steps # necessary for creating it. b2.use Call, IsState, :preparing do |env2, b3| if env2[:result] b3.use EnvSet, port_collision_repair: true b3.use HostMachinePortWarning b3.use HostMachinePortChecker b3.use ForwardedPorts # This action converts the `ports` param into proper network configs b3.use PrepareForwardedPortCollisionParams b3.use HandleForwardedPortCollisions b3.use Pull b3.use Create b3.use WaitForRunning else b3.use CompareSyncedFolders end end b2.use ConnectNetworks b2.use Start b2.use WaitForRunning b2.use Call, HasSSH do |env2, b3| if env2[:result] b3.use WaitForCommunicator end end else # We're in a run command, so we do things a bit differently. b2.use Create end end end end def self.action_suspend lambda do |env| raise Errors::SuspendNotSupported end end # The autoload farm action_root = Pathname.new(File.expand_path("../action", __FILE__)) autoload :Build, action_root.join("build") autoload :CompareSyncedFolders, action_root.join("compare_synced_folders") autoload :ConnectNetworks, action_root.join("connect_networks") autoload :Create, action_root.join("create") autoload :Destroy, action_root.join("destroy") autoload :DestroyBuildImage, action_root.join("destroy_build_image") autoload :DestroyNetwork, action_root.join("destroy_network") autoload :ForwardedPorts, action_root.join("forwarded_ports") autoload :HasSSH, action_root.join("has_ssh") autoload :HostMachine, action_root.join("host_machine") autoload :HostMachineBuildDir, action_root.join("host_machine_build_dir") autoload :HostMachinePortChecker, action_root.join("host_machine_port_checker") autoload :HostMachinePortWarning, action_root.join("host_machine_port_warning") autoload :HostMachineRequired, action_root.join("host_machine_required") autoload :HostMachineSyncFolders, action_root.join("host_machine_sync_folders") autoload :HostMachineSyncFoldersDisable, action_root.join("host_machine_sync_folders_disable") autoload :InitState, action_root.join("init_state") autoload :IsBuild, action_root.join("is_build") autoload :IsHostMachineCreated, action_root.join("is_host_machine_created") autoload :Login, action_root.join("login") autoload :PrepareForwardedPortCollisionParams, action_root.join("prepare_forwarded_port_collision_params") autoload :PrepareNetworks, action_root.join("prepare_networks") autoload :PrepareNFSValidIds, action_root.join("prepare_nfs_valid_ids") autoload :PrepareNFSSettings, action_root.join("prepare_nfs_settings") autoload :PrepareSSH, action_root.join("prepare_ssh") autoload :Pull, action_root.join("pull") autoload :Start, action_root.join("start") autoload :Stop, action_root.join("stop") autoload :WaitForRunning, action_root.join("wait_for_running") end end end ================================================ FILE: plugins/providers/docker/cap/has_communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Cap module HasCommunicator def self.has_communicator(machine) return machine.provider_config.has_ssh end end end end end ================================================ FILE: plugins/providers/docker/cap/proxy_machine.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Cap module ProxyMachine def self.proxy_machine(machine) return nil if !machine.provider.host_vm? machine.provider.host_vm end end end end end ================================================ FILE: plugins/providers/docker/cap/public_address.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Cap module PublicAddress def self.public_address(machine) return nil if machine.state.id != :running # If we're using a host VM, then return the IP of that # rather than of our own machine. if machine.provider.host_vm? host_machine = machine.provider.host_vm return nil if !host_machine.provider.capability?(:public_address) return host_machine.provider.capability(:public_address) end ssh_info = machine.ssh_info return nil if !ssh_info ssh_info[:host] end end end end end ================================================ FILE: plugins/providers/docker/command/exec.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/safe_exec' module VagrantPlugins module DockerProvider module Command class Exec < Vagrant.plugin("2", :command) def self.synopsis "attach to an already-running docker container" end def execute options = {} options[:detach] = false options[:pty] = false options[:interactive] = false options[:prefix] = true opts = OptionParser.new do |o| o.banner = "Usage: vagrant docker-exec [options] [name] -- [args]" o.separator "" o.separator "Options:" o.separator "" o.on("--[no-]detach", "Run in the background") do |d| options[:detach] = d end o.on("-i", "--[no-]interactive", "Keep STDIN open even if not attached") do |i| options[:interactive] = i end o.on("-t", "--[no-]tty", "Allocate a pty") do |t| options[:pty] = t end o.on("-u", "--user USER", "User or UID") do |u| options[:user] = u end o.on("--[no-]prefix", "Prefix output with machine names") do |p| options[:prefix] = p end end # Parse out the extra args to send to SSH, which is everything # after the "--" command = nil split_index = @argv.index("--") if split_index command = @argv.drop(split_index + 1) @argv = @argv.take(split_index) end # Parse the options argv = parse_options(opts) return if !argv # Show the error if we don't have "--" _after_ parse_options # so that "-h" and "--help" work properly. if !split_index raise Errors::ExecCommandRequired end target_opts = { provider: :docker } target_opts[:single_target] = options[:pty] with_target_vms(argv, target_opts) do |machine| if machine.state.id != :running @env.ui.info("#{machine.id} is not running.") next end exec_command(machine, command, options) end return 0 end def exec_command(machine, command, options) exec_cmd = %w(docker exec) exec_cmd << "-i" if options[:interactive] exec_cmd << "-t" if options[:pty] exec_cmd << "-u" << options[:user] if options[:user] exec_cmd << machine.id exec_cmd += options[:extra_args] if options[:extra_args] exec_cmd += command # Run this interactively if asked. exec_options = options if options[:pty] Vagrant::Util::SafeExec.exec(exec_cmd[0], *exec_cmd[1..-1]) else output = "" machine.provider.driver.execute(*exec_cmd, **exec_options) do |type, data| output += data end output_options = {} output_options[:prefix] = false if !options[:prefix] if !output.empty? machine.ui.output(output.chomp, **output_options) end end end end end end end ================================================ FILE: plugins/providers/docker/command/logs.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Command class Logs < Vagrant.plugin("2", :command) def self.synopsis "outputs the logs from the Docker container" end def execute options = {} options[:follow] = false options[:prefix] = true opts = OptionParser.new do |o| o.banner = "Usage: vagrant docker-logs [options]" o.separator "" o.separator "Options:" o.separator "" o.on("--[no-]follow", "Continue streaming in log output") do |f| options[:follow] = f end o.on("--[no-]prefix", "Prefix output with machine names") do |p| options[:prefix] = p end end # Parse the options argv = parse_options(opts) return if !argv # This keeps track of if we ran our action on any machines... any_success = false # Start a batch action that sends all the logs to stdout. This # will parallelize, if enabled, across all containers that are # chosen. @env.batch do |batch| with_target_vms(argv) do |machine| if machine.provider_name != :docker machine.ui.output(I18n.t("docker_provider.not_docker_provider")) next end state = machine.state.id if state == :host_state_unknown machine.ui.output(I18n.t("docker_provider.logs_host_state_unknown")) next elsif state == :not_created machine.ui.output(I18n.t("docker_provider.not_created_skip")) next end # At least one was run! any_success = true batch.custom(machine) do |m| execute_single(m, options) end end end # If we didn't run on any machines, then exit status 1 return any_success ? 0 : 1 end protected # Executes the "docker logs" command on a single machine and proxies # the output to our UI. def execute_single(machine, options) command = ["docker", "logs"] command << "--follow" if options[:follow] command << machine.id output_options = {} output_options[:prefix] = false if !options[:prefix] data_acc = "" machine.provider.driver.execute(*command) do |type, data| # Accumulate the data so we only output lines at a time data_acc << data # If we have a newline, then output all the lines we have so far if data_acc.include?("\n") lines = data_acc.split("\n") if !data_acc.end_with?("\n") data_acc = lines.pop.chomp else data_acc = "" end lines.each do |line| line = " " if line == "" machine.ui.output(line, **output_options) end end end # Output any remaining data machine.ui.output(data_acc, **output_options) if !data_acc.empty? end end end end end ================================================ FILE: plugins/providers/docker/command/run.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Command class Run < Vagrant.plugin("2", :command) def self.synopsis "run a one-off command in the context of a container" end def execute options = {} options[:detach] = false options[:pty] = false options[:rm] = true opts = OptionParser.new do |o| o.banner = "Usage: vagrant docker-run [command...]" o.separator "" o.separator "Options:" o.separator "" o.on("--[no-]detach", "Run in the background") do |d| options[:detach] = d end o.on("-t", "--[no-]tty", "Allocate a pty") do |t| options[:pty] = t end o.on("-r,", "--[no-]rm", "Remove container after execution") do |r| options[:rm] = r end end # Parse out the extra args to send to SSH, which is everything # after the "--" command = nil split_index = @argv.index("--") if split_index command = @argv.drop(split_index + 1) @argv = @argv.take(split_index) end # Parse the options argv = parse_options(opts) return if !argv # Show the error if we don't have "--" _after_ parse_options # so that "-h" and "--help" work properly. if !split_index @env.ui.error(I18n.t("docker_provider.run_command_required")) return 1 end target_opts = { provider: :docker } target_opts[:single_target] = options[:pty] with_target_vms(argv, target_opts) do |machine| # Run it! machine.action( :run_command, run_command: command, run_detach: options[:detach], run_pty: options[:pty], run_rm: options[:rm] ) end 0 end end end end end ================================================ FILE: plugins/providers/docker/communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/md5" require "tempfile" module VagrantPlugins module DockerProvider # This communicator uses the host VM as proxy to communicate to the # actual Docker container via SSH. class Communicator < Vagrant.plugin("2", :communicator) def initialize(machine) @machine = machine @host_vm = machine.provider.host_vm # We only work on the Docker provider if machine.provider_name != :docker raise Errors::CommunicatorNotDocker end end #------------------------------------------------------------------- # Communicator Methods #------------------------------------------------------------------- def ready? # We can't be ready if we can't talk to the host VM return false if !@host_vm.communicate.ready? # We're ready if we can establish an SSH connection to the container command = container_ssh_command return false if !command @host_vm.communicate.test("#{command} exit") end def download(from, to) # Same process as upload, but in reverse # First, we use `cat` to copy that file from the Docker container. temp = "/tmp/docker_d#{Time.now.to_i}_#{rand(100000)}" @host_vm.communicate.execute("#{container_ssh_command} 'cat #{from}' >#{temp}") # Then, we download this from the host VM. @host_vm.communicate.download(temp, to) # Remove the temporary file @host_vm.communicate.execute("rm -f #{temp}", error_check: false) end def execute(command, **opts, &block) fence = {} fence[:stderr] = "VAGRANT FENCE: #{Time.now.to_i} #{rand(100000)}" fence[:stdout] = "VAGRANT FENCE: #{Time.now.to_i} #{rand(100000)}" # We want to emulate how the SSH communicator actually executes # things, so we build up the list of commands to execute in a # giant shell script. tf = Tempfile.new("vagrant") tf.binmode tf.write("export TERM=vt100\n") tf.write("echo #{fence[:stdout]}\n") tf.write("echo #{fence[:stderr]} >&2\n") tf.write("#{command}\n") tf.write("exit\n") tf.close # Upload the temp file to the remote machine remote_temp = "/tmp/docker_#{Time.now.to_i}_#{rand(100000)}" @host_vm.communicate.upload(tf.path, remote_temp) # Determine the shell to execute. Prefer the explicitly passed in shell # over the default configured shell. If we are using `sudo` then we # need to wrap the shell in a `sudo` call. shell_cmd = @machine.config.ssh.shell shell_cmd = opts[:shell] if opts[:shell] shell_cmd = "sudo -E -H #{shell_cmd}" if opts[:sudo] acc = {} fenced = {} result = @host_vm.communicate.execute( "#{container_ssh_command} '#{shell_cmd}' <#{remote_temp}", opts) do |type, data| # If we don't have a block, we don't care about the data next if !block # We only care about stdout and stderr output next if ![:stdout, :stderr].include?(type) # If we reached our fence, then just output if fenced[type] block.call(type, data) next end # Otherwise, accumulate acc[type] = data # Look for the fence index = acc[type].index(fence[type]) next if !index fenced[type] = true index += fence[type].length data = acc[type][index..-1].chomp acc[type] = "" block.call(type, data) end @host_vm.communicate.execute("rm -f #{remote_temp}", error_check: false) return result end def sudo(command, **opts, &block) opts = { sudo: true }.merge(opts) execute(command, opts, &block) end def test(command, **opts) opts = { error_check: false }.merge(opts) execute(command, opts) == 0 end def upload(from, to) # First, we upload this to the host VM to some temporary directory. to_temp = "/tmp/docker_#{Time.now.to_i}_#{rand(100000)}" @host_vm.communicate.upload(from, to_temp) # Then, we use `cat` to get that file into the Docker container. @host_vm.communicate.execute( "#{container_ssh_command} 'cat >#{to}' <#{to_temp}") # Remove the temporary file @host_vm.communicate.execute("rm -f #{to_temp}", error_check: false) end #------------------------------------------------------------------- # Other Methods #------------------------------------------------------------------- # This returns the raw SSH command string that can be used to # connect via SSH to the container if you're on the same machine # as the container. # # @return [String] def container_ssh_command # Get the container's SSH info info = @machine.ssh_info return nil if !info info[:port] ||= 22 # Make sure our private keys are synced over to the host VM ssh_args = sync_private_keys(info).map do |path| "-i #{path}" end # Use ad-hoc SSH options for the hop on the docker proxy if info[:forward_agent] ssh_args << "-o ForwardAgent=yes" end ssh_args.concat(["-o Compression=yes", "-o ConnectTimeout=5", "-o StrictHostKeyChecking=no", "-o UserKnownHostsFile=/dev/null"]) # Build the SSH command "ssh #{info[:username]}@#{info[:host]} -p#{info[:port]} #{ssh_args.join(" ")}" end protected def sync_private_keys(info) @keys ||= {} id = Digest::MD5.hexdigest( @machine.env.root_path.to_s + @machine.name.to_s) result = [] info[:private_key_path].each do |path| if !@keys[path.to_s] # We haven't seen this before, upload it! guest_path = "/tmp/key_#{id}_#{Digest::MD5.hexdigest(path.to_s)}" @host_vm.communicate.upload(path.to_s, guest_path) # Make sure it has the proper chmod @host_vm.communicate.execute("chmod 0600 #{guest_path}") # Set it @keys[path.to_s] = guest_path end result << @keys[path.to_s] end result end end end end ================================================ FILE: plugins/providers/docker/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require_relative "../../../lib/vagrant/util/platform" module VagrantPlugins module DockerProvider class Config < Vagrant.plugin("2", :config) attr_accessor :image, :cmd, :ports, :volumes, :privileged # Additional arguments to pass to `docker build` when creating # an image using the build dir setting. # # @return [Array] attr_accessor :build_args # The directory with a Dockerfile to build and use as the basis # for this container. If this is set, neither "image" nor "git_repo" # should be set. # # @return [String] attr_accessor :build_dir # The URL for a git repository with a Dockerfile to build and use # as the basis for this container. If this is set, neither "image" # nor "build_dir" should be set. # # @return [String] attr_accessor :git_repo # Use docker-compose to manage the lifecycle and environment for # containers instead of using docker directly. # # @return [Boolean] attr_accessor :compose # Configuration Hash used for build the docker-compose composition # file. This can be used for adding networks or volumes. # # @return [Hash] attr_accessor :compose_configuration # An optional file name of a Dockerfile to be used when building # the image. This requires Docker >1.5.0. # # @return [String] attr_accessor :dockerfile # Additional arguments to pass to `docker run` when creating # the container for the first time. This is an array of args. # # @return [Array] attr_accessor :create_args # Environmental variables to set in the container. # # @return [Hash] attr_accessor :env # Ports to expose from the container but not to the host machine. # This is useful for links. # # @return [Array] attr_accessor :expose # Force using a proxy VM, even on Linux hosts. # # @return [Boolean] attr_accessor :force_host_vm # True if the Docker container exposes SSH access. If this is true, # then Vagrant can do a bunch more things like setting the hostname, # provisioning, etc. attr_accessor :has_ssh # Options for the build dir synced folder if a host VM is in use. # # @return [Hash] attr_accessor :host_vm_build_dir_options # The name for the container. This must be unique for all containers # on the proxy machine if it is made. # # @return [String] attr_accessor :name # If true, the image will be pulled on every `up` and `reload` # to ensure the latest image. # # @return [Bool] attr_accessor :pull # True if the docker container is meant to stay in the "running" # state (is a long running process). By default this is true. # # @return [Boolean] attr_accessor :remains_running # The time to wait before sending a SIGTERM to the container # when it is stopped. # # @return [Integer] attr_accessor :stop_timeout # The name of the machine in the Vagrantfile set with # "vagrant_vagrantfile" that will be the docker host. Defaults # to "default" # # See the "vagrant_vagrantfile" docs for more info. # # @return [String] attr_accessor :vagrant_machine # The path to the Vagrantfile that contains a VM that will be # started as the Docker host if needed (Windows, OS X, Linux # without container support). # # Defaults to a built-in Vagrantfile that will load boot2docker. # # NOTE: This only has an effect if Vagrant needs a Docker host. # Vagrant determines this automatically based on the environment # it is running in. # # @return [String] attr_accessor :vagrant_vagrantfile #-------------------------------------------------------------- # Auth Settings #-------------------------------------------------------------- # Server to authenticate to. If blank, will use the default # Docker authentication endpoint (which is the Docker Hub at the # time of this comment). # # @return [String] attr_accessor :auth_server # Email for logging in to a remote Docker server. # # @return [String] attr_accessor :email # Email for logging in to a remote Docker server. # # @return [String] attr_accessor :username # Password for logging in to a remote Docker server. If this is # not blank, then Vagrant will run `docker login` prior to any # Docker runs. # # The presence of auth will also force the Docker environments to # serialize on `up` so that different users/passwords don't overlap. # # @return [String] attr_accessor :password def initialize @build_args = [] @build_dir = UNSET_VALUE @git_repo = UNSET_VALUE @cmd = UNSET_VALUE @compose = UNSET_VALUE @compose_configuration = {} @create_args = UNSET_VALUE @dockerfile = UNSET_VALUE @env = {} @expose = [] @force_host_vm = UNSET_VALUE @has_ssh = UNSET_VALUE @host_vm_build_dir_options = UNSET_VALUE @image = UNSET_VALUE @name = UNSET_VALUE @links = [] @pull = UNSET_VALUE @ports = UNSET_VALUE @privileged = UNSET_VALUE @remains_running = UNSET_VALUE @stop_timeout = UNSET_VALUE @volumes = [] @vagrant_machine = UNSET_VALUE @vagrant_vagrantfile = UNSET_VALUE @auth_server = UNSET_VALUE @email = UNSET_VALUE @username = UNSET_VALUE @password = UNSET_VALUE end def link(name) @links << name end def merge(other) super.tap do |result| # This is a bit confusing. The tests explain the purpose of this # better than the code lets on, I believe. has_image = (other.image != UNSET_VALUE) has_build_dir = (other.build_dir != UNSET_VALUE) has_git_repo = (other.git_repo != UNSET_VALUE) if (has_image ^ has_build_dir ^ has_git_repo) && !(has_image && has_build_dir && has_git_repo) # image if has_image if @build_dir != UNSET_VALUE result.build_dir = nil end if @git_repo != UNSET_VALUE result.git_repo = nil end end # build_dir if has_build_dir if @image != UNSET_VALUE result.image = nil end if @git_repo != UNSET_VALUE result.git_repo = nil end end # git_repo if has_git_repo if @build_dir != UNSET_VALUE result.build_dir = nil end if @image != UNSET_VALUE result.image = nil end end end env = {} env.merge!(@env) if @env env.merge!(other.env) if other.env result.env = env expose = self.expose.dup expose += other.expose result.instance_variable_set(:@expose, expose) links = _links.dup links += other._links result.instance_variable_set(:@links, links) end end def finalize! @build_args = [] if @build_args == UNSET_VALUE @build_dir = nil if @build_dir == UNSET_VALUE @git_repo = nil if @git_repo == UNSET_VALUE @cmd = [] if @cmd == UNSET_VALUE @compose = false if @compose == UNSET_VALUE @create_args = [] if @create_args == UNSET_VALUE @dockerfile = nil if @dockerfile == UNSET_VALUE @env ||= {} @has_ssh = false if @has_ssh == UNSET_VALUE @image = nil if @image == UNSET_VALUE @name = nil if @name == UNSET_VALUE @pull = false if @pull == UNSET_VALUE @ports = [] if @ports == UNSET_VALUE @privileged = false if @privileged == UNSET_VALUE @remains_running = true if @remains_running == UNSET_VALUE @stop_timeout = 1 if @stop_timeout == UNSET_VALUE @vagrant_machine = nil if @vagrant_machine == UNSET_VALUE @vagrant_vagrantfile = nil if @vagrant_vagrantfile == UNSET_VALUE @auth_server = nil if @auth_server == UNSET_VALUE @email = "" if @email == UNSET_VALUE @username = "" if @username == UNSET_VALUE @password = "" if @password == UNSET_VALUE if @host_vm_build_dir_options == UNSET_VALUE @host_vm_build_dir_options = nil end # On non-linux platforms (where there is no native docker), force the # host VM. Other users can optionally disable this by setting the # value explicitly to false in their Vagrantfile. if @force_host_vm == UNSET_VALUE @force_host_vm = !Vagrant::Util::Platform.linux? && !Vagrant::Util::Platform.darwin? && !Vagrant::Util::Platform.windows? end # The machine name must be a symbol @vagrant_machine = @vagrant_machine.to_sym if @vagrant_machine @expose.uniq! if @compose_configuration.is_a?(Hash) # Ensures configuration is using basic types @compose_configuration = JSON.parse(@compose_configuration.to_json) end end def validate(machine) errors = _detected_errors if [@build_dir, @git_repo, @image].compact.size > 1 errors << I18n.t("docker_provider.errors.config.both_build_and_image_and_git") end if !@build_dir && !@git_repo && !@image errors << I18n.t("docker_provider.errors.config.build_dir_or_image") end if @build_dir build_dir_pn = Pathname.new(@build_dir) if !build_dir_pn.directory? errors << I18n.t("docker_provider.errors.config.build_dir_invalid") end end # Comparison logic taken directly from docker's urlutil.go if @git_repo && !( @git_repo =~ /^http(?:s)?:\/\/.*.git(?:#.+)?$/ || @git_repo =~ /^git(?:hub\.com|@|:\/\/)/) errors << I18n.t("docker_provider.errors.config.git_repo_invalid") end if !@compose_configuration.is_a?(Hash) errors << I18n.t("docker_provider.errors.config.compose_configuration_hash") end if @compose && @force_host_vm errors << I18n.t("docker_provider.errors.config.compose_force_vm") end if !@create_args.is_a?(Array) errors << I18n.t("docker_provider.errors.config.create_args_array") end @links.each do |link| parts = link.split(":") if parts.length != 2 || parts[0] == "" || parts[1] == "" errors << I18n.t( "docker_provider.errors.config.invalid_link", link: link) end end if @vagrant_vagrantfile vf_pn = Pathname.new(@vagrant_vagrantfile) if !vf_pn.file? errors << I18n.t("docker_provider.errors.config.invalid_vagrantfile") end end { "docker provider" => errors } end #-------------------------------------------------------------- # Functions below should not be called by config files #-------------------------------------------------------------- def _links @links end end end end ================================================ FILE: plugins/providers/docker/driver/compose.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" require "log4r" module VagrantPlugins module DockerProvider class Driver class Compose < Driver # @return [Integer] Maximum number of seconds to wait for lock LOCK_TIMEOUT = 60 # @return [String] Compose file format version COMPOSE_VERSION = "2".freeze # @return [Pathname] data directory to store composition attr_reader :data_directory # @return [Vagrant::Machine] attr_reader :machine # Create a new driver instance # # @param [Vagrant::Machine] machine Machine instance for this driver def initialize(machine) if !Vagrant::Util::Which.which("docker-compose") raise Errors::DockerComposeNotInstalledError end super() @machine = machine @data_directory = Pathname.new(machine.env.local_data_path). join("docker-compose") @data_directory.mkpath @logger = Log4r::Logger.new("vagrant::docker::driver::compose") @compose_lock = Mutex.new @logger.debug("Docker compose driver initialize for machine `#{@machine.name}` (`#{@machine.id}`)") @logger.debug("Data directory for composition file `#{@data_directory}`") end # Updates the docker compose config file with the given arguments # # @param [String] dir - local directory or git repo URL # @param [Hash] opts - valid key: extra_args # @param [Block] block # @return [Nil] def build(dir, **opts, &block) name = machine.name.to_s @logger.debug("Applying build for `#{name}` using `#{dir}` directory.") begin update_composition do |composition| services = composition["services"] ||= {} services[name] ||= {} services[name]["build"] = {"context" => dir} # Extract custom dockerfile location if set if opts[:extra_args] && opts[:extra_args].include?("--file") services[name]["build"]["dockerfile"] = opts[:extra_args][opts[:extra_args].index("--file") + 1] end # Extract any build args that can be found case opts[:extra_args] when Array if opts[:extra_args].include?("--build-arg") idx = 0 extra_args = {} while(idx < opts[:extra_args].size) arg_value = opts[:extra_args][idx] idx += 1 if arg_value.start_with?("--build-arg") if !arg_value.include?("=") arg_value = opts[:extra_args][idx] idx += 1 end key, val = arg_value.to_s.split("=", 2).to_s.split("=") extra_args[key] = val end end end when Hash services[name]["build"]["args"] = opts[:extra_args] end end rescue => error @logger.error("Failed to apply build using `#{dir}` directory: #{error.class} - #{error}") update_composition do |composition| composition["services"].delete(name) end raise end end def create(params, **opts, &block) # NOTE: Use the direct machine name as we don't # need to worry about uniqueness with compose name = machine.name.to_s image = params.fetch(:image) links = Array(params.fetch(:links, [])).map do |link| case link when Array link else link.to_s.split(":") end end ports = Array(params[:ports]) volumes = Array(params[:volumes]).map do |v| v = v.to_s host, guest = v.split(":", 2) if v.include?(":") && (Vagrant::Util::Platform.windows? || Vagrant::Util::Platform.wsl?) host = Vagrant::Util::Platform.windows_path(host) # NOTE: Docker does not support UNC style paths (which also # means that there's no long path support). Hopefully this # will be fixed someday and the gsub below can be removed. host.gsub!(/^[^A-Za-z]+/, "") end # if host path is a volume key, don't expand it. # if both exist (a path and a key) show warning and move on # otherwise assume it's a realative path and expand the host path compose_config = get_composition if compose_config["volumes"] && compose_config["volumes"].keys.include?(host) if File.directory?(@machine.env.cwd.join(host).to_s) @machine.env.ui.warn(I18n.t("docker_provider.volume_path_not_expanded", host: host)) end else @logger.debug("Path expanding #{host} to current Vagrant working dir instead of docker-compose config file directory") host = @machine.env.cwd.join(host).to_s end "#{host}:#{guest}" end cmd = Array(params.fetch(:cmd)) env = Hash[*params.fetch(:env).flatten.map(&:to_s)] expose = Array(params[:expose]) @logger.debug("Creating container `#{name}`") begin update_args = [:apply] update_args.push(:detach) if params[:detach] update_args << block update_composition(*update_args) do |composition| services = composition["services"] ||= {} services[name] ||= {} if params[:extra_args].is_a?(Hash) services[name].merge!( Hash[ params[:extra_args].map{ |k, v| [k.to_s, v] } ] ) end services[name].merge!( "environment" => env, "expose" => expose, "ports" => ports, "volumes" => volumes, "links" => links, "command" => cmd ) services[name]["image"] = image if image services[name]["hostname"] = params[:hostname] if params[:hostname] services[name]["privileged"] = true if params[:privileged] services[name]["pty"] = true if params[:pty] end rescue => error @logger.error("Failed to create container `#{name}`: #{error.class} - #{error}") update_composition do |composition| composition["services"].delete(name) end raise end get_container_id(name) end def rm(cid) if created?(cid) destroy = false synchronized do compose_execute("rm", "-f", machine.name.to_s) update_composition do |composition| if composition["services"] && composition["services"].key?(machine.name.to_s) @logger.info("Removing container `#{machine.name}`") if composition["services"].size > 1 composition["services"].delete(machine.name.to_s) else destroy = true end end end if destroy @logger.info("No containers remain. Destroying full environment.") compose_execute("down", "--volumes", "--rmi", "local") @logger.info("Deleting composition path `#{composition_path}`") composition_path.delete end end end end def rmi(*_) true end def created?(cid) result = super if !result composition = get_composition if composition["services"] && composition["services"].has_key?(machine.name.to_s) result = true end end result end private # Lookup the ID for the container with the given name # # @param [String] name Name of container # @return [String] Container ID def get_container_id(name) compose_execute("ps", "-q", name).chomp end # Execute a `docker-compose` command def compose_execute(*cmd, **opts, &block) synchronized do execute("docker-compose", "-f", composition_path.to_s, "-p", machine.env.cwd.basename.to_s, *cmd, **opts, &block) end end # Apply any changes made to the composition def apply_composition!(*args) block = args.detect{|arg| arg.is_a?(Proc) } execute_args = ["up", "--remove-orphans"] if args.include?(:detach) execute_args << "-d" end machine.env.lock("compose", retry: true) do if block compose_execute(*execute_args, &block) else compose_execute(*execute_args) end end end # Update the composition and apply changes if requested # # @param [Boolean] apply Apply composition changes def update_composition(*args) synchronized do machine.env.lock("compose", retry: true) do composition = get_composition result = yield composition write_composition(composition) if args.include?(:apply) || (args.include?(:conditional) && result) apply_composition!(*args) end end end end # @return [Hash] current composition contents def get_composition composition = {"version" => COMPOSE_VERSION.dup} if composition_path.exist? composition = Vagrant::Util::DeepMerge.deep_merge(composition, YAML.load(composition_path.read)) end composition = Vagrant::Util::DeepMerge.deep_merge(composition, machine.provider_config.compose_configuration.dup) @logger.debug("Fetched composition with provider configuration applied: #{composition}") composition end # Save the composition # # @param [Hash] composition New composition def write_composition(composition) @logger.debug("Saving composition to `#{composition_path}`: #{composition}") tmp_file = Tempfile.new("vagrant-docker-compose") tmp_file.write(composition.to_yaml) tmp_file.close synchronized do FileUtils.mv(tmp_file.path, composition_path.to_s) end end # @return [Pathname] path to the docker-compose.yml file def composition_path data_directory.join("docker-compose.yml") end def synchronized if !@compose_lock.owned? timeout = LOCK_TIMEOUT.to_f until @compose_lock.owned? if @compose_lock.try_lock if timeout > 0 timeout -= sleep(1) else raise Errors::ComposeLockTimeoutError end end end got_lock = true end begin result = yield ensure @compose_lock.unlock if got_lock end result end end end end end ================================================ FILE: plugins/providers/docker/driver.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" require "log4r" require_relative "./driver/compose" module VagrantPlugins module DockerProvider class Driver # The executor is responsible for actually executing Docker commands. # This is set by the provider, but defaults to local execution. attr_accessor :executor def initialize @logger = Log4r::Logger.new("vagrant::docker::driver") @executor = Executor::Local.new end # Returns the id for a new container built from `docker build`. Raises # an exception if the id was unable to be captured from the output # # @return [String] id - ID matched from the docker build output. def build(dir, **opts, &block) args = Array(opts[:extra_args]) args << dir opts = {with_stderr: true} result = execute('docker', 'build', *args, **opts, &block) # Check for the new output format 'writing image sha256...' # In this case, docker buildkit is enabled. Its format is different # from standard docker matches = result.scan(/writing image .+:([^\s]+)/i).last if !matches # Check for output of docker using containerd backend store matches = result.scan(/exporting manifest list .+:([^\s]+)/i).last end if !matches if podman? # Check for podman format when it is emulating docker CLI. # Podman outputs the full hash of the container on # the last line after a successful build. match = result.split.select { |str| str.match?(/^[0-9a-z]{64}/) }.last return match[0..7] unless match.nil? else matches = result.scan(/Successfully built (.+)$/i).last end if !matches # This will cause a stack trace in Vagrant, but it is a bug # if this happens anyways. raise Errors::BuildError, result: result end end # Return the matched group `id` matches[0].strip end # Check if podman emulating docker CLI is enabled. # # @return [Bool] def podman? execute('docker', '--version').include?("podman") end def create(params, **opts, &block) image = params.fetch(:image) links = params.fetch(:links) ports = Array(params[:ports]) volumes = Array(params[:volumes]) name = params.fetch(:name) cmd = Array(params.fetch(:cmd)) env = params.fetch(:env) expose = Array(params[:expose]) run_cmd = %W(docker run --name #{name}) run_cmd << "-d" if params[:detach] run_cmd += env.map { |k,v| ['-e', "#{k}=#{v}"] } run_cmd += expose.map { |p| ['--expose', "#{p}"] } run_cmd += links.map { |k, v| ['--link', "#{k}:#{v}"] } run_cmd += ports.map { |p| ['-p', p.to_s] } run_cmd += volumes.map { |v| v = v.to_s if v.include?(":") && @executor.windows? if v.index(":") != v.rindex(":") # If we have 2 colons, the host path is an absolute Windows URL # and we need to remove the colon from it host, _, guest = v.rpartition(":") host = "//" + host[0].downcase + host[2..-1] v = [host, guest].join(":") else host, guest = v.split(":", 2) host = Vagrant::Util::Platform.windows_path(host) # NOTE: Docker does not support UNC style paths (which also # means that there's no long path support). Hopefully this # will be fixed someday and the gsub below can be removed. host.gsub!(/^[^A-Za-z]+/, "") v = [host, guest].join(":") end end ['-v', v.to_s] } run_cmd += %W(--privileged) if params[:privileged] run_cmd += %W(-h #{params[:hostname]}) if params[:hostname] run_cmd << "-t" if params[:pty] run_cmd << "--rm=true" if params[:rm] run_cmd += params[:extra_args] if params[:extra_args] run_cmd += [image, cmd] execute(*run_cmd.flatten, **opts, &block).chomp.lines.last end def state(cid) case when running?(cid) :running when created?(cid) :stopped else :not_created end end def created?(cid) result = execute('docker', 'ps', '-a', '-q', '--no-trunc').to_s result =~ /^#{Regexp.escape cid}$/ end def image?(id) result = execute('docker', 'images', '-q', '--no-trunc').to_s result =~ /\b#{Regexp.escape(id)}\b/ end # Reads all current docker containers and determines what ports # are currently registered to be forwarded # {2222=>#, 8080=>#, 9090=>#} # # Note: This is this format because of what the builtin action for resolving colliding # port forwards expects. # # @return [Hash[Set]] used_ports - {forward_port: #} def read_used_ports used_ports = Hash.new{|hash,key| hash[key] = Set.new} all_containers.each do |c| container_info = inspect_container(c) active = container_info["State"]["Running"] next unless active # Ignore used ports on inactive containers if container_info["HostConfig"]["PortBindings"] port_bindings = container_info["HostConfig"]["PortBindings"] next if port_bindings.empty? # Nothing defined, but not nil either port_bindings.each do |guest_port,host_mapping| host_mapping.each do |h| if h["HostIp"] == "" hostip = "*" else hostip = h["HostIp"] end hostport = h["HostPort"] used_ports[hostport].add(hostip) end end end end used_ports end def running?(cid) result = execute('docker', 'ps', '-q', '--no-trunc') result =~ /^#{Regexp.escape cid}$/m end def privileged?(cid) inspect_container(cid)['HostConfig']['Privileged'] end def login(email, username, password, server) cmd = %W(docker login) cmd += ["-e", email] if email != "" cmd += ["-u", username] if username != "" cmd += ["-p", password] if password != "" cmd << server if server && server != "" execute(*cmd.flatten) end def logout(server) cmd = %W(docker logout) cmd << server if server && server != "" execute(*cmd.flatten) end def pull(image) execute('docker', 'pull', image) end def start(cid) if !running?(cid) execute('docker', 'start', cid) # This resets the cached information we have around, allowing `vagrant reload`s # to work properly @data = nil end end def stop(cid, timeout) if running?(cid) execute('docker', 'stop', '-t', timeout.to_s, cid) end end def rm(cid) if created?(cid) execute('docker', 'rm', '-f', '-v', cid) end end def rmi(id) execute('docker', 'rmi', id) return true rescue => e return false if e.to_s.include?("is using it") or e.to_s.include?("is being used") or e.to_s.include?("is in use") raise if !e.to_s.include?("No such image") end # Inspect the provided container # # @param [String] cid ID or name of container # @return [Hash] def inspect_container(cid) JSON.parse(execute('docker', 'inspect', cid)).first end # @return [Array] list of all container IDs def all_containers execute('docker', 'ps', '-a', '-q', '--no-trunc').to_s.split end # Attempts to first use the docker-cli tool to inspect the default bridge subnet # Falls back to using /sbin/ip if that fails # # @return [String] IP address of the docker bridge def docker_bridge_ip bridge = inspect_network("bridge")&.first if bridge bridge_ip = bridge.dig("IPAM", "Config", 0, "Gateway") end return bridge_ip if bridge_ip @logger.debug("Failed to get bridge ip from docker, falling back to `ip`") docker_bridge_ip_fallback end def docker_bridge_ip_fallback output = execute('ip', '-4', 'addr', 'show', 'scope', 'global', 'docker0') if output =~ /^\s+inet ([0-9.]+)\/[0-9]+\s+/ return $1.to_s else # TODO: Raise a user-friendly message raise 'Unable to fetch docker bridge IP!' end end # @param [String] network - name of network to connect container to # @param [String] cid - container id # @param [Array] opts - An array of flags used for listing networks def connect_network(network, cid, opts=nil) command = ['docker', 'network', 'connect', network, cid].push(*opts) output = execute(*command) output end # @param [String] network - name of network to create # @param [Array] opts - An array of flags used for listing networks def create_network(network, opts=nil) command = ['docker', 'network', 'create', network].push(*opts) output = execute(*command) output end # @param [String] network - name of network to disconnect container from # @param [String] cid - container id def disconnect_network(network, cid) command = ['docker', 'network', 'disconnect', network, cid, "--force"] output = execute(*command) output end # @param [Array] networks - list of networks to inspect # @param [Array] opts - An array of flags used for listing networks def inspect_network(network, opts=nil) command = ['docker', 'network', 'inspect'] + Array(network) command = command.push(*opts) output = execute(*command) begin JSON.load(output) rescue JSON::ParserError @logger.warn("Failed to parse network inspection of network: #{network}") @logger.debug("Failed network output content: `#{output.inspect}`") nil end end # @param [String] opts - Flags used for listing networks def list_network(*opts) command = ['docker', 'network', 'ls', *opts] output = execute(*command) output end # Will delete _all_ defined but unused networks in the docker engine. Even # networks not created by Vagrant. # # @param [Array] opts - An array of flags used for listing networks def prune_network(opts=nil) command = ['docker', 'network', 'prune', '--force'].push(*opts) output = execute(*command) output end # Delete network(s) # # @param [String] network - name of network to remove def rm_network(*network) command = ['docker', 'network', 'rm', *network] output = execute(*command) output end # @param [Array] opts - An array of flags used for listing networks def execute(*cmd, **opts, &block) @executor.execute(*cmd, **opts, &block) end # ###################### # Docker network helpers # ###################### # Determines if a given network has been defined through vagrant with a given # subnet string # # @param [String] subnet_string - Subnet to look for # @return [String] network name - Name of network with requested subnet.`nil` if not found def network_defined?(subnet_string) all_networks = list_network_names network_info = inspect_network(all_networks) network_info.each do |network| config = Array(network.dig("IPAM", "Config")) next if config.empty? || !config.first.is_a?(Hash) if (config.first["Subnet"] == subnet_string) @logger.debug("Found existing network #{network["Name"]} already configured with #{subnet_string}") return network["Name"] end end return nil end # Locate network which contains given address # # @param [String] address IP address # @return [String] network name def network_containing_address(address) names = list_network_names networks = inspect_network(names) return if !networks networks.each do |net| next if !net["IPAM"] config = net["IPAM"]["Config"] next if !config || config.size < 1 config.each do |opts| subnet = IPAddr.new(opts["Subnet"]) if subnet.include?(address) return net["Name"] end end end nil end # Looks to see if a docker network has already been defined # with the given name # # @param [String] network_name - name of network to look for # @return [Bool] def existing_named_network?(network_name) result = list_network_names result.any?{|net_name| net_name == network_name} end # @return [Array] list of all docker networks def list_network_names list_network("--format={{.Name}}").split("\n").map(&:strip) end # Returns true or false if network is in use or not. # Nil if Vagrant fails to receive proper JSON from `docker network inspect` # # @param [String] network - name of network to look for # @return [Bool,nil] def network_used?(network) result = inspect_network(network) return nil if !result return result.first["Containers"].size > 0 end end end end ================================================ FILE: plugins/providers/docker/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider module Errors class DockerError < Vagrant::Errors::VagrantError error_namespace("docker_provider.errors") end class BuildError < DockerError error_key(:build_error) end class CommunicatorNonDocker < DockerError error_key(:communicator_non_docker) end class ComposeLockTimeoutError < DockerError error_key(:compose_lock_timeout) end class ContainerNotRunningError < DockerError error_key(:not_running) end class ContainerNotCreatedError < DockerError error_key(:not_created) end class DockerComposeNotInstalledError < DockerError error_key(:docker_compose_not_installed) end class ExecuteError < DockerError error_key(:execute_error) end class ExecCommandRequired < DockerError error_key(:exec_command_required) end class HostVMCommunicatorNotReady < DockerError error_key(:host_vm_communicator_not_ready) end class ImageNotConfiguredError < DockerError error_key(:docker_provider_image_not_configured) end class NfsWithoutPrivilegedError < DockerError error_key(:docker_provider_nfs_without_privileged) end class NetworkAddressInvalid < DockerError error_key(:network_address_invalid) end class NetworkIPAddressRequired < DockerError error_key(:network_address_required) end class NetworkSubnetInvalid < DockerError error_key(:network_subnet_invalid) end class NetworkInvalidOption < DockerError error_key(:network_invalid_option) end class NetworkNameMissing < DockerError error_key(:network_name_missing) end class NetworkNameUndefined < DockerError error_key(:network_name_undefined) end class NetworkNoInterfaces < DockerError error_key(:network_no_interfaces) end class PackageNotSupported < DockerError error_key(:package_not_supported) end class StateNotRunning < DockerError error_key(:state_not_running) end class StateStopped < DockerError error_key(:state_stopped) end class SuspendNotSupported < DockerError error_key(:suspend_not_supported) end class SyncedFolderNonDocker < DockerError error_key(:synced_folder_non_docker) end class VagrantfileNotFound < DockerError error_key(:vagrantfile_not_found) end end end end ================================================ FILE: plugins/providers/docker/executor/local.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/busy" require "vagrant/util/subprocess" module VagrantPlugins module DockerProvider module Executor # The Local executor executes a Docker client that is running # locally. class Local def execute(*cmd, **opts, &block) # Append in the options for subprocess cmd << { notify: [:stdout, :stderr] } interrupted = false int_callback = ->{ interrupted = true } result = ::Vagrant::Util::Busy.busy(int_callback) do ::Vagrant::Util::Subprocess.execute(*cmd, &block) end result.stderr.gsub!("\r\n", "\n") result.stdout.gsub!("\r\n", "\n") if result.exit_code != 0 && !interrupted raise Errors::ExecuteError, command: cmd.inspect, stderr: result.stderr, stdout: result.stdout end if opts if opts[:with_stderr] return result.stdout + " " + result.stderr else return result.stdout end end end def windows? ::Vagrant::Util::Platform.windows? || ::Vagrant::Util::Platform.wsl? end end end end end ================================================ FILE: plugins/providers/docker/executor/vagrant.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/shell_quote" module VagrantPlugins module DockerProvider module Executor # The Vagrant executor runs Docker over SSH against the given # Vagrant-managed machine. class Vagrant def initialize(host_machine) @host_machine = host_machine end def execute(*cmd, **opts, &block) quote = '"' cmd = cmd.map do |a| "#{quote}#{::Vagrant::Util::ShellQuote.escape(a, quote)}#{quote}" end.join(" ") # If we want stdin, we just run in a full subprocess return ssh_run(cmd) if opts[:stdin] # Add a start fence so we know when to start reading output. # We have to do this because boot2docker outputs a login shell # boot2docker version that we get otherwise and messes up output. start_fence = "========== VAGRANT DOCKER BEGIN ==========" ssh_cmd = "echo -n \"#{start_fence}\"; #{cmd}" stderr = "" stdout = "" fenced = false comm = @host_machine.communicate code = comm.execute(ssh_cmd, error_check: false) do |type, data| next if ![:stdout, :stderr].include?(type) stderr << data if type == :stderr stdout << data if type == :stdout if !fenced index = stdout.index(start_fence) if index fenced = true index += start_fence.length stdout = stdout[index..-1] stdout.chomp! # We're now fenced, send all the data through if block block.call(:stdout, stdout) if stdout != "" block.call(:stderr, stderr) if stderr != "" end end else # If we're already fenced, just send the data through. block.call(type, data) if block && fenced end end if code != 0 raise Errors::ExecuteError, command: cmd, stderr: stderr.chomp, stdout: stdout.chomp end stdout.chomp end def windows? false end protected def ssh_run(cmd) @host_machine.action( :ssh_run, ssh_run_command: cmd, ) "" end end end end end ================================================ FILE: plugins/providers/docker/hostmachine/Vagrantfile ================================================ Vagrant.configure("2") do |config| config.vm.box = "hashicorp/boot2docker" end ================================================ FILE: plugins/providers/docker/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider autoload :Action, File.expand_path("../action", __FILE__) autoload :Driver, File.expand_path("../driver", __FILE__) autoload :Errors, File.expand_path("../errors", __FILE__) module Executor autoload :Local, File.expand_path("../executor/local", __FILE__) autoload :Vagrant, File.expand_path("../executor/vagrant", __FILE__) end class Plugin < Vagrant.plugin("2") name "docker-provider" description <<-EOF The Docker provider allows Vagrant to manage and control Docker containers. EOF provider(:docker, box_optional: true, parallel: true, defaultable: false) do require_relative 'provider' init! Provider end command("docker-exec", primary: false) do require_relative "command/exec" init! Command::Exec end command("docker-logs", primary: false) do require_relative "command/logs" init! Command::Logs end command("docker-run", primary: false) do require_relative "command/run" init! Command::Run end communicator(:docker_hostvm) do require_relative "communicator" init! Communicator end config(:docker, :provider) do require_relative 'config' init! Config end synced_folder(:docker) do require File.expand_path("../synced_folder", __FILE__) SyncedFolder end provider_capability("docker", "public_address") do require_relative "cap/public_address" Cap::PublicAddress end provider_capability("docker", "proxy_machine") do require_relative "cap/proxy_machine" Cap::ProxyMachine end provider_capability("docker", "has_communicator") do require_relative "cap/has_communicator" Cap::HasCommunicator end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path( "templates/locales/providers_docker.yml", Vagrant.source_root) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/providers/docker/provider.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/md5" require "fileutils" require "thread" require "log4r" require "vagrant/util/silence_warnings" module VagrantPlugins module DockerProvider class Provider < Vagrant.plugin("2", :provider) @@host_vm_mutex = Mutex.new def self.usable?(raise_error=false) Driver.new.execute("docker", "version") true rescue Vagrant::Errors::CommandUnavailable, Errors::ExecuteError raise if raise_error return false end def initialize(machine) @logger = Log4r::Logger.new("vagrant::provider::docker") @machine = machine if host_vm? # We need to use a special communicator that proxies our # SSH requests over our host VM to the container itself. @machine.config.vm.communicator = :docker_hostvm end end # @see Vagrant::Plugin::V2::Provider#action def action(name) action_method = "action_#{name}" return Action.send(action_method) if Action.respond_to?(action_method) nil end # Returns the driver instance for this provider. def driver if !@driver if @machine.provider_config.compose @driver = Driver::Compose.new(@machine) else @driver = Driver.new end end if host_vm? @driver.executor = Executor::Vagrant.new(host_vm) end @driver end # This returns the {Vagrant::Machine} that is our host machine. # It does not perform any action on the machine or verify it is # running. # # @return [Vagrant::Machine] def host_vm return @host_vm if @host_vm vf_path = @machine.provider_config.vagrant_vagrantfile host_machine_name = @machine.provider_config.vagrant_machine || :default if !vf_path # We don't have a Vagrantfile path set, so we're going to use # the default but we need to copy it into the data dir so that # we don't write into our installation dir (we can't). default_path = File.expand_path("../hostmachine/Vagrantfile", __FILE__) vf_path = @machine.env.data_dir.join("docker-host", "Vagrantfile") begin @machine.env.lock("docker-provider-hostvm") do vf_path.dirname.mkpath FileUtils.cp(default_path, vf_path) end rescue Vagrant::Errors::EnvironmentLockedError # Lock contention, just retry retry end # Set the machine name since we hardcode that for the default host_machine_name = :default end # Expand it so that the home directories and so on get processed # properly. vf_path = File.expand_path(vf_path, @machine.env.root_path) vf_file = File.basename(vf_path) vf_path = File.dirname(vf_path) # Create the env to manage this machine @host_vm = Vagrant::Util::SilenceWarnings.silence! do host_env = Vagrant::Environment.new( cwd: vf_path, home_path: @machine.env.home_path, ui_class: @machine.env.ui_class, vagrantfile_name: vf_file, ) # If there is no root path, then the Vagrantfile wasn't found # and it is an error... raise Errors::VagrantfileNotFound if !host_env.root_path host_env.machine( host_machine_name, host_env.default_provider( exclude: [:docker], force_default: false, )) end @host_vm end # This acquires a lock on the host VM. def host_vm_lock hash = Digest::MD5.hexdigest(host_vm.data_dir.to_s) # We do a process-level mutex on the outside, since we can # wait for that a short amount of time. Then, we do a process lock # on the inside, which will raise an exception if locked. host_vm_mutex.synchronize do @machine.env.lock(hash) do return yield end end end # This is a process-local mutex that can be used by parallel # providers to lock the host VM access. def host_vm_mutex @@host_vm_mutex end # This says whether or not Docker will be running within a VM # rather than directly on our system. Docker needs to run in a VM # when we're not on Linux, or not on a Linux that supports Docker. def host_vm? @machine.provider_config.force_host_vm end # Returns the SSH info for accessing the Container. def ssh_info # If the container isn't running, we can't SSH into it return nil if state.id != :running port_name = "#{@machine.config.ssh.guest_port}/tcp" network = driver.inspect_container(@machine.id)['NetworkSettings'] if network["Ports"][port_name].respond_to?(:first) port_info = network["Ports"][port_name].first else ip = network["IPAddress"] port = @machine.config.ssh.guest_port if !ip.to_s.empty? port_info = { "HostIp" => ip, "HostPort" => port } end end # If we were not able to identify the container's IP, we return nil # here and we let Vagrant core deal with it ;) return nil if port_info.nil? || port_info.empty? { host: port_info['HostIp'], port: port_info['HostPort'] } end def state state_id = nil state_id = :not_created if !@machine.id begin state_id = :host_state_unknown if !state_id && \ host_vm? && !host_vm.communicate.ready? rescue Errors::VagrantfileNotFound state_id = :host_state_unknown end state_id = :not_created if !state_id && \ (!@machine.id || !driver.created?(@machine.id)) state_id = driver.state(@machine.id) if @machine.id && !state_id state_id = :unknown if !state_id # This is a special pseudo-state so that we don't set the # NOT_CREATED_ID while we're setting up the machine. This avoids # clearing the data dir. state_id = :preparing if @machine.id == "preparing" short = state_id.to_s.gsub("_", " ") long = I18n.t("docker_provider.status.#{state_id}") # If we're not created, then specify the special ID flag if state_id == :not_created state_id = Vagrant::MachineState::NOT_CREATED_ID end Vagrant::MachineState.new(state_id, short, long) end def to_s id = @machine.id ? @machine.id : "new container" "Docker (#{id})" end end end end ================================================ FILE: plugins/providers/docker/synced_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvider class SyncedFolder < Vagrant.plugin("2", :synced_folder) def usable?(machine, raise_error=false) # These synced folders only work if the provider is Docker if machine.provider_name != :docker if raise_error raise Errors::SyncedFolderNonDocker, provider: machine.provider_name.to_s end return false end true end def prepare(machine, folders, _opts) folders.each do |id, data| next if data[:ignore] host_path = data[:hostpath] guest_path = data[:guestpath] # Append consistency option if it exists, otherwise let it nil out consistency = data[:docker_consistency] consistency &&= ":" + consistency machine.provider_config.volumes << "#{host_path}:#{guest_path}#{consistency}" end end end end end ================================================ FILE: plugins/providers/hyperv/action/check_access.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class CheckAccess def initialize(app, env) @app = app end def call(env) env[:ui].output("Verifying Hyper-V is accessible...") result = env[:machine].provider.driver.execute(:check_hyperv_access, "Path" => Vagrant::Util::Platform.wsl_to_windows_path(env[:machine].data_dir).gsub("/", "\\") ) if !result["result"] raise Errors::SystemAccessRequired, root_dir: result["root_dir"] end @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/check_enabled.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "log4r" module VagrantPlugins module HyperV module Action class CheckEnabled def initialize(app, env) @app = app end def call(env) env[:ui].output("Verifying Hyper-V is enabled...") result = env[:machine].provider.driver.execute("check_hyperv.ps1", {}) raise Errors::PowerShellFeaturesDisabled if !result["result"] @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/configure.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "log4r" module VagrantPlugins module HyperV module Action class Configure def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::hyperv::configure") end def call(env) switches = env[:machine].provider.driver.execute(:get_switches) if switches.empty? raise Errors::NoSwitches end switch = nil env[:machine].config.vm.networks.each do |type, opts| next if type != :public_network && type != :private_network if opts[:bridge] @logger.debug("Looking for switch with name or ID: #{opts[:bridge]}") switch = switches.find{ |s| s["Name"].downcase == opts[:bridge].to_s.downcase || s["Id"].downcase == opts[:bridge].to_s.downcase } if switch @logger.debug("Found switch - Name: #{switch["Name"]} ID: #{switch["Id"]}") switch = switch["Id"] break end end end # If we already configured previously don't prompt for switch sentinel = env[:machine].data_dir.join("action_configure") if !switch && !sentinel.file? if switches.length > 1 env[:ui].detail(I18n.t("vagrant_hyperv.choose_switch") + "\n ") switches.each_index do |i| switch = switches[i] env[:ui].detail("#{i+1}) #{switch["Name"]}") end env[:ui].detail(" ") switch = nil while !switch switch = env[:ui].ask("What switch would you like to use? ") next if !switch switch = switch.to_i - 1 switch = nil if switch < 0 || switch >= switches.length end switch = switches[switch]["Id"] else switch = switches.first["Id"] @logger.debug("Only single switch available so using that.") end end options = { "VMID" => env[:machine].id, "SwitchID" => switch, "Memory" => env[:machine].provider_config.memory, "MaxMemory" => env[:machine].provider_config.maxmemory, "Processors" => env[:machine].provider_config.cpus, "AutoStartAction" => env[:machine].provider_config.auto_start_action, "AutoStopAction" => env[:machine].provider_config.auto_stop_action, "EnableCheckpoints" => env[:machine].provider_config.enable_checkpoints, "EnableAutomaticCheckpoints" => env[:machine].provider_config.enable_automatic_checkpoints, "VirtualizationExtensions" => !!env[:machine].provider_config.enable_virtualization_extensions, } options.delete_if{|_,v| v.nil? } env[:ui].detail("Configuring the VM...") env[:machine].provider.driver.execute(:configure_vm, options) # Create the sentinel if !sentinel.file? sentinel.open("w") do |f| f.write(Time.now.to_i.to_s) end end if !env[:machine].provider_config.vm_integration_services.empty? env[:ui].detail("Setting VM Integration Services") env[:machine].provider_config.vm_integration_services.each do |key, value| state = value ? "enabled" : "disabled" env[:ui].output("#{key} is #{state}") end env[:machine].provider.driver.set_vm_integration_services( env[:machine].provider_config.vm_integration_services) end if env[:machine].provider_config.enable_enhanced_session_mode env[:ui].detail(I18n.t("vagrant.hyperv_enable_enhanced_session")) env[:machine].provider.driver.set_enhanced_session_transport_type("HvSocket") else env[:ui].detail(I18n.t("vagrant.hyperv_disable_enhanced_session")) env[:machine].provider.driver.set_enhanced_session_transport_type("VMBus") end @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/delete_vm.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class DeleteVM def initialize(app, env) @app = app end def call(env) env[:ui].info("Deleting the machine...") env[:machine].provider.driver.delete_vm # NOTE: We remove the data directory and recreate it # to overcome an issue seen when running within # the WSL. Hyper-V will successfully remove the # VM and the files will appear to be gone, but # on a subsequent up, they will cause collisions. # This forces them to be gone for real. FileUtils.rm_rf(env[:machine].data_dir) FileUtils.mkdir_p(env[:machine].data_dir) @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/export.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" module VagrantPlugins module HyperV module Action class Export def initialize(app, env) @app = app end def call(env) @env = env @env[:ui].info @env[:machine].state.id.to_s raise Vagrant::Errors::VMPowerOffToPackage if @env[:machine].state.id != :off export @app.call(env) end def export @env[:ui].info I18n.t("vagrant.actions.vm.export.exporting") export_tmp_dir = Vagrant::Util::Platform.wsl_to_windows_path(@env["export.temp_dir"]) @env[:machine].provider.driver.export(export_tmp_dir) do |progress| @env[:ui].rewriting do |ui| ui.clear_line ui.report_progress(progress.percent, 100, false) end end # Clear the line a final time so the next data can appear # alone on the line. @env[:ui].clear_line end end end end end ================================================ FILE: plugins/providers/hyperv/action/import.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "log4r" module VagrantPlugins module HyperV module Action class Import VALID_HD_EXTENSIONS = [".vhd".freeze, ".vhdx".freeze].freeze def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::hyperv::import") end def call(env) vm_dir = env[:machine].box.directory.join("Virtual Machines") hd_dir = env[:machine].box.directory.join("Virtual Hard Disks") if !vm_dir.directory? || !hd_dir.directory? @logger.error("Required virtual machine directory not found!") raise Errors::BoxInvalid, name: env[:machine].name end valid_config_ext = [".xml"] if env[:machine].provider.driver.has_vmcx_support? valid_config_ext << ".vmcx" end config_path = nil vm_dir.each_child do |file| if valid_config_ext.include?(file.extname.downcase) config_path = file break end end if !config_path @logger.error("Failed to locate box configuration path") raise Errors::BoxInvalid, name: env[:machine].name else @logger.info("Found box configuration path: #{config_path}") end image_path = nil hd_dir.each_child do |file| if VALID_HD_EXTENSIONS.include?(file.extname.downcase) image_path = file break end end if !image_path @logger.error("Failed to locate box image path") raise Errors::BoxInvalid, name: env[:machine].name else @logger.info("Found box image path: #{image_path}") end env[:ui].output("Importing a Hyper-V instance") dest_path = env[:machine].data_dir.join("Virtual Hard Disks").join(image_path.basename).to_s options = { "VMConfigFile" => Vagrant::Util::Platform.wsl_to_windows_path(config_path).gsub("/", "\\"), "DestinationPath" => Vagrant::Util::Platform.wsl_to_windows_path(dest_path).gsub("/", "\\"), "DataPath" => Vagrant::Util::Platform.wsl_to_windows_path(env[:machine].data_dir).gsub("/", "\\"), "LinkedClone" => !!env[:machine].provider_config.linked_clone, "SourcePath" => Vagrant::Util::Platform.wsl_to_windows_path(image_path).gsub("/", "\\"), "VMName" => env[:machine].provider_config.vmname, "Memory" => env[:machine].provider_config.memory, "MaxMemory" => env[:machine].provider_config.maxmemory, "Processors" => env[:machine].provider_config.cpus, } env[:ui].detail("Creating and registering the VM...") server = env[:machine].provider.driver.import(options) @logger.debug("import result value: #{server.inspect}") sid = case server["id"] when String server["id"] when Array server["id"].first else raise TypeError, "Expected String or Array value, received: #{server["id"].class}" end env[:ui].detail("Successfully imported VM") env[:machine].id = sid @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/is_windows.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class IsWindows def initialize(app, env) @app = app end def call(env) env[:result] = env[:machine].config.vm.guest == :windows @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/message_will_not_destroy.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class MessageWillNotDestroy def initialize(app, env) @app = app end def call(env) env[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", name: env[:machine].name) @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/net_set_mac.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class NetSetMac def initialize(app, env) @app = app end def call(env) mac = env[:machine].provider_config.mac if mac env[:ui].info("[Settings] [Network Adapter] Setting MAC address to: #{mac}") env[:machine].provider.driver.net_set_mac(mac) end @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/net_set_vlan.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class NetSetVLan def initialize(app, env) @app = app end def call(env) vlan_id = env[:machine].provider_config.vlan_id if vlan_id env[:ui].info("[Settings] [Network Adapter] Setting Vlan ID to: #{vlan_id}") env[:machine].provider.driver.net_set_vlan(vlan_id) end @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/package.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../lib/vagrant/action/general/package" module VagrantPlugins module HyperV module Action class Package < Vagrant::Action::General::Package # Doing this so that we can test that the parent is properly # called in the unit tests. alias_method :general_call, :call def call(env) general_call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/package_metadata_json.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" #require 'vagrant/util/template_renderer' module VagrantPlugins module HyperV module Action class PackageMetadataJson # For TemplateRenderer include Vagrant::Util def initialize(app, env) @app = app end def call(env) @env = env create_metadata @app.call(env) end # This method creates a metadata.json file to tell vagrant this is a # Hyper V box def create_metadata File.open(File.join(@env["export.temp_dir"], "metadata.json"), "w") do |f| f.write(JSON.generate({ provider: "hyperv" })) end end end end end end ================================================ FILE: plugins/providers/hyperv/action/package_setup_files.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../lib/vagrant/action/general/package_setup_files" module VagrantPlugins module HyperV module Action class PackageSetupFiles < Vagrant::Action::General::PackageSetupFiles # Doing this so that we can test that the parent is properly # called in the unit tests. alias_method :general_call, :call def call(env) general_call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/package_setup_folders.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require_relative "../../../../lib/vagrant/action/general/package_setup_folders" module VagrantPlugins module HyperV module Action class PackageSetupFolders < Vagrant::Action::General::PackageSetupFolders # Doing this so that we can test that the parent is properly # called in the unit tests. alias_method :general_call, :call def call(env) general_call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/package_vagrantfile.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/template_renderer' module VagrantPlugins module HyperV module Action class PackageVagrantfile # For TemplateRenderer include Vagrant::Util def initialize(app, env) @app = app end def call(env) @env = env create_vagrantfile @app.call(env) end # This method creates the auto-generated Vagrantfile at the root of the # box. This Vagrantfile contains the MAC address so that the user doesn't # have to worry about it. def create_vagrantfile File.open(File.join(@env["export.temp_dir"], "Vagrantfile"), "w") do |f| mac_address = @env[:machine].provider.driver.read_mac_address f.write(TemplateRenderer.render("package_Vagrantfile", { base_mac: mac_address["mac"] })) end end end end end end ================================================ FILE: plugins/providers/hyperv/action/read_guest_ip.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "timeout" module VagrantPlugins module HyperV module Action # This action reads the SSH info for the machine and puts it into the # `:machine_ssh_info` key in the environment. class ReadGuestIP def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::hyperv::connection") end def call(env) env[:machine_ssh_info] = read_host_ip(env) @app.call(env) end def read_host_ip(env) return nil if env[:machine].id.nil? # Get Network details from WMI Provider # Wait for 120 sec By then the machine should be ready host_ip = nil begin Timeout.timeout(120) do begin network_info = env[:machine].provider.driver.read_guest_ip host_ip = network_info["ip"] sleep 10 if host_ip.empty? end while host_ip.empty? end rescue Timeout::Error @logger.info("Cannot find the IP address of the virtual machine") end return { host: host_ip } unless host_ip.nil? end end end end end ================================================ FILE: plugins/providers/hyperv/action/read_state.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module HyperV module Action class ReadState def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::hyperv::connection") end def call(env) if env[:machine].id response = env[:machine].provider.driver.get_current_state env[:machine_state_id] = response["state"].downcase.to_sym # If the machine isn't created, then our ID is stale, so just # mark it as not created. if env[:machine_state_id] == :not_created env[:machine].id = nil end else env[:machine_state_id] = :not_created end @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/resume_vm.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class ResumeVM def initialize(app, env) @app = app end def call(env) env[:ui].info("Resuming the machine...") env[:machine].provider.driver.resume @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/set_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module HyperV module Action class SetName def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::hyperv::set_name") end def call(env) name = env[:machine].provider_config.vmname # If we already set the name before, then don't do anything sentinel = env[:machine].data_dir.join("action_set_name") if !name && sentinel.file? @logger.info("Default name was already set before, not doing it again.") return @app.call(env) end # If no name was manually set, then use a default if !name prefix = "#{env[:root_path].basename.to_s}_#{env[:machine].name}" prefix.gsub!(/[^-a-z0-9_]/i, "") # milliseconds + random number suffix to allow for simultaneous # `vagrant up` of the same box in different dirs name = prefix + "_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" end env[:machine].provider.driver.set_name(name) # Create the sentinel sentinel.open("w") do |f| f.write(Time.now.to_i.to_s) end @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/snapshot_delete.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV 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[:snapshot_name]) env[:ui].success(I18n.t( "vagrant.actions.vm.snapshot.deleted", name: env[:snapshot_name])) @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/snapshot_restore.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV 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[:snapshot_name]) @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/snapshot_save.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV 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[:snapshot_name]) env[:ui].success(I18n.t( "vagrant.actions.vm.snapshot.saved", name: env[:snapshot_name])) @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/start_instance.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class StartInstance def initialize(app, env) @app = app end def call(env) env[:ui].output('Starting the machine...') env[:machine].provider.driver.start @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/stop_instance.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class StopInstance def initialize(app, env) @app = app end def call(env) env[:ui].info("Stopping the machine...") env[:machine].provider.driver.stop @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/suspend_vm.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Action class SuspendVM def initialize(app, env) @app = app end def call(env) env[:ui].info("Suspending the machine...") env[:machine].provider.driver.suspend @app.call(env) end end end end end ================================================ FILE: plugins/providers/hyperv/action/wait_for_ip_address.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ipaddr" require "timeout" module VagrantPlugins module HyperV module Action class WaitForIPAddress def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::hyperv::wait_for_ip_addr") end def call(env) timeout = env[:machine].provider_config.ip_address_timeout env[:ui].output("Waiting for the machine to report its IP address...") env[:ui].detail("Timeout: #{timeout} seconds") guest_ip = nil Timeout.timeout(timeout) do while true # If a ctrl-c came through, break out return if env[:interrupted] # Try to get the IP begin network_info = env[:machine].provider.driver.read_guest_ip guest_ip = network_info["ip"] if guest_ip begin IPAddr.new(guest_ip) break rescue IPAddr::InvalidAddressError # Ignore, continue looking. @logger.warn("Invalid IP address returned: #{guest_ip}") end end rescue Errors::PowerShellError # Ignore, continue looking. @logger.warn("Failed to read guest IP.") end sleep 1 end end # If we were interrupted then return now return if env[:interrupted] env[:ui].detail("IP: #{guest_ip}") @app.call(env) rescue Timeout::Error raise Errors::IPAddrTimeout end end end end end ================================================ FILE: plugins/providers/hyperv/action.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "vagrant/action/builder" module VagrantPlugins module HyperV module Action # Include the built-in modules so we can use them as top-level things. include Vagrant::Action::Builtin def self.action_reload Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("vagrant_hyperv.message_not_created") next end b2.use action_halt b2.use action_start end end end def self.action_destroy Vagrant::Action::Builder.new.tap do |b| b.use Call, IsState, :not_created do |env1, b1| if env1[:result] b1.use Message, I18n.t("vagrant_hyperv.message_not_created") next end b1.use Call, DestroyConfirm do |env2, b2| if !env2[:result] b2.use MessageWillNotDestroy next end b2.use ConfigValidate b2.use ProvisionerCleanup, :before b2.use StopInstance b2.use DeleteVM b2.use SyncedFolderCleanup end end end end def self.action_halt Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("vagrant_hyperv.message_not_created") next end b2.use Call, GracefulHalt, :off, :running do |env2, b3| if !env2[:result] b3.use StopInstance end end end end end # This action packages the virtual machine into a single box file. def self.action_package Vagrant::Action::Builder.new.tap do |b| b.use CheckEnabled b.use Call, IsState, :not_created do |env1, b2| if env1[:result] b2.use Message, I18n.t("vagrant_hyperv.message_not_created") next end b2.use PackageSetupFolders b2.use PackageSetupFiles b2.use action_halt b2.use SyncedFolderCleanup b2.use Package b2.use PackageVagrantfile b2.use PackageMetadataJson b2.use Export end end end def self.action_provision Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("vagrant_hyperv.message_not_created") next end b2.use Call, IsState, :running do |env1, b3| if !env1[:result] b3.use Message, I18n.t("vagrant_hyperv.message_not_running") next end b3.use Provision end end end end def self.action_resume Vagrant::Action::Builder.new.tap do |b| b.use HandleBox b.use ConfigValidate b.use Call, IsState, :not_created do |env, b1| if env[:result] b1.use Message, I18n.t("vagrant_hyperv.message_not_created") next end b1.use ResumeVM b1.use WaitForIPAddress b1.use WaitForCommunicator, [:running] end end end def self.action_start Vagrant::Action::Builder.new.tap do |b| b.use Call, IsState, :running do |env1, b1| if env1[:result] b1.use action_provision next end b1.use Call, IsState, :paused do |env2, b2| if env2[:result] b2.use action_resume next end b2.use Call, IsState, :saved do |env3, b3| # When state is `:saved` it is a snapshot being restored if !env3[:result] b3.use Provision b3.use Configure b3.use SetName b3.use NetSetVLan b3.use NetSetMac end b3.use CloudInitSetup b3.use CleanupDisks b3.use Disk b3.use SyncedFolderCleanup b3.use StartInstance b3.use WaitForIPAddress b3.use WaitForCommunicator, [:running] b3.use CloudInitWait b3.use SyncedFolders b3.use SetHostname end end end end end def self.action_up Vagrant::Action::Builder.new.tap do |b| b.use CheckEnabled b.use CheckAccess b.use HandleBox b.use ConfigValidate b.use Call, IsState, :not_created do |env1, b1| if env1[:result] b1.use Import end b1.use action_start end end end def self.action_read_state Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use ReadState end end def self.action_ssh Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] raise Vagrant::Errors::VMNotCreatedError end b2.use Call, IsState, :running do |env1, b3| if !env1[:result] raise Vagrant::Errors::VMNotRunningError end b3.use SSHExec end end end end def self.action_ssh_run Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] raise Vagrant::Errors::VMNotCreatedError end b2.use Call, IsState, :running do |env1, b3| if !env1[:result] raise Vagrant::Errors::VMNotRunningError end b3.use SSHRun end end end end def self.action_suspend Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("vagrant_hyperv.message_not_created") next end b2.use SuspendVM end end end def self.action_snapshot_delete Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("vagrant_hyperv.message_not_created") next end b2.use SnapshotDelete end end end def self.action_snapshot_restore Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("vagrant_hyperv.message_not_created") next end 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 end def self.action_snapshot_save Vagrant::Action::Builder.new.tap do |b| b.use ConfigValidate b.use Call, IsState, :not_created do |env, b2| if env[:result] b2.use Message, I18n.t("vagrant_hyperv.message_not_created") next end b2.use SnapshotSave end end end # The autoload farm action_root = Pathname.new(File.expand_path("../action", __FILE__)) autoload :PackageSetupFolders, action_root.join("package_setup_folders") autoload :PackageSetupFiles, action_root.join("package_setup_files") autoload :PackageVagrantfile, action_root.join("package_vagrantfile") autoload :PackageMetadataJson, action_root.join("package_metadata_json") autoload :Export, action_root.join("export") autoload :CheckEnabled, action_root.join("check_enabled") autoload :CheckAccess, action_root.join("check_access") autoload :Configure, action_root.join("configure") autoload :DeleteVM, action_root.join("delete_vm") autoload :Import, action_root.join("import") autoload :Package, action_root.join("package") autoload :IsWindows, action_root.join("is_windows") autoload :ReadState, action_root.join("read_state") autoload :ResumeVM, action_root.join("resume_vm") autoload :StartInstance, action_root.join('start_instance') autoload :StopInstance, action_root.join('stop_instance') autoload :SuspendVM, action_root.join("suspend_vm") autoload :WaitForIPAddress, action_root.join("wait_for_ip_address") autoload :NetSetVLan, action_root.join("net_set_vlan") autoload :NetSetMac, action_root.join("net_set_mac") autoload :MessageWillNotDestroy, action_root.join("message_will_not_destroy") autoload :SnapshotDelete, action_root.join("snapshot_delete") autoload :SnapshotRestore, action_root.join("snapshot_restore") autoload :SnapshotSave, action_root.join("snapshot_save") autoload :SetName, action_root.join("set_name") end end end ================================================ FILE: plugins/providers/hyperv/cap/cleanup_disks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/util/experimental" module VagrantPlugins module HyperV module Cap module CleanupDisks LOGGER = Log4r::Logger.new("vagrant::plugins::hyperv::cleanup_disks") # @param [Vagrant::Machine] machine # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks # @param [Hash] disk_meta_file - A hash of all the previously defined disks from the last configure_disk action def self.cleanup_disks(machine, defined_disks, disk_meta_file) return if disk_meta_file.values.flatten.empty? handle_cleanup_disk(machine, defined_disks, disk_meta_file["disk"]) handle_cleanup_dvd(machine, defined_disks, disk_meta_file["dvd"]) # TODO: Floppy disks end protected # @param [Vagrant::Machine] machine # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks # @param [Hash] disk_meta - A hash of all the previously defined disks from the last configure_disk action def self.handle_cleanup_disk(machine, defined_disks, disk_meta) all_disks = machine.provider.driver.list_hdds disk_meta.each do |d| # look at Path instead of Name or UUID disk_name = File.basename(d["Path"], '.*') dsk = defined_disks.select { |dk| dk.name == disk_name } if !dsk.empty? || d["primary"] == true next else LOGGER.warn("Found disk not in Vagrantfile config: '#{d["Name"]}'. Removing disk from guest #{machine.name}") machine.ui.warn(I18n.t("vagrant.cap.cleanup_disks.disk_cleanup", name: d["Name"]), prefix: true) disk_actual = all_disks.select { |a| File.realdirpath(a["Path"]) == File.realdirpath(d["Path"]) }.first if !disk_actual machine.ui.warn(I18n.t("vagrant.cap.cleanup_disks.disk_not_found", name: d["Name"]), prefix: true) else machine.provider.driver.remove_disk(disk_actual["ControllerType"], disk_actual["ControllerNumber"], disk_actual["ControllerLocation"], disk_actual["Path"]) end end end end def self.handle_cleanup_dvd(machine, defined_disks, disk_meta) # Get a list of all attached DVD drives attached = machine.provider.driver.read_scsi_controllers.map do |controller| controller["Drives"].map do |drive| drive if drive["DvdMediaType"].to_i == 1 end.compact end.flatten.compact # Generate list of dvd disks that previously # existed but are no longer defined orphan_attachments = disk_meta.find_all do |mdisk| defined_disks.none? do |defined_disk| defined_disk.type == :dvd && File.expand_path(mdisk["Path"]) == File.expand_path(defined_disk.file) end end # Remove any entries that are not currently # attached orphan_attachments.delete_if do |mdisk| attached.any? do |attachment| File.expand_path(attachment["Path"]) == mdisk["Path"] end end # Now remove any orphan attachments that remain orphan_attachments.each do |mdisk| LOGGER.debug("removing dvd attachment: #{mdisk}") machine.provider.driver.detach_dvd( mdisk["ControllerLocation"], mdisk["ControllerNumber"] ) end end end end end end ================================================ FILE: plugins/providers/hyperv/cap/configure_disks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "fileutils" require "vagrant/util/numeric" require "vagrant/util/experimental" module VagrantPlugins module HyperV module Cap module ConfigureDisks LOGGER = Log4r::Logger.new("vagrant::plugins::hyperv::configure_disks") # @param [Vagrant::Machine] machine # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks # @return [Hash] configured_disks - A hash of all the current configured disks def self.configure_disks(machine, defined_disks) return {} if defined_disks.empty? machine.ui.info(I18n.t("vagrant.cap.configure_disks.start")) current_disks = machine.provider.driver.list_hdds configured_disks = {disk: [], floppy: [], dvd: []} defined_disks.each do |disk| if disk.type == :disk disk_data = handle_configure_disk(machine, disk, current_disks) configured_disks[:disk] << disk_data if !disk_data.empty? elsif disk.type == :floppy # TODO: Write me machine.ui.info(I18n.t("vagrant.cap.configure_disks.floppy_not_supported", name: disk.name)) elsif disk.type == :dvd disk_data = handle_configure_dvd(machine, disk) configured_disks[:dvd] << disk_data if !disk_data.empty? end end configured_disks end protected # @param [Vagrant::Machine] machine - the current machine # @param [Config::Disk] disk - the current disk to configure # @param [Array] all_disks - A list of all currently defined disks in VirtualBox # @return [Hash] current_disk - Returns the current disk. Returns nil if it doesn't exist def self.get_current_disk(machine, disk, all_disks) current_disk = nil if disk.primary # Ensure we grab the proper primary disk # We can't rely on the order of `all_disks`, as they will not # always come in port order, but primary should always be the # first disk. sorted_disks = all_disks.sort_by { |d| [d["ControllerNumber"].to_i, d["ControllerLocation"].to_i] } LOGGER.debug("sorted disks for primary detection: #{sorted_disks}") current_disk = sorted_disks.first # Need to get actual disk info to obtain UUID instead of what's returned # # This is not required for newly created disks, as its metadata is # set when creating and attaching the disk. This is only for the primary # disk, since it already exists. current_disk = machine.provider.driver.get_disk(current_disk["Path"]) else # Hyper-V disk names aren't the actual names of the disk, so we have # to grab the name from the file path instead current_disk = all_disks.detect { |d| File.basename(d["Path"], '.*') == disk.name} end current_disk end # Handles all disk configs of type `:disk` # # @param [Vagrant::Machine] machine - the current machine # @param [Config::Disk] disk - the current disk to configure # @param [Array] all_disks - A list of all currently defined disks in VirtualBox # @return [Hash] - disk_metadata def self.handle_configure_disk(machine, disk, all_disks) disk_metadata = {} # Grab the existing configured disk, if it exists current_disk = get_current_disk(machine, disk, all_disks) # Configure current disk if !current_disk # create new disk and attach disk_metadata = create_disk(machine, disk) elsif compare_disk_size(machine, disk, current_disk) disk_metadata = resize_disk(machine, disk, current_disk) else disk_metadata = {UUID: current_disk["DiskIdentifier"], Name: disk.name, Path: current_disk["Path"]} if disk.primary disk_metadata[:primary] = true end end disk_metadata end def self.handle_configure_dvd(machine, dvd) dvd_location = File.expand_path(dvd.file) find_dvd_disk = proc { catch(:found) do machine.provider.driver.read_scsi_controllers.each do |controller| controller["Drives"].each do |disk| throw :found, disk if File.expand_path(disk["Path"]) == dvd_location end end nil end } generate_dvd_metadata = proc { |disk| disk.slice( "Name", "Id", "Path", "ControllerLocation", "ControllerNumber", "ControllerType" ) } # If the disk is already attached just # return the information if dvd_attached = find_dvd_disk.call return generate_dvd_metadata.call(dvd_attached) end # Attach the disk machine.provider.driver.attach_dvd(dvd_location) # Find disk and return information disk = find_dvd_disk.call return generate_dvd_metadata.call(disk) if disk LOGGER.warn("failed to locate dvd on controller, stubbing") {"Path" => dvd_location} end # Check to see if current disk is configured based on defined_disks # # @param [Kernel_V2::VagrantConfigDisk] disk_config # @param [Hash] defined_disk # @return [Boolean] def self.compare_disk_size(machine, disk_config, defined_disk) # Hyper-V returns disk size in bytes requested_disk_size = disk_config.size disk_actual = machine.provider.driver.get_disk(defined_disk["Path"]) defined_disk_size = disk_actual["Size"] if defined_disk_size > requested_disk_size if File.extname(disk_actual["Path"]) == ".vhdx" # VHDX formats can be shrunk return true else machine.ui.warn(I18n.t("vagrant.cap.configure_disks.shrink_size_not_supported", name: disk_config.name)) return false end elsif defined_disk_size < requested_disk_size return true else return false end end # Creates and attaches a disk to a machine # # @param [Vagrant::Machine] machine # @param [Kernel_V2::VagrantConfigDisk] disk_config def self.create_disk(machine, disk_config) machine.ui.detail(I18n.t("vagrant.cap.configure_disks.create_disk", name: disk_config.name)) disk_provider_config = {} if disk_config.provider_config && disk_config.provider_config.key?(:hyperv) disk_provider_config = disk_config.provider_config[:hyperv] end if !disk_provider_config.empty? disk_provider_config = convert_size_vars!(disk_provider_config) end # Get the machines data dir, that will now be the path for the new disk guest_disk_folder = machine.data_dir.join("Virtual Hard Disks") if disk_config.file disk_file = disk_config.file LOGGER.info("Disk already defined by user at '#{disk_file}'. Using this disk instead of creating a new one...") else # Set the extension disk_ext = disk_config.disk_ext disk_file = File.join(guest_disk_folder, disk_config.name) + ".#{disk_ext}" LOGGER.info("Attempting to create a new disk file '#{disk_file}' of size '#{disk_config.size}' bytes") machine.provider.driver.create_disk(disk_file, disk_config.size, **disk_provider_config) end disk_info = machine.provider.driver.get_disk(disk_file) disk_metadata = {UUID: disk_info["DiskIdentifier"], Name: disk_config.name, Path: disk_info["Path"]} machine.provider.driver.attach_disk(disk_file, **disk_provider_config) disk_metadata end # Converts any "shortcut" options such as "123MB" into its byte form. This # is due to what parameter type is expected when calling the `New-VHD` # powershell command # # @param [Hash] disk_provider_config # @return [Hash] disk_provider_config def self.convert_size_vars!(disk_provider_config) if disk_provider_config.key?(:BlockSizeBytes) bytes = Vagrant::Util::Numeric.string_to_bytes(disk_provider_config[:BlockSizeBytes]) disk_provider_config[:BlockSizeBytes] = bytes end disk_provider_config end # @param [Vagrant::Machine] machine # @param [Config::Disk] disk_config - the current disk to configure # @param [Hash] defined_disk - current disk as represented by VirtualBox # @return [Hash] - disk_metadata def self.resize_disk(machine, disk_config, defined_disk) machine.ui.detail(I18n.t("vagrant.cap.configure_disks.resize_disk", name: disk_config.name), prefix: true) machine.provider.driver.resize_disk(defined_disk["Path"], disk_config.size.to_i) disk_info = machine.provider.driver.get_disk(defined_disk["Path"]) # Store updated metadata disk_metadata = {UUID: disk_info["DiskIdentifier"], Name: disk_config.name, Path: disk_info["Path"]} if disk_config.primary disk_metadata[:primary] = true end disk_metadata end end end end end ================================================ FILE: plugins/providers/hyperv/cap/public_address.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Cap module PublicAddress def self.public_address(machine) return nil if machine.state.id != :running ssh_info = machine.ssh_info return nil if !ssh_info ssh_info[:host] end end end end end ================================================ FILE: plugins/providers/hyperv/cap/snapshot_list.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Cap module SnapshotList def self.snapshot_list(machine) machine.provider.driver.list_snapshots end end end end end ================================================ FILE: plugins/providers/hyperv/cap/validate_disk_ext.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module HyperV module Cap module ValidateDiskExt LOGGER = Log4r::Logger.new("vagrant::plugins::hyperv::validate_disk_ext") # The default set of disk formats that Hyper-V supports DEFAULT_DISK_EXT_LIST = ["vhd", "vhdx"].map(&:freeze).freeze DEFAULT_DISK_EXT = "vhdx".freeze # @param [Vagrant::Machine] machine # @param [String] disk_ext # @return [Bool] def self.validate_disk_ext(machine, disk_ext) DEFAULT_DISK_EXT_LIST.include?(disk_ext) end # @param [Vagrant::Machine] machine # @return [Array] def self.default_disk_exts(machine) DEFAULT_DISK_EXT_LIST end # @param [Vagrant::Machine] machine # @return [String] def self.set_default_disk_ext(machine) DEFAULT_DISK_EXT end end end end end ================================================ FILE: plugins/providers/hyperv/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HyperV class Config < Vagrant.plugin("2", :config) # Allowed automatic start actions for VM ALLOWED_AUTO_START_ACTIONS = [ "Nothing".freeze, "StartIfRunning".freeze, "Start".freeze ].freeze # Allowed automatic stop actions for VM ALLOWED_AUTO_STOP_ACTIONS = [ "ShutDown".freeze, "TurnOff".freeze, "Save".freeze ].freeze # @return [Integer] Seconds to wait for an IP address when booting attr_accessor :ip_address_timeout # @return [Integer] Memory size in MB attr_accessor :memory # @return [Integer] Maximum memory size in MB. Enables dynamic memory. attr_accessor :maxmemory # @return [Integer] Number of CPUs attr_accessor :cpus # @return [String] Name of the VM (Shown in the Hyper-V Manager) attr_accessor :vmname # @return [Integer] VLAN ID for network interface attr_accessor :vlan_id # @return [String] MAC address for network interface attr_accessor :mac # @return [Boolean] Create linked clone instead of full clone # @note **DEPRECATED** use #linked_clone instead attr_accessor :differencing_disk # @return [Boolean] Create linked clone instead of full clone attr_accessor :linked_clone # @return [String] Automatic action on start of host. Default: Nothing (Nothing, StartIfRunning, Start) attr_accessor :auto_start_action # @return [String] Automatic action on stop of host. Default: ShutDown (ShutDown, TurnOff, Save) attr_accessor :auto_stop_action # @return [Boolean] Enable checkpoints. Default: true attr_accessor :enable_checkpoints # @return [Boolean] Enable automatic checkpoints. Default: false attr_accessor :enable_automatic_checkpoints # @return [Boolean] Enable virtualization extensions attr_accessor :enable_virtualization_extensions # @return [Hash] Options for VMServiceIntegration attr_accessor :vm_integration_services # @return [Boolean] Enable Enhanced session mode attr_accessor :enable_enhanced_session_mode def initialize @ip_address_timeout = UNSET_VALUE @memory = UNSET_VALUE @maxmemory = UNSET_VALUE @cpus = UNSET_VALUE @vmname = UNSET_VALUE @vlan_id = UNSET_VALUE @mac = UNSET_VALUE @linked_clone = UNSET_VALUE @differencing_disk = UNSET_VALUE @auto_start_action = UNSET_VALUE @auto_stop_action = UNSET_VALUE @enable_virtualization_extensions = UNSET_VALUE @enable_automatic_checkpoints = UNSET_VALUE @enable_checkpoints = UNSET_VALUE @vm_integration_services = {} @enable_enhanced_session_mode = UNSET_VALUE end def finalize! if @differencing_disk != UNSET_VALUE @_differencing_disk_deprecation = true end @linked_clone = false if @linked_clone == UNSET_VALUE @differencing_disk = false if @differencing_disk == UNSET_VALUE @linked_clone ||= @differencing_disk @differencing_disk ||= @linked_clone if @ip_address_timeout == UNSET_VALUE @ip_address_timeout = 120 end @memory = nil if @memory == UNSET_VALUE @maxmemory = nil if @maxmemory == UNSET_VALUE @cpus = nil if @cpus == UNSET_VALUE @vmname = nil if @vmname == UNSET_VALUE @vlan_id = nil if @vlan_id == UNSET_VALUE @mac = nil if @mac == UNSET_VALUE @auto_start_action = "Nothing" if @auto_start_action == UNSET_VALUE @auto_stop_action = "ShutDown" if @auto_stop_action == UNSET_VALUE @enable_virtualization_extensions = false if @enable_virtualization_extensions == UNSET_VALUE if @enable_automatic_checkpoints == UNSET_VALUE @enable_automatic_checkpoints = false else @enable_automatic_checkpoints = !!@enable_automatic_checkpoints end if @enable_checkpoints == UNSET_VALUE @enable_checkpoints = true else @enable_checkpoints = !!@enable_checkpoints end # If automatic checkpoints are enabled, checkpoints will automatically be enabled @enable_checkpoints ||= @enable_automatic_checkpoints @enable_enhanced_session_mode = false if @enable_enhanced_session_mode == UNSET_VALUE end def validate(machine) errors = _detected_errors if @_differencing_disk_deprecation && machine machine.ui.warn I18n.t("vagrant_hyperv.config.differencing_disk_deprecation") end if !vm_integration_services.is_a?(Hash) errors << I18n.t("vagrant_hyperv.config.invalid_integration_services_type", received: vm_integration_services.class) else vm_integration_services.each do |key, value| if ![true, false].include?(value) errors << I18n.t("vagrant_hyperv.config.invalid_integration_services_entry", entry_name: name, entry_value: value) end end end if !ALLOWED_AUTO_START_ACTIONS.include?(auto_start_action) errors << I18n.t("vagrant_hyperv.config.invalid_auto_start_action", action: auto_start_action, allowed_actions: ALLOWED_AUTO_START_ACTIONS.join(", ")) end if !ALLOWED_AUTO_STOP_ACTIONS.include?(auto_stop_action) errors << I18n.t("vagrant_hyperv.config.invalid_auto_stop_action", action: auto_stop_action, allowed_actions: ALLOWED_AUTO_STOP_ACTIONS.join(", ")) end {"Hyper-V" => errors} end end end end ================================================ FILE: plugins/providers/hyperv/driver.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" require "log4r" require "vagrant/util/powershell" require_relative "plugin" module VagrantPlugins module HyperV class Driver ERROR_REGEXP = /===Begin-Error===(.+?)===End-Error===/m OUTPUT_REGEXP = /===Begin-Output===(.+?)===End-Output===/m # Name mapping for integration services for id # https://social.technet.microsoft.com/Forums/de-DE/154917de-f3ca-4b1e-b3f8-23dd4b4f0f06/getvmintegrationservice-sprachabhngig?forum=powershell_de INTEGRATION_SERVICES_MAP = { guest_service_interface: "6C09BB55-D683-4DA0-8931-C9BF705F6480".freeze, heartbeat: "84EAAE65-2F2E-45F5-9BB5-0E857DC8EB47".freeze, key_value_pair_exchange: "2A34B1C2-FD73-4043-8A5B-DD2159BC743F".freeze, shutdown: "9F8233AC-BE49-4C79-8EE3-E7E1985B2077".freeze, time_synchronization: "2497F4DE-E9FA-4204-80E4-4B75C46419C0".freeze, vss: "5CED1297-4598-4915-A5FC-AD21BB4D02A4".freeze, }.freeze # @return [String] VM ID attr_reader :vm_id def initialize(id) @vm_id = id @logger = Log4r::Logger.new("vagrant::hyperv::driver") end # @return [Boolean] Supports VMCX def has_vmcx_support? !!execute(:has_vmcx_support)["result"] end # Execute a PowerShell command and process the results # # @param [String] path Path to PowerShell script # @param [Hash] options Options to pass to command # # @return [Object, nil] If the command returned JSON content # it will be parsed and returned, otherwise # nil will be returned def execute(path, options={}) if path.is_a?(Symbol) path = "#{path}.ps1" end r = execute_powershell(path, options) # We only want unix-style line endings within Vagrant r.stdout.gsub!("\r\n", "\n") r.stderr.gsub!("\r\n", "\n") error_match = ERROR_REGEXP.match(r.stdout) output_match = OUTPUT_REGEXP.match(r.stdout) if error_match data = JSON.parse(error_match[1]) # We have some error data. raise Errors::PowerShellError, script: path, stderr: data["error"] end if r.exit_code != 0 raise Errors::PowerShellError, script: path, stderr: r.stderr end # Nothing return nil if !output_match return JSON.parse(output_match[1]) end # Fetch current state of the VM # # @return [Hash] def get_current_state execute(:get_vm_status, VmId: vm_id) end # Delete the VM # # @return [nil] def delete_vm execute(:delete_vm, VmId: vm_id) end # Export the VM to the given path # # @param [String] path Path for export # @return [nil] def export(path) execute(:export_vm, VmId: vm_id, Path: path) end # Get the IP address of the VM # # @return [Hash] def read_guest_ip execute(:get_network_config, VmId: vm_id) end # Get the MAC address of the VM # # @return [Hash] def read_mac_address execute(:get_network_mac, VmId: vm_id) end # Resume the VM from suspension # # @return [nil] def resume execute(:resume_vm, VmId: vm_id) end # Start the VM # # @return [nil] def start execute(:start_vm, VmId: vm_id ) end # Stop the VM # # @return [nil] def stop execute(:stop_vm, VmId: vm_id) end # Suspend the VM # # @return [nil] def suspend execute(:suspend_vm, VmId: vm_id) end # Import a new VM # # @param [Hash] options Configuration options # @return [Hash] New VM ID def import(options) execute(:import_vm, options) end # Set the VLAN ID # # @param [String] vlan_id VLAN ID # @return [nil] def net_set_vlan(vlan_id) execute(:set_network_vlan, VmId: vm_id, VlanId: vlan_id) end # Set the VM adapter MAC address # # @param [String] mac_addr MAC address # @return [nil] def net_set_mac(mac_addr) execute(:set_network_mac, VmId: vm_id, Mac: mac_addr) end # Create a new snapshot with the given name # # @param [String] snapshot_name Name of the new snapshot # @return [nil] def create_snapshot(snapshot_name) execute(:create_snapshot, VmId: vm_id, SnapName: snapshot_name) end # Restore the given snapshot # # @param [String] snapshot_name Name of snapshot to restore # @return [nil] def restore_snapshot(snapshot_name) execute(:restore_snapshot, VmId: vm_id, SnapName: snapshot_name) end # Get list of current snapshots # # @return [Array] snapshot names def list_snapshots snaps = execute(:list_snapshots, VmID: vm_id) snaps.map { |s| s['Name'] } end # Delete snapshot with the given name # # @param [String] snapshot_name Name of snapshot to delete # @return [nil] def delete_snapshot(snapshot_name) execute(:delete_snapshot, VmID: vm_id, SnapName: snapshot_name) end # Enable or disable VM integration services # # @param [Hash] config Integration services to enable or disable # @return [nil] # @note Keys in the config hash will be remapped if found in the # INTEGRATION_SERVICES_MAP. If they are not, the name will # be passed directly. This allows new integration services # to configurable even if Vagrant is not aware of them. def set_vm_integration_services(config) config.each_pair do |srv_name, srv_enable| args = {VMID: vm_id, Id: INTEGRATION_SERVICES_MAP.fetch(srv_name.to_sym, srv_name).to_s} args[:Enable] = true if srv_enable execute(:set_vm_integration_services, args) end end # Set the name of the VM # # @param [String] vmname Name of the VM # @return [nil] def set_name(vmname) execute(:set_name, VMID: vm_id, VMName: vmname) end # # Disk Driver methods # # @param [String] controller_type # @param [String] controller_number # @param [String] controller_location # @param [Hash] opts # @option opts [String] :ControllerType # @option opts [String] :ControllerNumber # @option opts [String] :ControllerLocation def attach_disk(disk_file_path, **opts) execute(:attach_disk_drive, VmId: @vm_id, Path: disk_file_path, ControllerType: opts[:ControllerType], ControllerNumber: opts[:ControllerNumber], ControllerLocation: opts[:ControllerLocation]) end # @param [String] path # @param [Int] size_bytes # @param [Hash] opts # @option opts [Bool] :Fixed # @option opts [String] :BlockSizeBytes # @option opts [String] :LogicalSectorSizeBytes # @option opts [String] :PhysicalSectorSizeBytes # @option opts [String] :SourceDisk # @option opts [Bool] :Differencing # @option opts [String] :ParentPath def create_disk(path, size_bytes, **opts) execute(:new_vhd, Path: path, SizeBytes: size_bytes, Fixed: opts[:Fixed], BlockSizeBytes: opts[:BlockSizeBytes], LogicalSectorSizeBytes: opts[:LogicalSectorSizeBytes], PhysicalSectorSizeBytes: opts[:PhysicalSectorSizeBytes], SourceDisk: opts[:SourceDisk], Differencing: opts[:Differencing], ParentPath: opts[:ParentPath]) end # @param [String] disk_file_path def dismount_disk(disk_file_path) execute(:dismount_vhd, DiskFilePath: disk_file_path) end # @param [String] disk_file_path def get_disk(disk_file_path) execute(:get_vhd, DiskFilePath: disk_file_path) end # Add a DVD drive to VM # # @param [String] iso_path # @return [nil] def attach_dvd(iso_path) execute(:add_dvd, VmId: vm_id, ISOPath: iso_path) end # Remove a DVD drive from VM # # @param [Integer] controller_location # @param [Integer] controller_number # @return [nil] def detach_dvd(controller_location, controller_number) execute(:remove_dvd, VmId: vm_id, ControllerLocation: controller_location, ControllerNumber: controller_number ) end # @return [Array[Hash]] def list_hdds execute(:list_hdds, VmId: @vm_id) end # @param [String] controller_type # @param [String] controller_number # @param [String] controller_location # @param [String] disk_file_path # @param [Hash] opts # @option opts [String] :ControllerType # @option opts [String] :ControllerNumber # @option opts [String] :ControllerLocation def remove_disk(controller_type, controller_number, controller_location, disk_file_path, **opts) execute(:remove_disk_drive, VmId: @vm_id, ControllerType: controller_type, ControllerNumber: controller_number, ControllerLocation: controller_location, DiskFilePath: disk_file_path) end # @param [String] path # @param [Int] size_bytes # @param [Hash] opts def resize_disk(disk_file_path, size_bytes, **opts) execute(:resize_disk_drive, VmId: @vm_id, DiskFilePath: disk_file_path, DiskSize: size_bytes) end # Set enhanced session transport type of the VM # # @param [String] enhanced session transport type of the VM # @return [nil] def set_enhanced_session_transport_type(transport_type) result = execute(:set_enhanced_session_transport_type, VmID: vm_id, type: transport_type) if !result.nil? @logger.debug("EnhancedSessionTransportType is not supported by this version of hyperv, ignoring") end end # Get SCSI controllers attached to VM # # @return [Array] def read_scsi_controllers result = execute(:get_scsi_controller, VmId: vm_id) return result if result.is_a?(Array) [result] end protected def execute_powershell(path, options, &block) lib_path = Pathname.new(File.expand_path("../scripts", __FILE__)) mod_path = Vagrant::Util::Platform.wsl_to_windows_path(lib_path.join("utils")).to_s.gsub("/", "\\") path = Vagrant::Util::Platform.wsl_to_windows_path(lib_path.join(path)).to_s.gsub("/", "\\") options = options || {} ps_options = [] options.each do |key, value| next if !value || value.to_s.empty? next if value == false ps_options << "-#{key}" # If the value is a TrueClass assume switch next if value == true ps_options << "'#{value}'" end # Always have a stop error action for failures ps_options << "-ErrorAction" << "Stop" # Include our module path so we can nicely load helper modules opts = { notify: [:stdout, :stderr, :stdin], module_path: mod_path } Vagrant::Util::PowerShell.execute(path, *ps_options, **opts, &block) end end end end ================================================ FILE: plugins/providers/hyperv/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV module Errors # A convenient superclass for all our errors. class HyperVError < Vagrant::Errors::VagrantError error_namespace("vagrant_hyperv.errors") end class AdminRequired < HyperVError error_key(:admin_required) end class BoxInvalid < HyperVError error_key(:box_invalid) end class IPAddrTimeout < HyperVError error_key(:ip_addr_timeout) end class NoSwitches < HyperVError error_key(:no_switches) end class PowerShellFeaturesDisabled < HyperVError error_key(:powershell_features_disabled) end class PowerShellError < HyperVError error_key(:powershell_error) end class PowerShellRequired < HyperVError error_key(:powershell_required) end class WindowsRequired < HyperVError error_key(:windows_required) end class SystemAccessRequired < HyperVError error_key(:system_access_required) end end end end ================================================ FILE: plugins/providers/hyperv/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HyperV autoload :Action, File.expand_path("../action", __FILE__) autoload :Errors, File.expand_path("../errors", __FILE__) class Plugin < Vagrant.plugin("2") name "Hyper-V provider" description <<-DESC This plugin installs a provider that allows Vagrant to manage machines in Hyper-V. DESC provider(:hyperv, priority: 4) do require_relative "provider" init! Provider end config(:hyperv, :provider) do require_relative "config" init! Config end provider_capability("hyperv", "public_address") do require_relative "cap/public_address" Cap::PublicAddress end provider_capability("hyperv", "snapshot_list") do require_relative "cap/snapshot_list" Cap::SnapshotList end provider_capability(:hyperv, :configure_disks) do require_relative "cap/configure_disks" Cap::ConfigureDisks end provider_capability(:hyperv, :cleanup_disks) do require_relative "cap/cleanup_disks" Cap::CleanupDisks end provider_capability(:hyperv, :validate_disk_ext) do require_relative "cap/validate_disk_ext" Cap::ValidateDiskExt end provider_capability(:hyperv, :default_disk_exts) do require_relative "cap/validate_disk_ext" Cap::ValidateDiskExt end provider_capability(:hyperv, :set_default_disk_ext) do require_relative "cap/validate_disk_ext" Cap::ValidateDiskExt end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path( "templates/locales/providers_hyperv.yml", Vagrant.source_root) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/providers/hyperv/provider.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require_relative "driver" require_relative "plugin" require "vagrant/util/platform" require "vagrant/util/powershell" module VagrantPlugins module HyperV class Provider < Vagrant.plugin("2", :provider) attr_reader :driver def self.usable?(raise_error=false) if !Vagrant::Util::Platform.windows? && !Vagrant::Util::Platform.wsl? raise Errors::WindowsRequired end if !Vagrant::Util::Platform.windows_admin? && !Vagrant::Util::Platform.windows_hyperv_admin? raise Errors::AdminRequired end if !Vagrant::Util::PowerShell.available? raise Errors::PowerShellRequired end true rescue Errors::HyperVError raise if raise_error return false end def initialize(machine) @machine = machine # This method will load in our driver, so we call it now to # initialize it. machine_id_changed @logger = Log4r::Logger.new("vagrant::hyperv::provider") end def action(name) # Attempt to get the action method from the Action class if it # exists, otherwise return nil to show that we don't support the # given action. action_method = "action_#{name}" return Action.send(action_method) if Action.respond_to?(action_method) nil end def machine_id_changed @driver = Driver.new(@machine.id) end def state state_id = nil state_id = :not_created if !@machine.id if !state_id # Run a custom action we define called "read_state" which does # what it says. It puts the state in the `:machine_state_id` # key in the environment. env = @machine.action(:read_state) state_id = env[:machine_state_id] end # Get the short and long description short = state_id.to_s long = "" # If we're not created, then specify the special ID flag if state_id == :not_created state_id = Vagrant::MachineState::NOT_CREATED_ID end # Return the MachineState object Vagrant::MachineState.new(state_id, short, long) end def to_s id = @machine.id.nil? ? "new" : @machine.id "Hyper-V (#{id})" end # @return [Hash] def ssh_info # We can only SSH into a running machine return nil if state.id != :running # Read the IP of the machine using Hyper-V APIs guest_ip = nil begin network_info = @driver.read_guest_ip guest_ip = network_info["ip"] rescue Errors::PowerShellError @logger.warn("Failed to read guest IP.") end return nil if !guest_ip @logger.debug("IP: #{guest_ip}") { host: guest_ip, port: 22, } end end end end ================================================ FILE: plugins/providers/hyperv/scripts/add_dvd.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId, [Parameter(Mandatory=$true)] [string]$ISOPath ) try { Hyper-V\Get-VM -ID $VmId | Hyper-V\Add-VMDvdDrive -Path $ISOPath } catch { Write-ErrorMessage "Failed to add DVD drive for path - ${ISOPath}: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/attach_disk_drive.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId, [Parameter(Mandatory=$true)] [string]$Path, [string]$ControllerType, [string]$ControllerNumber, [string]$ControllerLocation ) $Params = @{} foreach ($key in $MyInvocation.BoundParameters.keys) { $value = (Get-Variable -Exclude "ErrorAction" $key).Value if (($key -ne "VmId") -and ($key -ne "ErrorAction")) { $Params.Add($key, $value) } } try { $VM = Hyper-V\Get-VM -Id $VmId Hyper-V\Add-VMHardDiskDrive -VMName $VM.Name @Params } catch { Write-ErrorMessage "Failed to attach disk ${DiskFilePath} to VM ${VM}: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/check_hyperv.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages $check = $(-Not (-Not (Get-Command "Hyper-V\Get-VMSwitch" -ErrorAction SilentlyContinue))) $result = @{ result = $check } Write-OutputMessage $(ConvertTo-Json $result) ================================================ FILE: plugins/providers/hyperv/scripts/check_hyperv_access.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages, VagrantVM param( [parameter (Mandatory=$true)] [string] $Path ) $check = Check-VagrantHyperVAccess -Path $Path $result = @{ root_dir = ($Path -split '\\')[0,1] -join '\'; result = $check } Write-OutputMessage $(ConvertTo-Json $result) ================================================ FILE: plugins/providers/hyperv/scripts/clone_vhd.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$Source, [Parameter(Mandatory=$true)] [string]$Destination ) $ErrorActionPreference = "Stop" try { Hyper-V\New-VHD -Path $Destination -ParentPath $Source } catch { Write-ErrorMessage "Failed to clone drive: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/configure_vm.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantVM, VagrantMessages param( [parameter (Mandatory=$true)] [Guid] $VMID, [parameter (Mandatory=$false)] [string] $SwitchID=$null, [parameter (Mandatory=$false)] [string] $Memory=$null, [parameter (Mandatory=$false)] [string] $MaxMemory=$null, [parameter (Mandatory=$false)] [string] $Processors=$null, [parameter (Mandatory=$false)] [string] $AutoStartAction=$null, [parameter (Mandatory=$false)] [string] $AutoStopAction=$null, [parameter (Mandatory=$false)] [switch] $VirtualizationExtensions, [parameter (Mandatory=$false)] [switch] $EnableCheckpoints, [parameter (Mandatory=$false)] [switch] $EnableAutomaticCheckpoints ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VMID } catch { Write-ErrorMessage "Failed to locate VM: ${PSItem}" exit 1 } if($Processors) { try { Set-VagrantVMCPUS -VM $VM -CPUCount ($Processors -as [int]) } catch { Write-ErrorMessage "Failed to configure CPUs: ${PSItem}" exit 1 } } if($Memory -or $MaxMemory) { try { Set-VagrantVMMemory -VM $VM -Memory $Memory -MaxMemory $MaxMemory } catch { Write-ErrorMessage "Failed to configure memory: ${PSItem}" exit 1 } } if($AutoStartAction -or $AutoStopAction) { try { Set-VagrantVMAutoActions -VM $VM -AutoStartAction $AutoStartAction -AutoStopAction $AutoStopAction } catch { Write-ErrorMessage "Failed to configure automatic actions: ${PSItem}" exit 1 } } if($VirtualizationExtensions) { $virtex = $true } else { $virtex = $false } try { Set-VagrantVMVirtExtensions -VM $VM -Enabled $virtex } catch { Write-ErrorMessage "Failed to configure virtualization extensions: ${PSItem}" exit 1 } if($SwitchID) { try { $SwitchName = Get-VagrantVMSwitch -NameOrID $SwitchID Set-VagrantVMSwitch -VM $VM -SwitchName $SwitchName } catch { Write-ErrorMessage "Failed to configure network adapter: ${PSItem}" exit 1 } } if($EnableCheckpoints) { $checkpoints = "Standard" $CheckpointAction = "enable" } else { $checkpoints = "Disabled" $CheckpointAction = "disable" } try { if((Get-Command Hyper-V\Set-VM).Parameters["CheckpointType"] -eq $null) { if($CheckpointAction -eq "enable") { Write-ErrorMessage "CheckpointType is not available. Cannot enable checkpoints." exit 1 } } else { Hyper-V\Set-VM -VM $VM -CheckpointType $checkpoints } } catch { Write-ErrorMessage "Failed to ${CheckpointAction} checkpoints on VM: ${PSItem}" exit 1 } if($EnableAutomaticCheckpoints) { $autochecks = 1 $AutoAction = "enabled" } else { $autochecks = 0 $AutoAction = "disable" } try { if((Get-Command Hyper-V\Set-VM).Parameters["AutomaticCheckpointsEnabled"] -eq $null) { if($autochecks -eq 1) { Write-ErrorMessage "AutomaticCheckpointsEnabled is not available" exit 1 } } else { Hyper-V\Set-VM -VM $VM -AutomaticCheckpointsEnabled $autochecks } } catch { Write-ErrorMessage "Failed to ${AutoAction} automatic checkpoints on VM: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/create_snapshot.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId, [string]$SnapName ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VmId $ChkPnt = $VM.CheckpointType if($ChkPnt -eq "Disabled") { Hyper-V\Set-VM -VM $VM -CheckpointType "Standard" } Hyper-V\Checkpoint-VM $VM -SnapshotName $SnapName if($ChkPnt -eq "Disabled") { Hyper-V\Set-VM -VM $VM -CheckpointType "Disabled" } } catch { Write-ErrorMessage "Failed to create snapshot: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/delete_snapshot.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId, [string]$SnapName ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VmId Hyper-V\Remove-VMSnapshot $VM -Name $SnapName } catch { Write-ErrorMessage "Failed to delete snapshot: ${PSItem}" } ================================================ FILE: plugins/providers/hyperv/scripts/delete_vm.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VmId if((Get-Command Hyper-V\Set-VM).Parameters["AutomaticCheckpointsEnabled"] -ne $null) { Hyper-V\Set-VM -VM $VM -AutomaticCheckpointsEnabled $false -ErrorAction SilentlyContinue } Hyper-V\Remove-VM $VM -Force } catch { Write-ErrorMessage "Failed to delete VM: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/dismount_vhd.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$DiskFilePath ) try { Hyper-V\Dismount-VHD -path $DiskFilePath } catch { Write-ErrorMessage "Failed to dismount disk info from disk file path ${DiskFilePath}: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/export_vm.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId, [Parameter(Mandatory=$true)] [string]$Path ) $ErrorActionPreference = "Stop" try { $vm = Hyper-V\Get-VM -Id $VmId $vm | Hyper-V\Export-VM -Path $Path } catch { Write-ErrorMessage "Failed to export VM: ${PSItem}" exit 1 } # Prepare directory structure for box import try { $name = $vm.Name Move-Item $Path/$name/* $Path Remove-Item -Path $Path/Snapshots -Force -Recurse Remove-Item -Path $Path/$name -Force } catch { Write-ErrorMessage "Failed to format exported box: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/file_sync.ps1 ================================================ #Requires -Modules VagrantMessages #------------------------------------------------------------------------- # Copyright (c) Microsoft Open Technologies, Inc. # All Rights Reserved. Licensed under the MIT License. #-------------------------------------------------------------------------- param ( [parameter (Mandatory=$true)] [string]$vm_id, [parameter (Mandatory=$true)] [string]$guest_ip, [parameter (Mandatory=$true)] [string]$username, [parameter (Mandatory=$true)] [string]$password, [parameter (Mandatory=$true)] [string]$host_path, [parameter (Mandatory=$true)] [string]$guest_path ) function Get-file-hash($source_path, $delimiter) { $source_files = @() (Get-ChildItem $source_path -rec | ForEach-Object -Process { Get-FileHash -Path $_.FullName -Algorithm MD5 } ) | ForEach-Object -Process { $source_files += $_.Path.Replace($source_path, "") + $delimiter + $_.Hash } $source_files } function Get-Remote-Session($guest_ip, $username, $password) { $secstr = convertto-securestring -AsPlainText -Force -String $password $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr New-PSSession -ComputerName $guest_ip -Credential $cred } function Get-remote-file-hash($source_path, $delimiter, $session) { Invoke-Command -Session $session -ScriptBlock ${function:Get-file-hash} -ArgumentList $source_path, $delimiter # TODO: # Check if remote PS Scripting errors } function Sync-Remote-Machine($machine, $remove_files, $copy_files, $host_path, $guest_path) { ForEach ($item in $copy_files) { $from = $host_path + $item $to = $guest_path + $item # Copy VM can also take a VM object Hyper-V\Copy-VMFile -VM $machine -SourcePath $from -DestinationPath $to -CreateFullPath -FileSource Host -Force } } function Create-Remote-Folders($empty_source_folders, $guest_path) { ForEach ($item in $empty_source_folders) { $new_name = $guest_path + $item New-Item "$new_name" -type directory -Force } } function Get-Empty-folders-From-Source($host_path) { Get-ChildItem $host_path -recurse | Where-Object {$_.PSIsContainer -eq $True} | Where-Object {$_.GetFiles().Count -eq 0} | Select-Object FullName | ForEach-Object -Process { $empty_source_folders += ($_.FullName.Replace($host_path, "")) } } $delimiter = " || " $machine = Hyper-V\Get-VM -Id $vm_id # FIXME: PowerShell guys please fix this. # The below script checks for all VMIntegrationService which are not enabled # and will enable this. # When when all the services are enabled this throws an error. # Enable VMIntegrationService to true try { Hyper-V\Get-VM -Id $vm_id | Hyper-V\Get-VMIntegrationService -Name "Guest Service Interface" | Hyper-V\Enable-VMIntegrationService -Passthru } catch { } $session = Get-Remote-Session $guest_ip $username $password $source_files = Get-file-hash $host_path $delimiter $destination_files = Get-remote-file-hash $guest_path $delimiter $session if (!$destination_files) { $destination_files = @() } if (!$source_files) { $source_files = @() } # Compare source and destination files $remove_files = @() $copy_files = @() Compare-Object -ReferenceObject $source_files -DifferenceObject $destination_files | ForEach-Object { if ($_.SideIndicator -eq '=>') { $remove_files += $_.InputObject.Split($delimiter)[0] } else { $copy_files += $_.InputObject.Split($delimiter)[0] } } # Update the files to remote machine Sync-Remote-Machine $machine $remove_files $copy_files $host_path $guest_path # Create any empty folders which missed to sync to remote machine $empty_source_folders = @() $directories = Get-Empty-folders-From-Source $host_path $result = Invoke-Command -Session $session -ScriptBlock ${function:Create-Remote-Folders} -ArgumentList $empty_source_folders, $guest_path # Always remove the connection after Use Remove-PSSession -Id $session.Id $resultHash = @{ message = "OK" } $result = ConvertTo-Json $resultHash Write-OutputMessage $result ================================================ FILE: plugins/providers/hyperv/scripts/get_network_config.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId ) $ErrorActionPreference = "Stop" try { $vm = Hyper-V\Get-VM -Id $VmId } catch { Write-ErrorMessage "Failed to locate VM: ${PSItem}" exit 1 } try { $netdev = Hyper-V\Get-VMNetworkAdapter -VM $vm | Select-Object -Index 0 if ($netdev -ne $null) { if ($netdev.IpAddresses.Length -gt 0) { foreach ($ip_address in $netdev.IpAddresses) { if ($ip_address.Contains(".") -And [string]::IsNullOrEmpty($ip4_address)) { $ip4_address = $ip_address } elseif ($ip_address.Contains(":") -And [string]::IsNullOrEmpty($ip6_address)) { $ip6_address = $ip_address } } } # If no address was found in the network settings, check for # neighbor with mac address and see if an IP exists if (([string]::IsNullOrEmpty($ip4_address)) -And ([string]::IsNullOrEmpty($ip6_address))) { $macaddress = $netdev.MacAddress -replace '(.{2})(?!$)', '${1}-' $addr = Get-NetNeighbor -LinkLayerAddress $macaddress -ErrorAction SilentlyContinue | select IPAddress if ($addr) { $ip_address = $addr.IPAddress if ($ip_address.Contains(".")) { $ip4_address = $ip_address } elseif ($ip_address.Contains(":")) { $ip6_address = $ip_address } } } } if (-Not ([string]::IsNullOrEmpty($ip4_address))) { $guest_ipaddress = $ip4_address } elseif (-Not ([string]::IsNullOrEmpty($ip6_address))) { $guest_ipaddress = $ip6_address } if (-Not ([string]::IsNullOrEmpty($guest_ipaddress))) { $resultHash = @{ ip = $guest_ipaddress } $result = ConvertTo-Json $resultHash Write-OutputMessage $result } else { Write-ErrorMessage "Failed to determine IP address" exit 1 } } catch { Write-ErrorMessage "Unexpected error while detecting network configuration: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/get_network_mac.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId ) $ErrorActionPreference = "Stop" try { $ip_address = "" $vm = Hyper-V\Get-VM -Id $VmId $networks = Hyper-V\Get-VMNetworkAdapter -VM $vm foreach ($network in $networks) { if ($network.MacAddress -gt 0) { $mac_address = $network.MacAddress if (-Not ([string]::IsNullOrEmpty($mac_address))) { # We found our mac address! break } } } $resultHash = @{ mac = "$mac_address" } $result = ConvertTo-Json $resultHash Write-OutputMessage $result } catch { Write-ErrorMessage "Unexpected error while fetching MAC: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/get_scsi_controller.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId ) try { $Controller = Hyper-V\Get-VM -ID $VmId | Hyper-V\Get-VMScsiController } catch { Write-ErrorMessage "Failed to retrieve scsi controller info for ${VmId}: ${PSItem}" exit 1 } $result = ConvertTo-json $Controller -Depth 20 Write-OutputMessage $result ================================================ FILE: plugins/providers/hyperv/scripts/get_switches.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages # This will have a SwitchType property. As far as I know the values are: # # 0 - Private # 1 - Internal # $Switches = @(Hyper-V\Get-VMSwitch ` | Select-Object Name,SwitchType,NetAdapterInterfaceDescription,Id) Write-OutputMessage $(ConvertTo-JSON $Switches) ================================================ FILE: plugins/providers/hyperv/scripts/get_vhd.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$DiskFilePath ) try { $Disk = Hyper-V\Get-VHD -path $DiskFilePath } catch { Write-ErrorMessage "Failed to retrieve disk info from disk file path ${DiskFilePath}: ${PSItem}" exit 1 } $result = ConvertTo-json $Disk Write-OutputMessage $result ================================================ FILE: plugins/providers/hyperv/scripts/get_vm_status.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId ) # Make sure the exception type is loaded try { # Microsoft.HyperV.PowerShell is present on all versions of Windows with HyperV [void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.HyperV.PowerShell, Culture=neutral, PublicKeyToken=31bf3856ad364e35') # Microsoft.HyperV.PowerShell.Objects is only present on Windows >= 10.0, so this will fail, and we ignore it since the needed exception # type was loaded in Microsoft.HyperV.PowerShell [void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.HyperV.PowerShell.Objects, Culture=neutral, PublicKeyToken=31bf3856ad364e35') } catch { # Empty catch ok, since if we didn't load the types, we will fail in the next block } $VmmsPath = if ([environment]::Is64BitProcess) { "$($env:SystemRoot)\System32\vmms.exe" } else { "$($env:SystemRoot)\Sysnative\vmms.exe" } $HyperVVersion = [version](Get-Item $VmmsPath).VersionInfo.ProductVersion if($HyperVVersion -lt ([version]'10.0')) { $ExceptionType = [Microsoft.HyperV.PowerShell.VirtualizationOperationFailedException] } else { $ExceptionType = [Microsoft.HyperV.PowerShell.VirtualizationException] } try { $VM = Hyper-V\Get-VM -Id $VmId -ErrorAction "Stop" $State = $VM.state $Status = $VM.status } catch [Exception] { if($_.Exception.GetType() -eq $ExceptionType) { $State = "not_created" $Status = $State } else { throw; } } $resultHash = @{ state = "$State" status = "$Status" } $result = ConvertTo-Json $resultHash Write-OutputMessage $result ================================================ FILE: plugins/providers/hyperv/scripts/has_vmcx_support.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages # Windows version 10 and up have support for binary format $check = [System.Environment]::OSVersion.Version.Major -ge 10 $result = @{ result = $check } Write-OutputMessage $(ConvertTo-Json $result) ================================================ FILE: plugins/providers/hyperv/scripts/import_vm.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantVM, VagrantMessages param( [parameter (Mandatory=$true)] [string] $VMConfigFile, [parameter (Mandatory=$true)] [string] $DestinationPath, [parameter (Mandatory=$true)] [string] $DataPath, [parameter (Mandatory=$true)] [string] $SourcePath, [parameter (Mandatory=$false)] [switch] $LinkedClone, [parameter (Mandatory=$false)] [int] $Memory = $null, [parameter (Mandatory=$false)] [int] $MaxMemory = $null, [parameter (Mandatory=$false)] [int] $Processors = $null, [parameter (Mandatory=$false)] [string] $VMName=$null ) $ErrorActionPreference = "Stop" try { if($LinkedClone) { $linked = $true } else { $linked = $false } $VM = New-VagrantVM -VMConfigFile $VMConfigFile -DestinationPath $DestinationPath ` -DataPath $DataPath -SourcePath $SourcePath -LinkedClone $linked -Memory $Memory ` -MaxMemory $MaxMemory -CPUCount $Processors -VMName $VMName $Result = @{ id = $VM.Id.Guid; } Write-OutputMessage (ConvertTo-Json $Result) } catch { Write-ErrorMessage "${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/list_hdds.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId ) try { $VM = Hyper-V\Get-VM -Id $VmId $Disks = @(Hyper-V\Get-VMHardDiskDrive -VMName $VM.Name) } catch { Write-ErrorMessage "Failed to retrieve all disk info from ${VM}: ${PSItem}" exit 1 } $result = ConvertTo-json $Disks Write-OutputMessage $result ================================================ FILE: plugins/providers/hyperv/scripts/list_snapshots.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VmId $Snapshots = @(Hyper-V\Get-VMSnapshot $VM | Select-Object Name) } catch { Write-ErrorMessage "Failed to get snapshot list: ${PSItem}" exit 1 } $result = ConvertTo-json $Snapshots Write-OutputMessage $result ================================================ FILE: plugins/providers/hyperv/scripts/new_vhd.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$Path, [Parameter(Mandatory=$true)] [UInt64]$SizeBytes, [switch]$Fixed, [switch]$Differencing, [string]$ParentPath, [Uint32]$BlockSizeBytes, [UInt32]$LogicalSectorSizeBytes, [UInt32]$PhysicalSectorSizeBytes, [UInt32]$SourceDisk ) $Params = @{} foreach ($key in $MyInvocation.BoundParameters.keys) { $value = (Get-Variable -Exclude "ErrorAction" $key).Value if ($key -ne "ErrorAction") { $Params.Add($key, $value) } } try { Hyper-V\New-VHD @Params } catch { Write-ErrorMessage "Failed to create disk ${DiskFilePath}: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/remove_disk_drive.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId, [Parameter(Mandatory=$true)] [string]$ControllerType, [Parameter(Mandatory=$true)] [string]$ControllerNumber, [Parameter(Mandatory=$true)] [string]$ControllerLocation, [Parameter(Mandatory=$true)] [string]$DiskFilePath ) try { $VM = Hyper-V\Get-VM -Id $VmId Hyper-v\Remove-VMHardDiskDrive -VMName $VM.Name -ControllerType $ControllerType -ControllerNumber $ControllerNumber -ControllerLocation $ControllerLocation Remove-Item -Path $DiskFilePath } catch { Write-ErrorMessage "Failed to remove disk ${DiskFilePath} to VM ${VM}: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/remove_dvd.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId, [Parameter(Mandatory=$true)] [Int32]$ControllerNumber, [Parameter(Mandatory=$true)] [Int32]$ControllerLocation ) try { $vm = Hyper-V\Get-VM -ID $VmId Hyper-V\Remove-VMDvdDrive -ControllerNumber $ControllerNumber -ControllerLocation $ControllerLocation -VMName $vm.Name } catch { Write-ErrorMessage "Failed to remove DVD drive (Location: '${ControllerLocation}' Number '${ControllerNumber}'): ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/resize_disk_drive.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId, [Parameter(Mandatory=$true)] [string]$DiskFilePath, [Parameter(Mandatory=$true)] [UInt64]$DiskSize ) try { $VM = Hyper-V\Get-VM -Id $VmId Hyper-V\Resize-VHD -Path $DiskFilePath -SizeBytes $DiskSize } catch { Write-ErrorMessage "Failed to resize disk ${DiskFilePath} for VM ${VM}: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/restore_snapshot.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId, [string]$SnapName ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VmId Hyper-V\Restore-VMSnapshot $VM -Name $SnapName -Confirm:$false } catch { Write-ErrorMessage "Failed to restore snapshot: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/resume_vm.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VmId Hyper-V\Resume-VM $VM } catch { Write-ErrorMessage "Failed to resume VM: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/set_enhanced_session_transport_type.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param ( [parameter (Mandatory=$true)] [Guid] $VMID, [parameter (Mandatory=$true)] [string] $Type ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VMID } catch { Write-ErrorMessage "Failed to locate VM: ${PSItem}" exit 1 } try { # HyperV 1.1 (Windows Server 2012R2) crashes on this call. Vagrantfiles before 2.2.10 do break without skipping this. $present = Get-Command Hyper-V\Set-VM -ParameterName EnhancedSessionTransportType -ErrorAction SilentlyContinue if($present) { Hyper-V\Set-VM -VM $VM -EnhancedSessionTransportType $Type }else{ $message = @{ "EnhancedSessionTransportTypeSupportPresent"=$false; } | ConvertTo-Json Write-OutputMessage $message } } catch { Write-ErrorMessage "Failed to assign EnhancedSessionTransportType to ${Type}:${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/set_name.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param ( [parameter (Mandatory=$true)] [Guid] $VMID, [parameter (Mandatory=$true)] [string] $VMName ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VMID } catch { Write-ErrorMessage "Failed to locate VM: ${PSItem}" exit 1 } try { Hyper-V\Set-VM -VM $VM -NewVMName $VMName } catch { Write-ErrorMessage "Failed to assign new VM name ${VMName}: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/set_network_mac.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param ( [parameter (Mandatory=$true)] [string]$VmId, [parameter (Mandatory=$true)] [string]$Mac ) $ErrorActionPreference = "Stop" try { $vm = Hyper-V\Get-VM -Id $VmId Hyper-V\Set-VMNetworkAdapter $vm -StaticMacAddress $Mac } catch { Write-ErrorMessage "Failed to set VM MAC address: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/set_network_vlan.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param ( [parameter (Mandatory=$true)] [string]$VmId, [parameter (Mandatory=$true)] [int]$VlanId ) try { $vm = Hyper-V\Get-VM -Id $VmId -ErrorAction "stop" Hyper-V\Set-VMNetworkAdapterVlan $vm -Access -Vlanid $VlanId } catch { Write-ErrorMessage "Failed to set VM's Vlan ID $_" } ================================================ FILE: plugins/providers/hyperv/scripts/set_vm_integration_services.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantVM, VagrantMessages param ( [parameter (Mandatory=$true)] [string] $VMID, [parameter (Mandatory=$true)] [string] $Id, [parameter (Mandatory=$false)] [switch] $Enable ) $ErrorActionPreference = "Stop" try { $VM = Hyper-V\Get-VM -Id $VMID } catch { Write-ErrorMessage "Failed to locate VM: ${PSItem}" exit 1 } try { Set-VagrantVMService -VM $VM -Id $Id -Enable $Enable } catch { if($Enable){ $action = "enable" } else { $action = "disable" } Write-ErrorMessage "Failed to ${action} VM integration service id ${Id}: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/start_vm.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param ( [parameter (Mandatory=$true)] [string]$VmId ) $ErrorActionPreference = "Stop" try { $vm = Hyper-V\Get-VM -Id $VmId Hyper-V\Start-VM $vm $state = $vm.state $status = $vm.status $name = $vm.name $resultHash = @{ state = "$state" status = "$status" name = "$name" } $result = ConvertTo-Json $resultHash Write-OutputMessage $result } catch { Write-ErrorMessage "Failed to start VM ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/stop_vm.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages Param( [Parameter(Mandatory=$true)] [string]$VmId ) $ErrorActionPreference = "Stop" try{ # Shuts down virtual machine regardless of any unsaved application data $VM = Hyper-V\Get-VM -Id $VmId Hyper-V\Stop-VM $VM -Force } catch { Write-ErrorMessage "Failed to stop VM: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/suspend_vm.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 #Requires -Modules VagrantMessages param( [Parameter(Mandatory=$true)] [string]$VmId ) $ErrorActionPreference = "Stop" try{ $VM = Hyper-V\Get-VM -Id $VmId Hyper-V\Suspend-VM $VM } catch { Write-ErrorMessage "Failed to suspend VM: ${PSItem}" exit 1 } ================================================ FILE: plugins/providers/hyperv/scripts/utils/VagrantMessages/VagrantMessages.psm1 ================================================ #------------------------------------------------------------------------- # Copyright (c) Microsoft Open Technologies, Inc. # All Rights Reserved. Licensed under the MIT License. #-------------------------------------------------------------------------- function Write-ErrorMessage { param ( [parameter (Mandatory=$true,Position=0)] [string] $Message ) $error_message = @{ error = $Message } Write-Host "===Begin-Error===" Write-Host (ConvertTo-Json $error_message) Write-Host "===End-Error===" } function Write-OutputMessage { param ( [parameter (Mandatory=$true,Position=0)] [string] $Message ) Write-Host "===Begin-Output===" Write-Host $Message Write-Host "===End-Output===" } ================================================ FILE: plugins/providers/hyperv/scripts/utils/VagrantVM/VagrantVM.psm1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # Always stop when errors are encountered unless instructed not to $ErrorActionPreference = "Stop" # Check the version of Powershell currently in use. If it's # under 7.3.0 we need to restrict the maximum version of the # security module to prevent errors. # Source: https://github.com/PowerShell/PowerShell/issues/18530 $checkVersion = $PSVersionTable.PSVersion if($checkVersion -eq "") { $checkVersion = $(Get-Host).Version } if([System.Version]$checkVersion -lt [System.Version]"7.3.0") { Import-Module Microsoft.Powershell.Security -MaximumVersion 3.0.0.0 } else { Import-Module Microsoft.Powershell.Security } # Vagrant VM creation functions function New-VagrantVM { param ( [parameter(Mandatory=$true)] [string] $VMConfigFile, [parameter(Mandatory=$true)] [string] $DestinationPath, [parameter (Mandatory=$true)] [string] $DataPath, [parameter (Mandatory=$true)] [string] $SourcePath, [parameter (Mandatory=$false)] [bool] $LinkedClone = $false, [parameter (Mandatory=$false)] [int] $Memory = $null, [parameter (Mandatory=$false)] [int] $MaxMemory = $null, [parameter (Mandatory=$false)] [int] $CPUCount = $null, [parameter(Mandatory=$false)] [string] $VMName ) if([IO.Path]::GetExtension($VMConfigFile).ToLower() -eq ".xml") { return New-VagrantVMXML @PSBoundParameters } else { return New-VagrantVMVMCX @PSBoundParameters } <# .SYNOPSIS Create a new Vagrant Hyper-V VM by cloning original. This is the general use function with will call the specialized function based on the extension of the configuration file. .DESCRIPTION Using an existing Hyper-V VM a new Hyper-V VM is created by cloning the original. .PARAMETER VMConfigFile Path to the original Hyper-V VM configuration file. .PARAMETER DestinationPath Path to new Hyper-V VM hard drive. .PARAMETER DataPath Directory path of the original Hyper-V VM to be cloned. .PARAMETER SourcePath Path to the original Hyper-V VM hard drive. .PARAMETER LinkedClone New Hyper-V VM should be linked clone instead of complete copy. .PARAMETER VMName Name of the new Hyper-V VM. .INPUTS None. .OUTPUTS VirtualMachine. The cloned Hyper-V VM. #> } function New-VagrantVMVMCX { param ( [parameter(Mandatory=$true)] [string] $VMConfigFile, [parameter(Mandatory=$true)] [string] $DestinationPath, [parameter (Mandatory=$true)] [string] $DataPath, [parameter (Mandatory=$true)] [string] $SourcePath, [parameter (Mandatory=$false)] [bool] $LinkedClone = $false, [parameter (Mandatory=$false)] [int] $Memory = $null, [parameter (Mandatory=$false)] [int] $MaxMemory = $null, [parameter (Mandatory=$false)] [int] $CPUCount = $null, [parameter(Mandatory=$false)] [string] $VMName ) $NewVMConfig = @{ Path = $VMConfigFile; SnapshotFilePath = Join-Path $DataPath "Snapshots"; VhdDestinationPath = Join-Path $DataPath "Virtual Hard Disks"; VirtualMachinePath = $DataPath; } $VMConfig = (Hyper-V\Compare-VM -Copy -GenerateNewID @NewVMConfig -ErrorAction SilentlyContinue) # If the config is empty it means the import failed. Attempt to provide # context for failure if($VMConfig -eq $null) { Report-ErrorVagrantVMImport -VMConfigFile $VMConfigFile } $VM = $VMConfig.VM $Gen = $VM.Generation # Set VM name if name has been provided if($VMName) { Hyper-V\Set-VM -VM $VM -NewVMName $VMName } # Set EFI secure boot on machines after Gen 1 if($Gen -gt 1) { Hyper-V\Set-VMFirmware -VM $VM -EnableSecureBoot (Hyper-V\Get-VMFirmware -VM $VM).SecureBoot } # Disconnect adapters from switches Hyper-V\Get-VMNetworkAdapter -VM $VM | Hyper-V\Disconnect-VMNetworkAdapter # If we have a memory value provided, set it here if($Memory -ne $null) { Set-VagrantVMMemory -VM $VM -Memory $Memory -MaxMemory $MaxMemory } # If we have a CPU count provided, set it here if($CPUCount -ne $null) { Set-VagrantVMCPUS -VM $VM -CPUCount $CPUCount } # Verify new VM $Report = Hyper-V\Compare-VM -CompatibilityReport $VMConfig if($Report.Incompatibilities.Length -gt 0){ throw $(ConvertTo-Json $($Report.Incompatibilities | Select -ExpandProperty Message)) } if($LinkedClone) { $Controllers = Hyper-V\Get-VMScsiController -VM $VM if($Gen -eq 1){ $Controllers = @($Controllers) + @(Hyper-V\Get-VMIdeController -VM $VM) } foreach($Controller in $Controllers) { foreach($Drive in $Controller.Drives) { if([System.IO.Path]::GetFileName($Drive.Path) -eq [System.IO.Path]::GetFileName($SourcePath)) { $Path = $Drive.Path Hyper-V\Remove-VMHardDiskDrive $Drive Hyper-V\New-VHD -Path $DestinationPath -ParentPath $SourcePath -Differencing Hyper-V\Add-VMHardDiskDrive -VM $VM -Path $DestinationPath break } } } } return Hyper-V\Import-VM -CompatibilityReport $VMConfig <# .SYNOPSIS Create a new Vagrant Hyper-V VM by cloning original (VMCX based). .DESCRIPTION Using an existing Hyper-V VM a new Hyper-V VM is created by cloning the original. .PARAMETER VMConfigFile Path to the original Hyper-V VM configuration file. .PARAMETER DestinationPath Path to new Hyper-V VM hard drive. .PARAMETER DataPath Directory path of the original Hyper-V VM to be cloned. .PARAMETER SourcePath Path to the original Hyper-V VM hard drive. .PARAMETER LinkedClone New Hyper-V VM should be linked clone instead of complete copy. .PARAMETER VMName Name of the new Hyper-V VM. .INPUTS None. .OUTPUTS VirtualMachine. The cloned Hyper-V VM. #> } function New-VagrantVMXML { param ( [parameter(Mandatory=$true)] [string] $VMConfigFile, [parameter(Mandatory=$true)] [string] $DestinationPath, [parameter (Mandatory=$true)] [string] $DataPath, [parameter (Mandatory=$true)] [string] $SourcePath, [parameter (Mandatory=$false)] [bool] $LinkedClone = $false, [parameter (Mandatory=$false)] [int] $Memory = $null, [parameter (Mandatory=$false)] [int] $MaxMemory = $null, [parameter (Mandatory=$false)] [int] $CPUCount = $null, [parameter(Mandatory=$false)] [string] $VMName ) $DestinationDirectory = [System.IO.Path]::GetDirectoryName($DestinationPath) New-Item -ItemType Directory -Force -Path $DestinationDirectory if($LinkedClone){ Hyper-V\New-VHD -Path $DestinationPath -ParentPath $SourcePath -ErrorAction Stop } else { Copy-Item $SourcePath -Destination $DestinationPath -ErrorAction Stop } [xml]$VMConfig = Get-Content -Path $VMConfigFile $Gen = [int]($VMConfig.configuration.properties.subtype."#text") + 1 if(!$VMName) { $VMName = $VMConfig.configuration.properties.name."#text" } # Determine boot device if($Gen -eq 1) { 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" } } } else { Switch ((Select-Xml -xml $VMConfig -XPath "//boot").node.device0."#text") { "HardDrive" { $BootDevice = "VHD" } "Optical" { $BootDevice = "CD" } "Network" { $BootDevice = "NetworkAdapter" } "Default" { $BootDevice = "VHD" } } } # Determine if secure boot is enabled $SecureBoot = (Select-Xml -XML $VMConfig -XPath "//secure_boot_enabled").Node."#text" $SecureBootTemplate = (Select-Xml -XML $VMConfig -XPath "//secure_boot_template").Node."#text" $NewVMConfig = @{ Name = $VMName; NoVHD = $true; BootDevice = $BootDevice; } # Generation parameter in PS4 so validate before using if((Get-Command Hyper-V\New-VM).Parameters.Keys.Contains("generation")) { $NewVMConfig.Generation = $Gen } # Create new VM instance $VM = Hyper-V\New-VM @NewVMConfig # Configure secure boot if($Gen -gt 1) { if($SecureBoot -eq "True") { Hyper-V\Set-VMFirmware -VM $VM -EnableSecureBoot On if ( ( ![System.String]::IsNullOrEmpty($SecureBootTemplate) )` -and` ( (Get-Command Hyper-V\Set-VMFirmware).Parameters.Keys.Contains("secureboottemplate") ) ) { Hyper-V\Set-VMFirmware -VM $VM -SecureBootTemplate $SecureBootTemplate } } else { Hyper-V\Set-VMFirmware -VM $VM -EnableSecureBoot Off } } # Configure drives [regex]$DriveNumberMatcher = "\d" $Controllers = Select-Xml -XML $VMConfig -XPath "//*[starts-with(name(.),'controller')]" foreach($Controller in $Controllers) { $Node = $Controller.Node if($Node.ParentNode.ChannelInstanceGuid) { $ControllerType = "SCSI" } else { $ControllerType = "IDE" } $Drives = $Node.ChildNodes | where {$_.pathname."#text"} foreach($Drive in $Drives) { $DriveType = $Drive.type."#text" if($DriveType -ne "VHD") { continue } $NewDriveConfig = @{ ControllerNumber = $DriveNumberMatcher.Match($Controller.node.name).value; Path = $DestinationPath; ControllerType = $ControllerType; } if($Drive.pool_id."#text") { $NewDriveConfig.ResourcePoolname = $Drive.pool_id."#text" } $VM | Hyper-V\Add-VMHardDiskDrive @NewDriveConfig } } # Apply original VM configuration to new VM instance if($CPUCount -ne $null -and $CPUCount -gt 0) { $processors = $CPUCount } else { $processors = $VMConfig.configuration.settings.processors.count."#text" } $notes = (Select-Xml -XML $VMConfig -XPath "//notes").node."#text" $memoryNode = (Select-Xml -XML $VMConfig -XPath "//memory").node.bank if ($memoryNode.dynamic_memory_enabled."#text" -eq "True") { $dynamicmemory = $True } else { $dynamicmemory = $False } if($Memory -ne $null -and $Memory -gt 0) { $MemoryMaximumBytes = $Memory * 1MB $MemoryStartupBytes = $Memory * 1MB $MemoryMinimumBytes = $Memory * 1MB } else { $MemoryMaximumBytes = ($memoryNode.limit."#text" -as [int]) * 1MB $MemoryStartupBytes = ($memoryNode.size."#text" -as [int]) * 1MB $MemoryMinimumBytes = ($memoryNode.reservation."#text" -as [int]) * 1MB } if($MaxMemory -ne $null -and $MaxMemory -gt 0) { $dynamicmemory = $true $MemoryMaximumBytes = $MaxMemory * 1MB } $Config = @{ ProcessorCount = $processors; MemoryStartupBytes = $MemoryStartupBytes } if($dynamicmemory) { $Config.DynamicMemory = $true $Config.MemoryMinimumBytes = $MemoryMinimumBytes $Config.MemoryMaximumBytes = $MemoryMaximumBytes } else { $Config.StaticMemory = $true } if($notes) { $Config.Notes = $notes } Hyper-V\Set-VM -VM $VM @Config return $VM <# .SYNOPSIS Create a new Vagrant Hyper-V VM by cloning original (XML based). .DESCRIPTION Using an existing Hyper-V VM a new Hyper-V VM is created by cloning the original. .PARAMETER VMConfigFile Path to the original Hyper-V VM configuration file. .PARAMETER DestinationPath Path to new Hyper-V VM hard drive. .PARAMETER DataPath Directory path of the original Hyper-V VM to be cloned. .PARAMETER SourcePath Path to the original Hyper-V VM hard drive. .PARAMETER LinkedClone New Hyper-V VM should be linked clone instead of complete copy. .PARAMETER VMName Name of the new Hyper-V VM. .INPUTS None. .OUTPUTS VirtualMachine. The cloned Hyper-V VM. #> } function Report-ErrorVagrantVMImport { param ( [parameter(Mandatory=$true)] [string] $VMConfigFile ) $ManagementService = Get-WmiObject -Namespace 'root\virtualization\v2' -Class 'Msvm_VirtualSystemManagementService' if($null -eq $ManagementService) { throw 'The Hyper-V Virtual Machine Management Service (VMMS) is not running.' } # Relative path names will fail when attempting to import a system # definition so always ensure we are using the full path to the # configuration file. $FullPathFile = (Resolve-Path $VMConfigFile).Path $Result = $ManagementService.ImportSystemDefinition($FullPathFile, $null, $true) if($Result.ReturnValue -eq 0) { throw "Unknown error encountered while importing VM" } elseif($Result.ReturnValue -eq 4096) { $job = Get-WmiObject -Namespace 'root\virtualization\v2' -Query 'select * from Msvm_ConcreteJob' | Where {$_.__PATH -eq $Result.Job} while($job.JobState -eq 3 -or $job.JobState -eq 4) { start-sleep 1 $job = Get-WmiObject -Namespace 'root\virtualization\v2' -Query 'select * from Msvm_ConcreteJob' | Where {$_.__PATH -eq $Result.Job} } $ErrorMsg = $job.ErrorDescription + "`n`n" $ErrorMsg = $ErrorMsg + "Error Code: " + $job.ErrorCode + "`n" $cause = "Unknown" switch($job.ErrorCode) { 32768 { $cause = "Failed" } 32769 { $cause = "Access Denied" } 32770 { $cause = "Not Supported" } 32771 { $cause = "Status is unknown" } 32772 { $cause = "Timeout" } 32773 { $cause = "Invalid parameter" } 32774 { $cause = "System is in use" } 32775 { $cause = "Invalid state for this operation" } 32776 { $cause = "Incorrect data type" } 32777 { $cause = "System is not available" } 32778 { $cause = "Out of memory" } 32779 { $cause = "File in Use" } 32784 { $cause = "VM version is unsupported" } } $ErrorMsg = $ErrorMsg + "Cause: ${cause}" throw $ErrorMsg } else { throw "Failed to run VM import job. Error value: ${Result.ReturnValue}" } <# .SYNOPSIS Determines cause of error for VM import. .DESCRIPTION Runs a local import of the VM configuration and attempts to determine the underlying cause of the import failure. .PARAMETER VMConfigFile Path to the Hyper-V VM configuration file. .INPUTS None. .OUTPUTS None. #> } # Vagrant VM configuration functions function Set-VagrantVMMemory { param ( [parameter (Mandatory=$true)] [Microsoft.HyperV.PowerShell.VirtualMachine] $VM, [parameter (Mandatory=$false)] [int] $Memory, [parameter (Mandatory=$false)] [int] $MaxMemory ) $ConfigMemory = Hyper-V\Get-VMMemory -VM $VM if(!$Memory) { $MemoryStartupBytes = ($ConfigMemory.Startup) $MemoryMinimumBytes = ($ConfigMemory.Minimum) $MemoryMaximumBytes = ($ConfigMemory.Maximum) } else { $MemoryStartupBytes = $Memory * 1MB $MemoryMinimumBytes = $Memory * 1MB $MemoryMaximumBytes = $Memory * 1MB } if($MaxMemory) { $DynamicMemory = $true $MemoryMaximumBytes = $MaxMemory * 1MB } if($DynamicMemory) { if($MemoryMaximumBytes -lt $MemoryMinimumBytes) { throw "Maximum memory value is less than required minimum memory value." } if ($MemoryMaximumBytes -lt $MemoryStartupBytes) { throw "Maximum memory value is less than configured startup memory value." } Hyper-V\Set-VM -VM $VM -DynamicMemory Hyper-V\Set-VM -VM $VM -MemoryMinimumBytes $MemoryMinimumBytes -MemoryMaximumBytes ` $MemoryMaximumBytes -MemoryStartupBytes $MemoryStartupBytes } else { Hyper-V\Set-VM -VM $VM -StaticMemory Hyper-V\Set-VM -VM $VM -MemoryStartupBytes $MemoryStartupBytes } return $VM <# .SYNOPSIS Configure VM memory settings. .DESCRIPTION Adjusts the VM memory settings. If MaxMemory is defined, dynamic memory is enabled on the VM. .PARAMETER VM Hyper-V VM for modification. .Parameter Memory Memory to allocate to the given VM in MB. .Parameter MaxMemory Maximum memory to allocate to the given VM in MB. When this value is provided dynamic memory is enabled for the VM. The Memory value or the currently configured memory of the VM will be used as the minimum and startup memory value. .Output VirtualMachine. #> } function Set-VagrantVMCPUS { param ( [parameter (Mandatory=$true)] [Microsoft.HyperV.PowerShell.VirtualMachine] $VM, [parameter (Mandatory=$false)] [int] $CPUCount ) if($CPUCount) { Hyper-V\Set-VM -VM $VM -ProcessorCount $CPUCount } return $VM <# .SYNOPSIS Configure VM CPU count. .DESCRIPTION Configure the number of CPUs on the given VM. .PARAMETER VM Hyper-V VM for modification. .PARAMETER CPUCount Number of CPUs. .Output VirtualMachine. #> } function Set-VagrantVMVirtExtensions { param ( [parameter (Mandatory=$true)] [Microsoft.HyperV.PowerShell.VirtualMachine] $VM, [parameter (Mandatory=$false)] [bool] $Enabled=$false ) # Check that this option is available if((Get-Command Hyper-V\Set-VMProcessor).Parameters["ExposeVirtualizationExtensions"] -eq $null) { if($Enabled) { throw "ExposeVirtualizationExtensions is not available" } else { return $VM } } Hyper-V\Set-VMProcessor -VM $VM -ExposeVirtualizationExtensions $Enabled return $VM <# .SYNOPSIS Enable virtualization extensions on VM. .PARAMETER VM Hyper-V VM for modification. .PARAMETER Enabled Enable virtualization extensions on given VM. .OUTPUT VirtualMachine. #> } function Set-VagrantVMAutoActions { param ( [parameter (Mandatory=$true)] [Microsoft.HyperV.PowerShell.VirtualMachine] $VM, [parameter (Mandatory=$false)] [string] $AutoStartAction="Nothing", [parameter (Mandatory=$false)] [string] $AutoStopAction="ShutDown" ) Hyper-V\Set-VM -VM $VM -AutomaticStartAction $AutoStartAction Hyper-V\Set-VM -VM $VM -AutomaticStopAction $AutoStopAction return $VM <# .SYNOPSIS Configure automatic start and stop actions for VM .DESCRIPTION Configures the automatic start and automatic stop actions for the given VM. .PARAMETER VM Hyper-V VM for modification. .PARAMETER AutoStartAction Action the VM should automatically take when the host is started. .PARAMETER AutoStopAction Action the VM should automatically take when the host is stopped. .OUTPUT VirtualMachine. #> } function Set-VagrantVMService { param ( [parameter (Mandatory=$true)] [Microsoft.HyperV.PowerShell.VirtualMachine] $VM, [parameter (Mandatory=$true)] [string] $Id, [parameter (Mandatory=$true)] [bool] $Enable ) if($Enable) { Hyper-V\Get-VMIntegrationService -VM $VM | ?{$_.Id -match $Id} | Hyper-V\Enable-VMIntegrationService } else { Hyper-V\Get-VMIntegrationService -VM $VM | ?{$_.Id -match $Id} | Hyper-V\Disable-VMIntegrationService } return $VM <# .SYNOPSIS Enable or disable Hyper-V VM integration services. .PARAMETER VM Hyper-V VM for modification. .PARAMETER Id Id of the integration service. .PARAMETER Enable Enable or disable the service. .OUTPUT VirtualMachine. #> } # Vagrant networking functions function Get-VagrantVMSwitch { param ( [parameter (Mandatory=$true)] [string] $NameOrID ) $SwitchName = $(Hyper-V\Get-VMSwitch -Id $NameOrID).Name if(!$SwitchName) { $SwitchName = $(Hyper-V\Get-VMSwitch -Name $NameOrID).Name } if(!$SwitchName) { throw "Failed to locate switch with name or ID: ${NameOrID}" } return $SwitchName <# .SYNOPSIS Get name of VMSwitch. .DESCRIPTION Find VMSwitch by name or ID and return name. .PARAMETER NameOrID Name or ID of VMSwitch. .OUTPUT Name of VMSwitch. #> } function Set-VagrantVMSwitch { param ( [parameter (Mandatory=$true)] [Microsoft.HyperV.PowerShell.VirtualMachine] $VM, [parameter (Mandatory=$true)] [String] $SwitchName ) $Adapter = Hyper-V\Get-VMNetworkAdapter -VM $VM Hyper-V\Connect-VMNetworkAdapter -VMNetworkAdapter $Adapter -SwitchName $SwitchName return $VM <# .SYNOPSIS Configure VM to use given switch. .DESCRIPTION Configures VM adapter to use the the VMSwitch with the given name. .PARAMETER VM Hyper-V VM for modification. .PARAMETER SwitchName Name of the VMSwitch. .OUTPUT VirtualMachine. #> } function Check-VagrantHyperVAccess { param ( [parameter (Mandatory=$true)] [string] $Path ) $acl = Get-ACL -Path $Path $systemACL = $acl.Access | where { try { return $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq "S-1-5-18" } catch { return $false } -and $_.FileSystemRights -eq "FullControl" -and $_.AccessControlType -eq "Allow" -and $_.IsInherited -eq $true} if($systemACL) { return $true } return $false <# .SYNOPSIS Check Hyper-V access at given path. .DESCRIPTION Checks that the given path has the correct access rules for Hyper-V .PARAMETER PATH Path to check .OUTPUT Boolean #> } ================================================ FILE: plugins/providers/virtualbox/action/boot.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class Boot def initialize(app, env) @app = app end def call(env) @env = env boot_mode = @env[:machine].provider_config.gui ? "gui" : "headless" # Start up the VM and wait for it to boot. env[:ui].info I18n.t("vagrant.actions.vm.boot.booting") env[:machine].provider.driver.start(boot_mode) @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/check_accessible.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class CheckAccessible def initialize(app, env) @app = app end def call(env) if env[:machine].state.id == :inaccessible # The VM we are attempting to manipulate is inaccessible. This # is a very bad situation and can only be fixed by the user. It # also prohibits us from actually doing anything with the virtual # machine, so we raise an error. raise Vagrant::Errors::VMInaccessible end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/check_created.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action # This middleware checks that the VM is created, and raises an exception # if it is not, notifying the user that creation is required. class CheckCreated def initialize(app, env) @app = app end def call(env) if env[:machine].state.id == :not_created raise Vagrant::Errors::VMNotCreatedError end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/check_guest_additions.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module ProviderVirtualBox module Action class CheckGuestAdditions def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::plugins::virtualbox::check_guest_additions") end def call(env) if !env[:machine].provider_config.check_guest_additions @logger.info("Not checking guest additions because configuration") return @app.call(env) end env[:ui].output(I18n.t("vagrant.virtualbox.checking_guest_additions")) # Use the raw interface for now, while the virtualbox gem # doesn't support guest properties (due to cross platform issues) version = env[:machine].provider.driver.read_guest_additions_version if !version env[:ui].detail(I18n.t("vagrant.actions.vm.check_guest_additions.not_detected")) else # Read the versions versions = [version, env[:machine].provider.driver.version] # Strip of any -OSE or _OSE and read only the first two parts # of the version such as "4.2" in "4.2.0" versions.map! do |v| v = v.gsub(/[-_]ose/i, '') match = /^(\d+\.\d+)\.(\d+)/.match(v) v = match[1] if match v end guest_version = versions[0] vb_version = versions[1] if guest_version != vb_version env[:ui].detail(I18n.t("vagrant.actions.vm.check_guest_additions.version_mismatch", guest_version: version, virtualbox_version: vb_version)) end end # Continue @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/check_running.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action # This middleware checks that the VM is running, and raises an exception # if it is not, notifying the user that the VM must be running. class CheckRunning def initialize(app, env) @app = app end def call(env) if env[:machine].state.id != :running raise Vagrant::Errors::VMNotRunningError end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/check_virtualbox.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/platform' module VagrantPlugins module ProviderVirtualBox module Action # Checks that VirtualBox is installed and ready to be used. class CheckVirtualbox def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::provider::virtualbox") end def call(env) # This verifies that VirtualBox is installed and the driver is # ready to function. If not, then an exception will be raised # which will break us out of execution of the middleware sequence. Driver::Meta.new.verify! # Carry on. @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/clean_machine_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" module VagrantPlugins module ProviderVirtualBox module Action # Cleans up the VirtualBox machine folder for any ".xml-prev" # files which VirtualBox may have left over. This is a bug in # VirtualBox. As soon as this is fixed, this middleware can and # will be removed. class CleanMachineFolder def initialize(app, env) @app = app end def call(env) machine_folder = env[:machine].provider.driver.read_machine_folder begin clean_machine_folder(machine_folder) rescue Errno::EPERM raise Vagrant::Errors::MachineFolderNotAccessible, name: env[:machine].name, path: machine_folder end @app.call(env) end def clean_machine_folder(machine_folder) folder = File.join(machine_folder, "*") # Small safeguard against potentially unwanted rm-rf, since the default # machine folder will typically always be greater than 10 characters long. # For users with it < 10, out of luck? return if folder.length < 10 Dir[folder].each do |f| next unless File.directory?(f) keep = Dir["#{f}/**/*"].find do |d| # Find a file that doesn't have ".xml-prev" as the suffix, # which signals that we want to keep this folder File.file?(d) && !(File.basename(d) =~ /\.vbox-prev$/) end FileUtils.rm_rf(f) if !keep end end end end end end ================================================ FILE: plugins/providers/virtualbox/action/clear_forwarded_ports.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class ClearForwardedPorts def initialize(app, env) @app = app end def call(env) if !env[:machine].provider.driver.read_forwarded_ports.empty? env[:ui].info I18n.t("vagrant.actions.vm.clear_forward_ports.deleting") env[:machine].provider.driver.clear_forwarded_ports end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/clear_network_interfaces.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class ClearNetworkInterfaces def initialize(app, env) @app = app end def call(env) # Create the adapters array to make all adapters nothing. # We do adapters 2 to 8 because that is every built-in adapter # excluding the NAT adapter on port 1 which Vagrant always # expects to exist. adapters = [] 2.upto(env[:machine].provider.driver.max_network_adapters).each do |i| adapters << { adapter: i, type: :none } end # "Enable" all the adapters we setup. env[:ui].info I18n.t("vagrant.actions.vm.clear_network_interfaces.deleting") env[:machine].provider.driver.enable_adapters(adapters) @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/created.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class Created def initialize(app, env) @app = app end def call(env) # Set the result to be true if the machine is created. env[:result] = env[:machine].state.id != :not_created # Call the next if we have one (but we shouldn't, since this # middleware is built to run with the Call-type middlewares) @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/customize.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class Customize def initialize(app, env, event) @app = app @event = event end def call(env) customizations = [] env[:machine].provider_config.customizations.each do |event, command| if event == @event customizations << command end end if !customizations.empty? env[:ui].info I18n.t("vagrant.actions.vm.customize.running", event: @event) # Execute each customization command. customizations.each do |command| processed_command = command.collect do |arg| arg = env[:machine].id if arg == :id arg.to_s end begin env[:machine].provider.driver.execute_command( processed_command + [retryable: true]) rescue Vagrant::Errors::VBoxManageError => e raise Vagrant::Errors::VMCustomizationFailed, { command: command, error: e.inspect } end end end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/destroy.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class Destroy def initialize(app, env) @app = app end def call(env) env[:ui].info I18n.t("vagrant.actions.vm.destroy.destroying") env[:machine].provider.driver.delete env[:machine].id = nil @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/destroy_unused_network_interfaces.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module ProviderVirtualBox module Action class DestroyUnusedNetworkInterfaces def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::plugins::virtualbox::destroy_unused_netifs") end def call(env) if env[:machine].provider_config.destroy_unused_network_interfaces @logger.info("Destroying unused network interfaces...") env[:machine].provider.driver.delete_unused_host_only_networks end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/discard_state.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class DiscardState def initialize(app, env) @app = app end def call(env) if env[:machine].state.id == :saved env[:ui].info I18n.t("vagrant.actions.vm.discard_state.discarding") env[:machine].provider.driver.discard_saved_state end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/export.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require 'vagrant/util/platform' module VagrantPlugins module ProviderVirtualBox module Action class Export def initialize(app, env) @app = app end def call(env) @env = env raise Vagrant::Errors::VMPowerOffToPackage if \ @env[:machine].state.id != :poweroff export @app.call(env) end def export @env[:ui].info I18n.t("vagrant.actions.vm.export.exporting") @env[:machine].provider.driver.export(ovf_path) do |progress| @env[:ui].rewriting do |ui| ui.clear_line ui.report_progress(progress.percent, 100, false) end end # Clear the line a final time so the next data can appear # alone on the line. @env[:ui].clear_line end def ovf_path path = File.join(@env["export.temp_dir"], "box.ovf") # If we're within WSL, we should use the correct path rather than # the mnt path. GH-9059 if Vagrant::Util::Platform.wsl? path = Vagrant::Util::Platform.wsl_to_windows_path(path) end return path end end end end end ================================================ FILE: plugins/providers/virtualbox/action/forced_halt.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class ForcedHalt def initialize(app, env) @app = app end def call(env) current_state = env[:machine].state.id if current_state == :running || current_state == :gurumeditation env[:ui].info I18n.t("vagrant.actions.vm.halt.force") env[:machine].provider.driver.halt end # Sleep for a second to verify that the VM properly # cleans itself up. Silly VirtualBox. sleep 1 if !env["vagrant.test"] @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/forward_ports.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class ForwardPorts include Util::CompileForwardedPorts def initialize(app, env) @app = app end #-------------------------------------------------------------- # Execution #-------------------------------------------------------------- def call(env) @env = env # Get the ports we're forwarding env[:forwarded_ports] ||= compile_forwarded_ports(env[:machine].config) # Warn if we're port forwarding to any privileged ports... env[:forwarded_ports].each do |fp| if fp.host_port <= 1024 env[:ui].warn I18n.t("vagrant.actions.vm.forward_ports.privileged_ports") break end end env[:ui].output(I18n.t("vagrant.actions.vm.forward_ports.forwarding")) forward_ports @app.call(env) end def forward_ports ports = [] interfaces = @env[:machine].provider.driver.read_network_interfaces @env[:forwarded_ports].each do |fp| message_attributes = { adapter: fp.adapter, guest_port: fp.guest_port, host_port: fp.host_port } # Assuming the only reason to establish port forwarding is # because the VM is using Virtualbox NAT networking. Host-only # bridged networking don't require port-forwarding and establishing # forwarded ports on these attachment types has uncertain behaviour. @env[:ui].detail(I18n.t("vagrant.actions.vm.forward_ports.forwarding_entry", **message_attributes)) # Verify we have the network interface to attach to if !interfaces[fp.adapter] raise Vagrant::Errors::ForwardPortAdapterNotFound, adapter: fp.adapter.to_s, guest: fp.guest_port.to_s, host: fp.host_port.to_s end # Port forwarding requires the network interface to be a NAT interface, # so verify that that is the case. if interfaces[fp.adapter][:type] != :nat @env[:ui].detail(I18n.t("vagrant.actions.vm.forward_ports.non_nat", **message_attributes)) next end # Add the options to the ports array to send to the driver later ports << { adapter: fp.adapter, guestip: fp.guest_ip, guestport: fp.guest_port, hostip: fp.host_ip, hostport: fp.host_port, name: fp.id, protocol: fp.protocol } end if !ports.empty? # We only need to forward ports if there are any to forward @env[:machine].provider.driver.forward_ports(ports) end end end end end end ================================================ FILE: plugins/providers/virtualbox/action/import.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class Import def initialize(app, env) @app = app 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].rewriting do |ui| ui.clear_line ui.report_progress(progress, 100, false) end 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) # Import the virtual machine ovf_file = env[:machine].box.directory.join("box.ovf").to_s id = env[:machine].provider.driver.import(ovf_file) do |progress| env[:ui].rewriting do |ui| ui.clear_line ui.report_progress(progress, 100, false) end 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 # immediately. env[:ui].clear_line # If we got interrupted, then the import could have been # interrupted and its not a big deal. Just return out. return if env[:interrupted] # Flag as erroneous and return if import failed raise Vagrant::Errors::VMImportFailure if !id # Import completed successfully. Continue the chain @app.call(env) end def recover(env) if env[:machine] && env[:machine].state.id != Vagrant::MachineState::NOT_CREATED_ID 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 # We don't want to double-execute any hooks attached to # machine_action_up. Instead we should be honoring destroy hooks. # Changing the action name here should make the Builder do the # right thing. destroy_env[:raw_action_name] = :destroy destroy_env[:action_name] = :machine_action_destroy env[:action_runner].run(Action.action_destroy, destroy_env) end end end end end end ================================================ FILE: plugins/providers/virtualbox/action/import_master.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "digest/md5" 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) # 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. lock_key = Digest::MD5.hexdigest(env[:machine].box.name) env[:machine].env.lock(lock_key, retry: true) do import_master(env) end # If we got interrupted, then the import could have been # interrupted and its not a big deal. Just return out. if env[:interrupted] @logger.info("Import of master VM was interrupted -> exiting.") return end # 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[: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[:clone_id] && env[:machine].provider.driver.vm_exists?(env[:clone_id]) @logger.info( "Master VM for '#{env[:machine].box.name}' already exists " + " (id=#{env[:clone_id]}) - skipping import step.") return else if env.delete(:clone_id) @logger.info("Deleting previous reference for master VM" + "'#{env[:machine].box.name}' (id=#{env[:clone_id]}) - " + "maybe it was removed manually") end end env[:ui].info(I18n.t("vagrant.actions.vm.clone.setup_master")) env[:ui].detail(I18n.t("vagrant.actions.vm.clone.setup_master_detail")) # Import the virtual machine import_env = env[:action_runner].run(Import, env.dup.merge(skip_machine: true)) env[:clone_id] = import_env[:machine_id] @logger.info( "Imported box #{env[:machine].box.name} as master vm " + "with id #{env[:clone_id]}") @logger.debug("Writing id of master VM '#{env[:clone_id]}' to #{master_id_file}") master_id_file.open("w+") do |f| f.write(env[:clone_id]) end end end end end end ================================================ FILE: plugins/providers/virtualbox/action/is_paused.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class IsPaused def initialize(app, env) @app = app end def call(env) # Set the result to be true if the machine is paused. env[:result] = env[:machine].state.id == :paused # Call the next if we have one (but we shouldn't, since this # middleware is built to run with the Call-type middlewares) @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/is_running.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class IsRunning def initialize(app, env) @app = app end def call(env) # Set the result to be true if the machine is running. env[:result] = env[:machine].state.id == :running # Call the next if we have one (but we shouldn't, since this # middleware is built to run with the Call-type middlewares) @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/is_saved.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class IsSaved def initialize(app, env) @app = app end def call(env) # Set the result to be true if the machine is saved. env[:result] = env[:machine].state.id == :saved # Call the next if we have one (but we shouldn't, since this # middleware is built to run with the Call-type middlewares) @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/match_mac_address.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class MatchMACAddress def initialize(app, env) @app = app 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 if env[:machine].config.vm.base_mac # Create the proc which we want to use to modify the virtual machine env[:ui].info I18n.t("vagrant.actions.vm.match_mac.matching") env[:machine].provider.driver.set_mac_address(env[:machine].config.vm.base_mac) else env[:ui].info I18n.t("vagrant.actions.vm.match_mac.generating") env[:machine].provider.driver.set_mac_address(nil) end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/message_already_running.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class MessageAlreadyRunning def initialize(app, env) @app = app end def call(env) env[:ui].info I18n.t("vagrant.commands.common.vm_already_running") @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/message_not_created.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class MessageNotCreated def initialize(app, env) @app = app end def call(env) env[:ui].info I18n.t("vagrant.commands.common.vm_not_created") @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/message_not_running.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class MessageNotRunning def initialize(app, env) @app = app end def call(env) env[:ui].info I18n.t("vagrant.commands.common.vm_not_running") @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/message_will_not_destroy.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class MessageWillNotDestroy def initialize(app, env) @app = app end def call(env) env[:ui].info I18n.t("vagrant.commands.destroy.will_not_destroy", name: env[:machine].name) @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/network.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ipaddr" require "resolv" require "set" require "log4r" require "vagrant/util/network_ip" require "vagrant/util/scoped_hash_override" module VagrantPlugins module ProviderVirtualBox module Action # This middleware class sets up all networking for the VirtualBox # instance. This includes host only networks, bridged networking, # forwarded ports, etc. # # This handles all the `config.vm.network` configurations. class Network # Location of the VirtualBox networks configuration file VBOX_NET_CONF = "/etc/vbox/networks.conf".freeze # Version of VirtualBox that introduced hostonly network range restrictions HOSTONLY_VALIDATE_VERSION = Gem::Version.new("6.1.28") # Version of VirtualBox on darwin platform that ignores restrictions DARWIN_IGNORE_HOSTONLY_VALIDATE_VERSION = Gem::Version.new("7.0.0") # Default valid range for hostonly networks HOSTONLY_DEFAULT_RANGE = [ IPAddr.new("192.168.56.0/21").freeze, IPAddr.new("fe80::/10").freeze ].freeze include Vagrant::Util::NetworkIP include Vagrant::Util::ScopedHashOverride def initialize(app, env) @logger = Log4r::Logger.new("vagrant::plugins::virtualbox::network") @app = app end def call(env) # TODO: Validate network configuration prior to anything below @env = env # Get the list of network adapters from the configuration network_adapters_config = env[:machine].provider_config.network_adapters.dup # Assign the adapter slot for each high-level network available_slots = Set.new(1..36) network_adapters_config.each do |slot, _data| available_slots.delete(slot) end @logger.debug("Available slots for high-level adapters: #{available_slots.inspect}") @logger.info("Determining network adapters required for high-level configuration...") available_slots = available_slots.to_a.sort env[:machine].config.vm.networks.each do |type, options| # We only handle private and public networks next if type != :private_network && type != :public_network options = scoped_hash_override(options, :virtualbox) # Figure out the slot that this adapter will go into slot = options[:adapter] if !slot if available_slots.empty? raise Vagrant::Errors::VirtualBoxNoRoomForHighLevelNetwork end slot = available_slots.shift end # Internal network is a special type if type == :private_network && options[:intnet] type = :internal_network end if !options.key?(:type) && options.key?(:ip) begin addr = IPAddr.new(options[:ip]) options[:type] = if addr.ipv4? :static else :static6 end rescue IPAddr::Error => err raise Vagrant::Errors::NetworkAddressInvalid, address: options[:ip], mask: options[:netmask], error: err.message end end # Configure it data = nil if type == :private_network # private_network = hostonly data = [:hostonly, options] elsif type == :public_network # public_network = bridged data = [:bridged, options] elsif type == :internal_network data = [:intnet, options] end # Store it! @logger.info(" -- Slot #{slot}: #{data[0]}") network_adapters_config[slot] = data end @logger.info("Determining adapters and compiling network configuration...") adapters = [] networks = [] network_adapters_config.each do |slot, data| type = data[0] options = data[1] @logger.info("Network slot #{slot}. Type: #{type}.") # Get the normalized configuration for this type config = send("#{type}_config", options) config[:adapter] = slot @logger.debug("Normalized configuration: #{config.inspect}") # Get the VirtualBox adapter configuration adapter = send("#{type}_adapter", config) adapters << adapter @logger.debug("Adapter configuration: #{adapter.inspect}") # Get the network configuration network = send("#{type}_network_config", config) network[:auto_config] = config[:auto_config] networks << network end if !adapters.empty? # Enable the adapters @logger.info("Enabling adapters...") env[:ui].output(I18n.t("vagrant.actions.vm.network.preparing")) adapters.each do |adapter| env[:ui].detail(I18n.t( "vagrant.virtualbox.network_adapter", adapter: adapter[:adapter].to_s, type: adapter[:type].to_s, extra: "", )) end env[:machine].provider.driver.enable_adapters(adapters) end # Continue the middleware chain. @app.call(env) # If we have networks to configure, then we configure it now, since # that requires the machine to be up and running. if !adapters.empty? && !networks.empty? assign_interface_numbers(networks, adapters) # Only configure the networks the user requested us to configure networks_to_configure = networks.select { |n| n[:auto_config] } if !networks_to_configure.empty? env[:ui].info I18n.t("vagrant.actions.vm.network.configuring") env[:machine].guest.capability(:configure_networks, networks_to_configure) end end end def bridged_config(options) return { auto_config: true, bridge: nil, mac: nil, nic_type: nil, use_dhcp_assigned_default_route: false }.merge(options || {}) end def bridged_adapter(config) # Find the bridged interfaces that are available bridgedifs = @env[:machine].provider.driver.read_bridged_interfaces bridgedifs.delete_if { |interface| interface[:status] == "Down" || interface[:status] == "Unknown" } # The name of the chosen bridge interface will be assigned to this # variable. chosen_bridge = nil if config[:bridge] @logger.debug("Bridge was directly specified in config, searching for: #{config[:bridge]}") # Search for a matching bridged interface Array(config[:bridge]).each do |bridge| bridge = bridge.downcase if bridge.respond_to?(:downcase) bridgedifs.each do |interface| if bridge === interface[:name].downcase @logger.debug("Specific bridge found as configured in the Vagrantfile. Using it.") chosen_bridge = interface[:name] break end end break if chosen_bridge end # If one wasn't found, then we notify the user here. if !chosen_bridge @env[:ui].info I18n.t("vagrant.actions.vm.bridged_networking.specific_not_found", bridge: config[:bridge]) end end # If we still don't have a bridge chosen (this means that one wasn't # specified in the Vagrantfile, or the bridge specified in the Vagrantfile # wasn't found), then we fall back to the normal means of searching for a # bridged network. if !chosen_bridge if bridgedifs.length == 1 # One bridgeable interface? Just use it. chosen_bridge = bridgedifs[0][:name] @logger.debug("Only one bridged interface available. Using it by default.") else # More than one bridgeable interface requires a user decision, so # show options to choose from. @env[:ui].info I18n.t( "vagrant.actions.vm.bridged_networking.available", prefix: false) bridgedifs.each_index do |index| interface = bridgedifs[index] @env[:ui].info("#{index + 1}) #{interface[:name]}", prefix: false) end @env[:ui].info(I18n.t( "vagrant.actions.vm.bridged_networking.choice_help")+"\n") # The range of valid choices valid = Range.new(1, bridgedifs.length) # The choice that the user has chosen as the bridging interface choice = nil while !valid.include?(choice) choice = @env[:ui].ask( "Which interface should the network bridge to? ") choice = choice.to_i end chosen_bridge = bridgedifs[choice - 1][:name] end end @logger.info("Bridging adapter #{config[:adapter]} to #{chosen_bridge}") # Given the choice we can now define the adapter we're using return { adapter: config[:adapter], type: :bridged, bridge: chosen_bridge, mac_address: config[:mac], nic_type: config[:nic_type] } end def bridged_network_config(config) if config[:ip] options = { auto_config: true, mac: nil, netmask: "255.255.255.0", type: :static }.merge(config) options[:type] = options[:type].to_sym return options end return { type: :dhcp, use_dhcp_assigned_default_route: config[:use_dhcp_assigned_default_route] } end def hostonly_config(options) options = { auto_config: true, mac: nil, nic_type: nil, type: :static, }.merge(options) # Make sure the type is a symbol options[:type] = options[:type].to_sym if options[:type] == :dhcp && !options[:ip] # Try to find a matching device to set the config ip to matching_device = hostonly_find_matching_network(options) if matching_device options[:ip] = matching_device[:ip] else # Default IP is in the 20-bit private network block for DHCP based networks options[:ip] = "192.168.56.1" end end begin ip = IPAddr.new(options[:ip]) if ip.ipv4? options[:netmask] ||= "255.255.255.0" elsif ip.ipv6? options[:netmask] ||= 64 # Append a 6 to the end of the type if it is not already set options[:type] = "#{options[:type]}6".to_sym if !options[:type].to_s.end_with?("6") else raise IPAddr::AddressFamilyError, 'unknown address family' end # Calculate our network address for the given IP/netmask netaddr = IPAddr.new("#{options[:ip]}/#{options[:netmask]}") rescue IPAddr::Error => e raise Vagrant::Errors::NetworkAddressInvalid, address: options[:ip], mask: options[:netmask], error: e.message end validate_hostonly_ip!(options[:ip], @env[:machine].provider.driver) if ip.ipv4? # 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]) if netaddr == that_netaddr && interface[:status] != "Down" raise Vagrant::Errors::NetworkCollision, netaddr: netaddr, that_netaddr: that_netaddr, interface_name: interface[:name] end end end # Calculate the adapter IP which is the network address with # the final bit + 1. Usually it is "x.x.x.1" for IPv4 and # "::1" for IPv6 options[:adapter_ip] ||= (netaddr | 1).to_s dhcp_options = {} if options[:type] == :dhcp # Calculate the DHCP server IP and lower & upper bound # Example: for "192.168.22.64/26" network range those are: # dhcp_ip: "192.168.22.66", # dhcp_lower: "192.168.22.67" # dhcp_upper: "192.168.22.126" ip_range = netaddr.to_range dhcp_options[:dhcp_ip] = options[:dhcp_ip] || (ip_range.first | 2).to_s dhcp_options[:dhcp_lower] = options[:dhcp_lower] || (ip_range.first | 3).to_s dhcp_options[:dhcp_upper] = options[:dhcp_upper] || (ip_range.last(2).first).to_s end # Find the hostonly interface name if display name was # provided if options[:name] hostif = @env[:machine].provider.driver.read_host_only_interfaces.detect { |interface| interface[:name] == options[:name] || interface[:display_name] == options[:name] } options[:name] = hostif[:name] if hostif end return { adapter_ip: options[:adapter_ip], auto_config: options[:auto_config], ip: options[:ip], mac: options[:mac], name: options[:name], netmask: options[:netmask], nic_type: options[:nic_type], type: options[:type] }.merge(dhcp_options) end def hostonly_adapter(config) @logger.info("Searching for matching hostonly network: #{config[:ip]}") interface = hostonly_find_matching_network(config) if !interface @logger.info("Network not found. Creating if we can.") # It is an error if a specific host only network name was specified # but the network wasn't found. if config[:name] raise Vagrant::Errors::NetworkNotFound, name: config[:name] end # Create a new network interface = hostonly_create_network(config) @logger.info("Created network: #{interface[:name]}") end if config[:type] == :dhcp create_dhcp_server_if_necessary(interface, config) end return { adapter: config[:adapter], hostonly: interface[:name], mac_address: config[:mac], nic_type: config[:nic_type], type: :hostonly } end def hostonly_network_config(config) return { type: config[:type], adapter_ip: config[:adapter_ip], ip: config[:ip], netmask: config[:netmask] } end def intnet_config(options) return { type: "static", ip: nil, netmask: "255.255.255.0", adapter: nil, mac: nil, intnet: nil, auto_config: true }.merge(options || {}) end def intnet_adapter(config) intnet_name = config[:intnet] intnet_name = "intnet" if intnet_name == true return { adapter: config[:adapter], type: :intnet, mac_address: config[:mac], nic_type: config[:nic_type], intnet: intnet_name, } end def intnet_network_config(config) return { type: config[:type], ip: config[:ip], netmask: config[:netmask] } end def nat_config(options) return options.merge( auto_config: false ) end def nat_adapter(config) return { adapter: config[:adapter], type: :nat, nic_type: config[:nic_type], } end def nat_network_config(config) return {} end #----------------------------------------------------------------- # Misc. helpers #----------------------------------------------------------------- # Assigns the actual interface number of a network based on the # enabled NICs on the virtual machine. # # This interface number is used by the guest to configure the # NIC on the guest VM. # # The networks are modified in place by adding an ":interface" # field to each. def assign_interface_numbers(networks, adapters) current = 0 adapter_to_interface = {} # Make a first pass to assign interface numbers by adapter location vm_adapters = @env[:machine].provider.driver.read_network_interfaces vm_adapters.sort.each do |number, adapter| if adapter[:type] != :none # Not used, so assign the interface number and increment adapter_to_interface[number] = current current += 1 end end # Make a pass through the adapters to assign the :interface # key to each network configuration. adapters.each_index do |i| adapter = adapters[i] network = networks[i] # Figure out the interface number by simple lookup network[:interface] = adapter_to_interface[adapter[:adapter]] end end #----------------------------------------------------------------- # Hostonly Helper Functions #----------------------------------------------------------------- # This creates a host only network for the given configuration. def hostonly_create_network(config) @env[:machine].provider.driver.create_host_only_network(config) end # This finds a matching host only network for the given configuration. def hostonly_find_matching_network(config) this_netaddr = network_address(config[:ip], config[:netmask]) if config[:ip] @env[:machine].provider.driver.read_host_only_interfaces.each do |interface| return interface if config[:name] && config[:name] == interface[:name] #if a config name is specified, we should only look for that. if config[:name].to_s != "" next end 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 end # Validates the IP used to configure the network is within the allowed # ranges. It only validates if the network configuration file exists. # This was introduced in 6.1.28 so previous version won't have restrictions # placed on the valid ranges def validate_hostonly_ip!(ip, driver) return if Gem::Version.new(driver.version) < HOSTONLY_VALIDATE_VERSION || ( Vagrant::Util::Platform.darwin? && Gem::Version.new(driver.version) >= DARWIN_IGNORE_HOSTONLY_VALIDATE_VERSION ) || Vagrant::Util::Platform.windows? ip = IPAddr.new(ip.to_s) if !ip.is_a?(IPAddr) valid_ranges = load_net_conf return if valid_ranges.any?{ |range| range.include?(ip) } raise Vagrant::Errors::VirtualBoxInvalidHostSubnet, address: ip, ranges: valid_ranges.map{ |r| "#{r}/#{r.prefix}" }.join(", ") end def load_net_conf return HOSTONLY_DEFAULT_RANGE if !File.exist?(VBOX_NET_CONF) File.readlines(VBOX_NET_CONF).map do |line| line = line.strip next if !line.start_with?("*") line[1,line.length].strip.split(" ").map do |entry| IPAddr.new(entry) end end.flatten.compact end #----------------------------------------------------------------- # DHCP Server Helper Functions #----------------------------------------------------------------- DEFAULT_DHCP_SERVER_FROM_VBOX_INSTALL = { network_name: 'HostInterfaceNetworking-vboxnet0', network: 'vboxnet0', ip: '192.168.56.100', netmask: '255.255.255.0', lower: '192.168.56.101', upper: '192.168.56.254' }.freeze # # When a host-only network of type: :dhcp is configured, # this handles the potential creation of a vbox dhcpserver to manage # it. # # @param [Hash] interface hash as returned from read_host_only_interfaces # @param [Hash] config hash as returned from hostonly_config def create_dhcp_server_if_necessary(interface, config) existing_dhcp_server = find_matching_dhcp_server(interface) if existing_dhcp_server if dhcp_server_matches_config?(existing_dhcp_server, config) @logger.debug("DHCP server already properly configured") return elsif existing_dhcp_server == DEFAULT_DHCP_SERVER_FROM_VBOX_INSTALL @env[:ui].info I18n.t("vagrant.actions.vm.network.cleanup_vbox_default_dhcp") @env[:machine].provider.driver.remove_dhcp_server(existing_dhcp_server[:network_name]) else # We have an invalid DHCP server that we're not able to # automatically clean up, so we need to give up and tell the user # to sort out their own vbox dhcpservers and hostonlyifs raise Vagrant::Errors::NetworkDHCPAlreadyAttached end end @logger.debug("Creating a DHCP server...") @env[:machine].provider.driver.create_dhcp_server(interface[:name], config) end # Detect when an existing DHCP server matches precisely the # requested config for a hostonly interface. # # @param [Hash] dhcp_server as found by read_dhcp_servers # @param [Hash] config as returned from hostonly_config # @return [Boolean] def dhcp_server_matches_config?(dhcp_server, config) dhcp_server[:ip] == config[:dhcp_ip] && dhcp_server[:lower] == config[:dhcp_lower] && dhcp_server[:upper] == config[:dhcp_upper] end # Returns the existing dhcp server, if any, that is attached to the # specified interface. # # @return [Hash] dhcp_server or nil if not found def find_matching_dhcp_server(interface) @env[:machine].provider.driver.read_dhcp_servers.detect do |dhcp_server| interface[:name] && interface[:name] == dhcp_server[:network] end end end end end end ================================================ FILE: plugins/providers/virtualbox/action/network_fix_ipv6.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ipaddr" require "socket" require "log4r" require "vagrant/util/presence" 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 sometimes lose the # route to that machine. class NetworkFixIPv6 include Vagrant::Util::Presence 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].to_s.strip == "" 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 link_local_range = IPAddr.new("fe80::/10") host_only_interfaces(env).each do |interface| next if !present?(interface[:ipv6]) next if interface[:status] != "Up" ip = IPAddr.new(interface[:ipv6]) ip |= ("1" * (128 - interface[:ipv6_prefix].to_i)).to_i(2) next if link_local_range.include?(ip) @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 # The list of interface names for host-only adapters. # @return [Array] def host_only_interface_names(env) env[:machine].provider.driver.read_network_interfaces .map { |_, i| i[:hostonly] if i[:type] == :hostonly }.compact end # The list of host_only_interfaces that are tied to a host-only adapter. # @return [Array] def host_only_interfaces(env) iface_names = self.host_only_interface_names(env) env[:machine].provider.driver.read_host_only_interfaces .select { |interface| iface_names.include?(interface[:name]) } end end end end end ================================================ FILE: plugins/providers/virtualbox/action/package.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../lib/vagrant/action/general/package" module VagrantPlugins module ProviderVirtualBox module Action class Package < Vagrant::Action::General::Package # Doing this so that we can test that the parent is properly # called in the unit tests. alias_method :general_call, :call def call(env) general_call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/package_setup_files.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../lib/vagrant/action/general/package_setup_files" module VagrantPlugins module ProviderVirtualBox module Action class PackageSetupFiles < Vagrant::Action::General::PackageSetupFiles # Doing this so that we can test that the parent is properly # called in the unit tests. alias_method :general_call, :call def call(env) general_call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/package_setup_folders.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require_relative "../../../../lib/vagrant/action/general/package_setup_folders" module VagrantPlugins module ProviderVirtualBox module Action class PackageSetupFolders < Vagrant::Action::General::PackageSetupFolders # Doing this so that we can test that the parent is properly # called in the unit tests. alias_method :general_call, :call def call(env) general_call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/package_vagrantfile.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'vagrant/util/template_renderer' module VagrantPlugins module ProviderVirtualBox module Action class PackageVagrantfile # For TemplateRenderer include Vagrant::Util def initialize(app, env) @app = app end def call(env) @env = env create_vagrantfile @app.call(env) end # This method creates the auto-generated Vagrantfile at the root of the # box. This Vagrantfile contains the MAC address so that the user doesn't # have to worry about it. def create_vagrantfile File.open(File.join(@env["export.temp_dir"], "Vagrantfile"), "w") do |f| f.write(TemplateRenderer.render("package_Vagrantfile", { base_mac: @env[:machine].provider.driver.read_mac_address })) end end end end end end ================================================ FILE: plugins/providers/virtualbox/action/prepare_clone_snapshot.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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].rewriting do |ui| ui.clear_line ui.report_progress(progress, 100, false) end end end end end end end ================================================ FILE: plugins/providers/virtualbox/action/prepare_forwarded_port_collision_params.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class PrepareForwardedPortCollisionParams def initialize(app, env) @app = app end def call(env) # Get the forwarded ports used by other virtual machines and # consider those in use as well. env[:port_collision_extra_in_use] = env[:machine].provider.driver.read_used_ports # Build the remap for any existing collision detections remap = {} env[:port_collision_remap] = remap env[:machine].provider.driver.read_forwarded_ports.each do |_nic, name, hostport, _guestport| env[:machine].config.vm.networks.each do |type, options| next if type != :forwarded_port # If the ID matches the name of the forwarded port, then # remap. if options[:id] == name remap[options[:host]] = hostport break end end end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/prepare_nfs_settings.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ipaddr" require "vagrant/action/builtin/mixin_synced_folders" module VagrantPlugins module ProviderVirtualBox module Action class PrepareNFSSettings include Vagrant::Action::Builtin::MixinSyncedFolders include Vagrant::Util::Retryable def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::vm::nfs") end def call(env) @machine = env[:machine] @app.call(env) opts = { cached: !!env[:synced_folders_cached], config: env[:synced_folders_config], disable_usable_check: !!env[:test], } folders = synced_folders(env[:machine], **opts) if folders.key?(:nfs) @logger.info("Using NFS, preparing NFS settings by reading host IP and machine IP") add_ips_to_env!(env) end end # Extracts the proper host and guest IPs for NFS mounts and stores them # in the environment for the SyncedFolder action to use them in # mounting. # # The ! indicates that this method modifies its argument. def add_ips_to_env!(env) adapter, host_ip = find_host_only_adapter machine_ip = read_static_machine_ips if !machine_ip # No static IP, attempt to use the dynamic IP. machine_ip = read_dynamic_machine_ip(adapter) else # We have static IPs, also attempt to read any dynamic IPs. # If there is no dynamic IP on the adapter, it doesn't matter. We # already have a static IP. begin dynamic_ip = read_dynamic_machine_ip(adapter) rescue Vagrant::Errors::NFSNoGuestIP dynamic_ip = nil end # If we found a dynamic IP and we didn't include it in the # machine_ip array yet, do so. if dynamic_ip && !machine_ip.include?(dynamic_ip) machine_ip.push(dynamic_ip) end end if host_ip && !machine_ip.empty? interface = @machine.provider.driver.read_host_only_interfaces.detect do |iface| iface[:ip] == host_ip end host_ipaddr = IPAddr.new("#{host_ip}/#{interface.fetch(:netmask, "0.0.0.0")}") case machine_ip when String machine_ip = nil if !host_ipaddr.include?(machine_ip) when Array machine_ip.delete_if do |m_ip| !host_ipaddr.include?(m_ip) end machine_ip = nil if machine_ip.empty? end end raise Vagrant::Errors::NFSNoHostonlyNetwork if !host_ip || !machine_ip env[:nfs_host_ip] = host_ip env[:nfs_machine_ip] = machine_ip end # Finds first host only network adapter and returns its adapter number # and IP address # # @return [Integer, String] adapter number, ip address of found host-only adapter def find_host_only_adapter @machine.provider.driver.read_network_interfaces.each do |adapter, opts| if opts[:type] == :hostonly @machine.provider.driver.read_host_only_interfaces.each do |interface| if interface[:name] == opts[:hostonly] return adapter, interface[:ip] end end end end nil end # Returns the IP address(es) of the guest by looking for static IPs # given to host only adapters in the Vagrantfile # # @return [Array] Configured static IPs def read_static_machine_ips ips = [] @machine.config.vm.networks.each do |type, options| options = scoped_hash_override(options, :virtualbox) if type == :private_network && options[:type] != :dhcp && options[:ip].is_a?(String) ips << options[:ip] end end if ips.empty? return nil end ips end # Returns the IP address of the guest by looking at vbox guest property # for the appropriate guest adapter. # # For DHCP interfaces, the guest property will not be present until the # guest completes # # @param [Integer] adapter number to read IP for # @return [String] ip address of adapter def read_dynamic_machine_ip(adapter) return nil unless adapter # vbox guest properties are 0-indexed, while showvminfo network # interfaces are 1-indexed. go figure. guestproperty_adapter = adapter - 1 # we need to wait for the guest's IP to show up as a guest property. # retry thresholds are relatively high since we might need to wait # for DHCP, but even static IPs can take a second or two to appear. retryable(retry_options.merge(on: Vagrant::Errors::VirtualBoxGuestPropertyNotFound)) do @machine.provider.driver.read_guest_ip(guestproperty_adapter) end rescue Vagrant::Errors::VirtualBoxGuestPropertyNotFound # this error is more specific with a better error message directing # the user towards the fact that it's probably a reportable bug raise Vagrant::Errors::NFSNoGuestIP end # Separating these out so we can stub out the sleep in tests def retry_options {tries: 15, sleep: 1} end end end end end ================================================ FILE: plugins/providers/virtualbox/action/prepare_nfs_valid_ids.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class PrepareNFSValidIds def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::action::vm::nfs") end def call(env) env[:nfs_valid_ids] = env[:machine].provider.driver.read_vms.values @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/resume.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class Resume def initialize(app, env) @app = app end def call(env) current_state = env[:machine].state.id if current_state == :paused env[:ui].info I18n.t("vagrant.actions.vm.resume.unpausing") env[:machine].provider.driver.resume elsif current_state == :saved env[:ui].info I18n.t("vagrant.actions.vm.resume.resuming") env[:action_runner].run(Boot, env) end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/sane_defaults.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module ProviderVirtualBox module Action class SaneDefaults def initialize(app, env) @logger = Log4r::Logger.new("vagrant::action::vm::sanedefaults") @app = app end def call(env) # Set the env on an instance variable so we can access it in # helpers. @env = env # Use rtcuseutc so that the VM sees UTC time. command = ["modifyvm", env[:machine].id, "--rtcuseutc", "on"] attempt_and_log(command, "Enabling rtcuseutc...") if env[:machine].provider_config.auto_nat_dns_proxy @logger.info("Automatically figuring out whether to enable/disable NAT DNS proxy...") # Enable/disable the NAT DNS proxy as necessary if enable_dns_proxy? command = ["modifyvm", env[:machine].id, "--natdnsproxy1", "on"] attempt_and_log(command, "Enable the NAT DNS proxy on adapter 1...") else command = ["modifyvm", env[:machine].id, "--natdnsproxy1", "off" ] attempt_and_log(command, "Disable the NAT DNS proxy on adapter 1...") command = ["modifyvm", env[:machine].id, "--natdnshostresolver1", "off" ] attempt_and_log(command, "Disable the NAT DNS resolver on adapter 1...") end else @logger.info("NOT trying to automatically manage NAT DNS proxy.") end @app.call(env) end protected # This is just a helper method that executes a single command, logs # the given string to the log, and also includes the exit status in # the log message. # # We assume every command is idempotent and pass along the `retryable` # flag. This is because VBoxManage is janky about running simultaneously # on the same box, and if we up multiple boxes at the same time, a bunch # of modifyvm commands get fired # # @param [Array] command Command to run # @param [String] log Log message to write. def attempt_and_log(command, log) begin @env[:machine].provider.driver.execute_command( command + [retryable: true]) rescue Vagrant::Errors::VBoxManageError => e @logger.info("#{log} (error = #{e.inspect})") end end # This uses some heuristics to determine if the NAT DNS proxy should # be enabled or disabled. See the comments within the function body # itself to see the checks it does. # # @return [Boolean] def enable_dns_proxy? begin contents = File.read("/etc/resolv.conf") if contents =~ /^nameserver 127\.0\.(0|1)\.1$/ # The use of both natdnsproxy and natdnshostresolver break on # Ubuntu 12.04 and 12.10 that uses resolvconf with localhost. When used # VirtualBox will give the client dns server 10.0.2.3, while # not binding to that address itself. Therefore disable this # feature if host uses the resolvconf server 127.0.0.1 or # 127.0.1.1 @logger.info("Disabling DNS proxy since resolv.conf contains 127.0.0.1 or 127.0.1.1") return false end rescue Errno::ENOENT; end return true end end end end end ================================================ FILE: plugins/providers/virtualbox/action/set_default_nic_type.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module ProviderVirtualBox module Action # This sets the default NIC type used for network adapters created # on the guest. Also includes a check of NIC types in use and VirtualBox # version to determine if E1000 NIC types are vulnerable. # # NOTE: Vulnerability was fixed here: https://www.virtualbox.org/changeset/75330/vbox class SetDefaultNICType # Defines versions of VirtualBox with susceptible implementation # of the E1000 devices. E1000_SUSCEPTIBLE = Gem::Requirement.new("< 5.2.22").freeze def initialize(app, env) @logger = Log4r::Logger.new("vagrant::plugins::virtualbox::set_default_nic_type") @app = app end def call(env) default_nic_type = env[:machine].provider_config.default_nic_type e1000_in_use = [ # simple check on default_nic_type ->{ default_nic_type.nil? || default_nic_type.to_s.start_with?("8254") }, # check provider defined adapters ->{ env[:machine].provider_config.network_adapters.values.detect{ |_, opts| opts[:nic_type].to_s.start_with?("8254") } }, # finish with inspecting configured networks ->{ env[:machine].config.vm.networks.detect{ |_, opts| opts.fetch(:virtualbox__nic_type, opts[:nic_type]).to_s.start_with?("8254") } } ] # Check if VirtualBox E1000 implementation is vulnerable if E1000_SUSCEPTIBLE.satisfied_by?(Gem::Version.new(env[:machine].provider.driver.version)) @logger.info("Detected VirtualBox version with susceptible E1000 implementation (`#{E1000_SUSCEPTIBLE}`)") if e1000_in_use.any?(&:call) env[:ui].warn I18n.t("vagrant.actions.vm.set_default_nic_type.e1000_warning") end end if default_nic_type @logger.info("Default NIC type for VirtualBox interfaces `#{default_nic_type}`") # Update network adapters defined in provider configuration env[:machine].provider_config.network_adapters.each do |slot, args| _, opts = args if opts && !opts.key?(:nic_type) @logger.info("Setting default NIC type (`#{default_nic_type}`) adapter `#{slot}` - `#{args}`") opts[:nic_type] = default_nic_type end end # Update generally defined networks env[:machine].config.vm.networks.each do |type, options| next if !type.to_s.end_with?("_network") if !options.key?(:nic_type) && !options.key?(:virtualbox__nic_type) @logger.info("Setting default NIC type (`#{default_nic_type}`) for `#{type}` - `#{options}`") options[:virtualbox__nic_type] = default_nic_type end end end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/set_name.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module ProviderVirtualBox module Action class SetName def initialize(app, env) @logger = Log4r::Logger.new("vagrant::action::vm::setname") @app = app end def call(env) name = env[:machine].provider_config.name # If we already set the name before, then don't do anything sentinel = env[:machine].data_dir.join("action_set_name") if !name && sentinel.file? @logger.info("Default name was already set before, not doing it again.") return @app.call(env) end # If no name was manually set, then use a default if !name prefix = "#{env[:root_path].basename.to_s}_#{env[:machine].name}" prefix.gsub!(/[^-a-z0-9_]/i, "") # milliseconds + random number suffix to allow for simultaneous # `vagrant up` of the same box in different dirs name = prefix + "_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" end # Verify the name is not taken vms = env[:machine].provider.driver.read_vms raise Vagrant::Errors::VMNameExists, name: name if \ vms.key?(name) && vms[name] != env[:machine].id if vms.key?(name) @logger.info("Not setting the name because our name is already set.") else env[:ui].info(I18n.t( "vagrant.actions.vm.set_name.setting_name", name: name)) env[:machine].provider.driver.set_name(name) end # Create the sentinel sentinel.open("w") do |f| f.write(Time.now.to_i.to_s) end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/setup_package_files.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require_relative "package_setup_files" module VagrantPlugins module ProviderVirtualBox module Action class SetupPackageFiles < PackageSetupFiles def initialize(*) @logger = Log4r::Logger.new("vagrant::plugins::virtualbox::setup_package_files") @logger.warn { "SetupPackageFiles has been renamed to PackageSetupFiles" } super end end end end end ================================================ FILE: plugins/providers/virtualbox/action/snapshot_delete.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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]) do |progress| env[:ui].rewriting do |ui| ui.clear_line ui.report_progress(progress, 100, false) end 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", name: env[:snapshot_name])) @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/snapshot_restore.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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]) do |progress| env[:ui].rewriting do |ui| ui.clear_line ui.report_progress(progress, 100, false) end end # Clear the line one last time since the progress meter doesn't disappear # immediately. env[:ui].clear_line @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action/snapshot_save.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 ================================================ FILE: plugins/providers/virtualbox/action/suspend.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Action class Suspend def initialize(app, env) @app = app end def call(env) if env[:machine].state.id == :running env[:ui].info I18n.t("vagrant.actions.vm.suspend.suspending") env[:machine].provider.driver.suspend end @app.call(env) end end end end end ================================================ FILE: plugins/providers/virtualbox/action.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/action/builder" module VagrantPlugins module ProviderVirtualBox module Action autoload :Boot, File.expand_path("../action/boot", __FILE__) autoload :CheckAccessible, File.expand_path("../action/check_accessible", __FILE__) autoload :CheckCreated, File.expand_path("../action/check_created", __FILE__) autoload :CheckGuestAdditions, File.expand_path("../action/check_guest_additions", __FILE__) autoload :CheckRunning, File.expand_path("../action/check_running", __FILE__) autoload :CheckVirtualbox, File.expand_path("../action/check_virtualbox", __FILE__) 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 :Created, File.expand_path("../action/created", __FILE__) autoload :Customize, File.expand_path("../action/customize", __FILE__) autoload :Destroy, File.expand_path("../action/destroy", __FILE__) autoload :DestroyUnusedNetworkInterfaces, File.expand_path("../action/destroy_unused_network_interfaces", __FILE__) autoload :DiscardState, File.expand_path("../action/discard_state", __FILE__) autoload :Export, File.expand_path("../action/export", __FILE__) 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__) autoload :MatchMACAddress, File.expand_path("../action/match_mac_address", __FILE__) autoload :MessageAlreadyRunning, File.expand_path("../action/message_already_running", __FILE__) autoload :MessageNotCreated, File.expand_path("../action/message_not_created", __FILE__) 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 :PackageSetupFiles, File.expand_path("../action/package_setup_files", __FILE__) autoload :PackageSetupFolders, File.expand_path("../action/package_setup_folders", __FILE__) autoload :PackageVagrantfile, File.expand_path("../action/package_vagrantfile", __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__) autoload :Resume, File.expand_path("../action/resume", __FILE__) autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__) autoload :SetDefaultNICType, File.expand_path("../action/set_default_nic_type", __FILE__) autoload :SetName, File.expand_path("../action/set_name", __FILE__) autoload :SnapshotDelete, File.expand_path("../action/snapshot_delete", __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__) # @deprecated use {PackageSetupFiles} instead autoload :SetupPackageFiles, File.expand_path("../action/setup_package_files", __FILE__) # Include the built-in modules so that we can use them as top-level # things. include Vagrant::Action::Builtin # This action boots the VM, assuming the VM is in a state that requires # a bootup (i.e. not saved). def self.action_boot Vagrant::Action::Builder.new.tap do |b| b.use CheckAccessible b.use CleanMachineFolder b.use SetName b.use ClearForwardedPorts b.use Provision b.use EnvSet, port_collision_repair: true b.use PrepareForwardedPortCollisionParams b.use HandleForwardedPortCollisions b.use PrepareNFSValidIds b.use SyncedFolderCleanup b.use SyncedFolders b.use PrepareNFSSettings b.use SetDefaultNICType b.use ClearNetworkInterfaces b.use Network b.use NetworkFixIPv6 b.use ForwardPorts b.use SetHostname b.use SaneDefaults 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 CloudInitWait b.use Customize, "post-comm" b.use CheckGuestAdditions end end # This is the action that is primarily responsible for completely # freeing the resources of the underlying virtual machine. def self.action_destroy Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use Call, Created do |env1, b2| if !env1[:result] b2.use MessageNotCreated next end b2.use Call, DestroyConfirm do |env2, b3| if env2[:result] b3.use ConfigValidate b3.use ProvisionerCleanup, :before b3.use CheckAccessible b3.use EnvSet, force_halt: env2[:force_halt] b3.use action_halt b3.use Destroy b3.use CleanMachineFolder b3.use DestroyUnusedNetworkInterfaces b3.use PrepareNFSValidIds b3.use SyncedFolderCleanup else b3.use MessageWillNotDestroy end end end end end # This is the action that is primarily responsible for halting # the virtual machine, gracefully or by force. def self.action_halt Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use Call, Created do |env, b2| if env[:result] b2.use CheckAccessible b2.use DiscardState b2.use Call, IsPaused do |env2, b3| next if !env2[:result] b3.use Resume end b2.use Call, GracefulHalt, :poweroff, :running do |env2, b3| if !env2[:result] b3.use ForcedHalt end end else b2.use MessageNotCreated end end end end # This action packages the virtual machine into a single box file. def self.action_package Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use Call, Created do |env1, b2| if !env1[:result] b2.use MessageNotCreated next end b2.use PackageSetupFolders b2.use PackageSetupFiles b2.use CheckAccessible b2.use action_halt b2.use ClearForwardedPorts b2.use PrepareNFSValidIds b2.use SyncedFolderCleanup b2.use Package b2.use Export b2.use PackageVagrantfile end end end # This action just runs the provisioners on the machine. def self.action_provision Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use ConfigValidate b.use Call, Created do |env1, b2| if !env1[:result] b2.use MessageNotCreated next end b2.use Call, IsRunning do |env2, b3| if !env2[:result] b3.use MessageNotRunning next end b3.use CheckAccessible b3.use Provision end end end end # This action is responsible for reloading the machine, which # brings it down, sucks in new configuration, and brings the # machine back up with the new configuration. def self.action_reload Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use Call, Created do |env1, b2| if !env1[:result] b2.use MessageNotCreated next end b2.use ConfigValidate b2.use action_halt b2.use action_start end end end # This is the action that is primarily responsible for resuming # suspended machines. def self.action_resume Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use Call, Created do |env, b2| if env[:result] b2.use CheckAccessible b2.use EnvSet, port_collision_repair: false b2.use PrepareForwardedPortCollisionParams b2.use HandleForwardedPortCollisions b2.use Resume b2.use Provision b2.use WaitForCommunicator, [:restoring, :running] else b2.use MessageNotCreated end end 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 restoring 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] raise Vagrant::Errors::VMNotCreatedError end b2.use CheckAccessible 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 Call, IsEnvSet, :snapshot_start do |env2, b3| if env2[:result] b3.use action_start end 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| b.use CheckVirtualbox b.use CheckCreated b.use CheckAccessible b.use CheckRunning b.use SSHExec end end # This is the action that will run a single SSH command. def self.action_ssh_run Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use CheckCreated b.use CheckAccessible b.use CheckRunning b.use SSHRun end end # This action starts a VM, assuming it is already imported and exists. # A precondition of this action is that the VM exists. def self.action_start Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use ConfigValidate b.use BoxCheckOutdated b.use Call, IsRunning do |env, b2| # If the VM is running, run the necessary provisioners if env[:result] b2.use action_provision next end b2.use Call, IsSaved do |env2, b3| if env2[:result] # The VM is saved, so just resume it b3.use action_resume next end b3.use Call, IsPaused do |env3, b4| if env3[:result] b4.use Resume next end # The VM is not saved, so we must have to boot it up # like normal. Boot! b4.use action_boot end end end end end # This is the action that is primarily responsible for suspending # the virtual machine. def self.action_suspend Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox b.use Call, Created do |env, b2| if env[:result] b2.use CheckAccessible b2.use Suspend else b2.use MessageNotCreated end end end end # This is the action that is called to sync folders to a running # machine without a reboot. def self.action_sync_folders Vagrant::Action::Builder.new.tap do |b| b.use PrepareNFSValidIds b.use SyncedFolders b.use PrepareNFSSettings end end # This action brings the machine up from nothing, including importing # the box, configuring metadata, and booting. def self.action_up Vagrant::Action::Builder.new.tap do |b| b.use CheckVirtualbox # Handle box_url downloading early so that if the Vagrantfile # references any files in the box or something it all just # works fine. b.use Call, Created do |env, b2| if !env[:result] b2.use HandleBox end end b.use ConfigValidate b.use Call, Created do |env, b2| # If the VM is NOT created yet, then do the setup steps if !env[:result] b2.use CheckAccessible b2.use Customize, "pre-import" 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 b2.use MatchMACAddress end end b.use action_start end end end end end ================================================ FILE: plugins/providers/virtualbox/cap/cleanup_disks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/util/experimental" module VagrantPlugins module ProviderVirtualBox module Cap module CleanupDisks LOGGER = Log4r::Logger.new("vagrant::plugins::virtualbox::cleanup_disks") # @param [Vagrant::Machine] machine # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks # @param [Hash] disk_meta_file - A hash of all the previously defined disks from the last configure_disk action def self.cleanup_disks(machine, defined_disks, disk_meta_file) return if disk_meta_file.values.flatten.empty? handle_cleanup_disk(machine, defined_disks, disk_meta_file["disk"]) handle_cleanup_dvd(machine, defined_disks, disk_meta_file["dvd"]) # TODO: Floppy disks end protected # @param [Vagrant::Machine] machine # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks # @param [Array] disk_meta - An array of all the previously defined disks from the last configure_disk action def self.handle_cleanup_disk(machine, defined_disks, disk_meta) raise TypeError, "Expected `Array` but received `#{disk_meta.class}`" if !disk_meta.is_a?(Array) storage_controllers = machine.provider.driver.read_storage_controllers primary = storage_controllers.get_primary_attachment primary_uuid = primary[:uuid] disk_meta.each do |d| dsk = defined_disks.select { |dk| dk.name == d["name"] } if !dsk.empty? || d["uuid"] == primary_uuid next else LOGGER.warn("Found disk not in Vagrantfile config: '#{d["name"]}'. Removing disk from guest #{machine.name}") machine.ui.warn(I18n.t("vagrant.cap.cleanup_disks.disk_cleanup", name: d["name"]), prefix: true) controller = storage_controllers.get_controller(d["controller"]) attachment = controller.get_attachment(uuid: d["uuid"]) if !attachment LOGGER.warn("Disk '#{d["name"]}' not attached to guest, but still exists.") else machine.provider.driver.remove_disk(controller.name, attachment[:port], attachment[:device]) end machine.provider.driver.close_medium(d["uuid"]) end end end # @param [Vagrant::Machine] machine # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_dvds # @param [Array] dvd_meta - An array of all the previously defined dvds from the last configure_disk action def self.handle_cleanup_dvd(machine, defined_dvds, dvd_meta) raise TypeError, "Expected `Array` but received `#{dvd_meta.class}`" if !dvd_meta.is_a?(Array) dvd_meta.each do |d| dsk = defined_dvds.select { |dk| dk.name == d["name"] } if !dsk.empty? next else LOGGER.warn("Found dvd not in Vagrantfile config: '#{d["name"]}'. Removing dvd from guest #{machine.name}") machine.ui.warn("DVD '#{d["name"]}' no longer exists in Vagrant config. Removing medium from guest...", prefix: true) storage_controllers = machine.provider.driver.read_storage_controllers controller = storage_controllers.get_controller(d["controller"]) attachment = controller.get_attachment(uuid: d["uuid"]) if !attachment LOGGER.warn("DVD '#{d["name"]}' not attached to guest, but still exists.") else machine.provider.driver.remove_disk(controller.name, attachment[:port], attachment[:device]) end end end end end end end end ================================================ FILE: plugins/providers/virtualbox/cap/configure_disks.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "fileutils" require "vagrant/util/numeric" require "vagrant/util/experimental" module VagrantPlugins module ProviderVirtualBox module Cap module ConfigureDisks LOGGER = Log4r::Logger.new("vagrant::plugins::virtualbox::configure_disks") # @param [Vagrant::Machine] machine # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks # @return [Hash] configured_disks - A hash of all the current configured disks def self.configure_disks(machine, defined_disks) return {} if defined_disks.empty? machine.ui.info(I18n.t("vagrant.cap.configure_disks.start")) storage_controllers = machine.provider.driver.read_storage_controllers # Check to determine which controller we should attach disks to. # If there is only one storage controller attached to the VM, use # it. If there are multiple controllers (e.g. IDE/SATA), attach DVDs # to the IDE controller and disks to the SATA controller. if storage_controllers.size == 1 controller = storage_controllers.first # The only way you can define up to the controller limit is if # exactly one disk is a primary disk, otherwise we need to reserve # a slot for the primary if (defined_disks.any? { |d| d.primary } && defined_disks.size > controller.limit) || defined_disks.size > controller.limit - 1 raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit, limit: controller.limit, name: controller.name else disk_controller = controller dvd_controller = controller end else disks_defined = defined_disks.select { |d| d.type == :disk } if disks_defined.any? disk_controller = storage_controllers.get_primary_controller if (disks_defined.any? { |d| d.primary } && disks_defined.size > disk_controller.limit) || disks_defined.size > disk_controller.limit - 1 raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit, limit: disk_controller.limit, name: disk_controller.name end end dvds_defined = defined_disks.select { |d| d.type == :dvd } if dvds_defined.any? dvd_controller = storage_controllers.get_dvd_controller if dvds_defined.size > dvd_controller.limit raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit, limit: dvd_controller.limit, name: dvd_controller.name end end end configured_disks = { disk: [], floppy: [], dvd: [] } defined_disks.each do |disk| if disk.type == :disk disk_data = handle_configure_disk(machine, disk, disk_controller.name) configured_disks[:disk] << disk_data unless disk_data.empty? elsif disk.type == :floppy # TODO: Write me machine.ui.info(I18n.t("vagrant.cap.configure_disks.floppy_not_supported", name: disk.name)) elsif disk.type == :dvd dvd_data = handle_configure_dvd(machine, disk, dvd_controller.name) configured_disks[:dvd] << dvd_data unless dvd_data.empty? end end configured_disks end protected # @param [Vagrant::Machine] machine - the current machine # @param [Config::Disk] disk - the current disk to configure # @param [Array] all_disks - A list of all currently defined disks in VirtualBox # @return [Hash] current_disk - Returns the current disk. Returns nil if it doesn't exist def self.get_current_disk(machine, disk, all_disks) current_disk = nil if disk.primary storage_controllers = machine.provider.driver.read_storage_controllers current_disk = storage_controllers.get_primary_attachment else current_disk = all_disks.detect { |d| d[:disk_name] == disk.name } end current_disk end # Handles all disk configs of type `:disk` # # @param [Vagrant::Machine] machine - the current machine # @param [Config::Disk] disk - the current disk to configure # @param [String] controller_name - the name of the storage controller to use # @return [Hash] - disk_metadata def self.handle_configure_disk(machine, disk, controller_name) storage_controllers = machine.provider.driver.read_storage_controllers controller = storage_controllers.get_controller(controller_name) all_disks = controller.attachments disk_metadata = {} # Grab the existing configured disk attached to guest, if it exists current_disk = get_current_disk(machine, disk, all_disks) if !current_disk # Look for an existing disk that's not been attached but exists # inside VirtualBox # # NOTE: This assumes that if that disk exists and was created by # Vagrant, it exists in the same location as the primary disk file. # Otherwise Vagrant has no good way to determining if the disk was # associated with the guest, since disk names are not unique # globally to VirtualBox. primary = storage_controllers.get_primary_attachment existing_disk = machine.provider.driver.list_hdds.detect do |d| File.dirname(d["Location"]) == File.dirname(primary[:location]) && d["Disk Name"] == disk.name end if !existing_disk # create new disk and attach to guest disk_metadata = create_disk(machine, disk, controller) else # Disk has been created but failed to be attached to guest, so # this method recovers that disk from previous failure # and attaches it onto the guest LOGGER.warn("Disk '#{disk.name}' is not connected to guest '#{machine.name}', Vagrant will attempt to connect disk to guest") dsk_info = get_next_port(machine, controller) machine.provider.driver.attach_disk(controller.name, dsk_info[:port], dsk_info[:device], "hdd", existing_disk["Location"]) disk_metadata[:uuid] = existing_disk["UUID"] disk_metadata[:port] = dsk_info[:port] disk_metadata[:device] = dsk_info[:device] disk_metadata[:name] = disk.name disk_metadata[:controller] = controller.name end elsif compare_disk_size(machine, disk, current_disk) disk_metadata = resize_disk(machine, disk, current_disk, controller) else LOGGER.info("No further configuration required for disk '#{disk.name}'") disk_metadata[:uuid] = current_disk[:uuid] disk_metadata[:port] = current_disk[:port] disk_metadata[:device] = current_disk[:device] disk_metadata[:name] = disk.name disk_metadata[:controller] = controller.name end disk_metadata end # Handles all disk configs of type `:dvd` # # @param [Vagrant::Machine] machine - the current machine # @param [Config::Disk] dvd - the current disk to configure # @param [String] controller_name - the name of the storage controller to use # @return [Hash] - dvd_metadata def self.handle_configure_dvd(machine, dvd, controller_name) storage_controllers = machine.provider.driver.read_storage_controllers controller = storage_controllers.get_controller(controller_name) dvd_metadata = {} dvd_location = File.expand_path(dvd.file) dvd_attached = controller.attachments.detect { |a| a[:location] == dvd_location } if dvd_attached LOGGER.info("No further configuration required for dvd '#{dvd.name}'") dvd_metadata[:name] = dvd.name dvd_metadata[:port] = dvd_attached[:port] dvd_metadata[:device] = dvd_attached[:device] dvd_metadata[:uuid] = dvd_attached[:uuid] dvd_metadata[:controller] = controller.name else LOGGER.warn("DVD '#{dvd.name}' is not connected to guest '#{machine.name}', Vagrant will attempt to connect dvd to guest") dsk_info = get_next_port(machine, controller) machine.provider.driver.attach_disk(controller.name, dsk_info[:port], dsk_info[:device], "dvddrive", dvd.file) # Refresh the controller information storage_controllers = machine.provider.driver.read_storage_controllers controller = storage_controllers.get_controller(controller_name) attachment = controller.attachments.detect { |a| a[:port] == dsk_info[:port] && a[:device] == dsk_info[:device] } dvd_metadata[:name] = dvd.name dvd_metadata[:port] = dsk_info[:port] dvd_metadata[:device] = dsk_info[:device] dvd_metadata[:uuid] = attachment[:uuid] dvd_metadata[:controller] = controller.name end dvd_metadata end # Check to see if current disk is configured based on defined_disks # # @param [Kernel_V2::VagrantConfigDisk] disk_config # @param [Hash] defined_disk # @return [Boolean] def self.compare_disk_size(machine, disk_config, defined_disk) requested_disk_size = Vagrant::Util::Numeric.bytes_to_megabytes(disk_config.size) defined_disk_size = defined_disk[:capacity].split(" ").first.to_f if defined_disk_size > requested_disk_size machine.ui.warn(I18n.t("vagrant.cap.configure_disks.shrink_size_not_supported", name: disk_config.name)) return false elsif defined_disk_size < requested_disk_size return true else return false end end # Creates and attaches a disk to a machine # # @param [Vagrant::Machine] machine # @param [Kernel_V2::VagrantConfigDisk] disk_config # @param [VagrantPlugins::ProviderVirtualBox::Model::StorageController] controller - # the storage controller to use def self.create_disk(machine, disk_config, controller) machine.ui.detail(I18n.t("vagrant.cap.configure_disks.create_disk", name: disk_config.name)) # NOTE: At the moment, there are no provider specific configs for VirtualBox # but we grab it anyway for future use. disk_provider_config = disk_config.provider_config[:virtualbox] if disk_config.provider_config guest_info = machine.provider.driver.show_vm_info guest_folder = File.dirname(guest_info["CfgFile"]) disk_ext = disk_config.disk_ext disk_file = File.join(guest_folder, disk_config.name) + ".#{disk_ext}" LOGGER.info("Attempting to create a new disk file '#{disk_file}' of size '#{disk_config.size}' bytes") disk_var = machine.provider.driver.create_disk(disk_file, disk_config.size, disk_ext.upcase) dsk_controller_info = get_next_port(machine, controller) machine.provider.driver.attach_disk(controller.name, dsk_controller_info[:port], dsk_controller_info[:device], "hdd", disk_file) disk_metadata = { uuid: disk_var.split(":").last.strip, name: disk_config.name, controller: controller.name, port: dsk_controller_info[:port], device: dsk_controller_info[:device] } disk_metadata end # Finds the next available port # # SATA Controller-ImageUUID-0-0 (sub out ImageUUID) # - Controller: SATA Controller # - Port: 0 # - Device: 0 # # Note: Virtualbox returns the string above with the port and device info # disk_info = key.split("-") # port = disk_info[2] # device = disk_info[3] # # @param [Vagrant::Machine] machine # @param [VagrantPlugins::ProviderVirtualBox::Model::StorageController] controller - # the storage controller to use # @return [Hash] dsk_info - The next available port and device on a given controller def self.get_next_port(machine, controller) dsk_info = {} if controller.devices_per_port == 1 used_ports = controller.attachments.map { |a| a[:port].to_i } next_available_port = ((0..(controller.maxportcount - 1)).to_a - used_ports).first dsk_info[:port] = next_available_port.to_s dsk_info[:device] = "0" elsif controller.devices_per_port == 2 # IDE Controllers have primary/secondary devices, so find the first port # with an empty device (0..(controller.maxportcount - 1)).each do |port| # Skip this port if it's full port_attachments = controller.attachments.select { |a| a[:port] == port.to_s } next if port_attachments.count == controller.devices_per_port dsk_info[:port] = port.to_s # Check for a free device if port_attachments.any? { |a| a[:device] == "0" } dsk_info[:device] = "1" else dsk_info[:device] = "0" end break if dsk_info[:port] end else raise Vagrant::Errors::VirtualBoxDisksUnsupportedController, controller_name: controller.name end if dsk_info[:port].to_s.empty? # This likely only occurs if additional disks have been added outside of Vagrant configuration LOGGER.warn("There is no more available space to attach disks to for the controller '#{controller}'. Clear up some space on the controller '#{controller}' to attach new disks.") raise Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit, limit: controller.limit, name: controller.name end dsk_info end # @param [Vagrant::Machine] machine # @param [Config::Disk] disk_config - the current disk to configure # @param [Hash] defined_disk - current disk as represented by VirtualBox # @param [VagrantPlugins::ProviderVirtualBox::Model::StorageController] controller - # the storage controller to use # @return [Hash] - disk_metadata def self.resize_disk(machine, disk_config, defined_disk, controller) machine.ui.detail(I18n.t("vagrant.cap.configure_disks.resize_disk", name: disk_config.name), prefix: true) if defined_disk[:storage_format] == "VMDK" LOGGER.warn("Disk type VMDK cannot be resized in VirtualBox. Vagrant will convert disk to VDI format to resize first, and then convert resized disk back to VMDK format") # original disk information in case anything goes wrong during clone/resize original_disk = defined_disk backup_disk_location = "#{original_disk[:location]}.backup" # clone disk to vdi formatted disk vdi_disk_file = machine.provider.driver.vmdk_to_vdi(defined_disk[:location]) # resize vdi machine.provider.driver.resize_disk(vdi_disk_file, disk_config.size.to_i) begin # Danger Zone # remove and close original volume machine.provider.driver.remove_disk(controller.name, defined_disk[:port], defined_disk[:device]) # Create a backup of the original disk if something goes wrong LOGGER.warn("Making a backup of the original disk at #{defined_disk[:location]}") FileUtils.mv(defined_disk[:location], backup_disk_location) # we have to close here, otherwise we can't re-clone after # resizing the vdi disk machine.provider.driver.close_medium(defined_disk[:uuid]) # clone back to original vmdk format and attach resized disk vmdk_disk_file = machine.provider.driver.vdi_to_vmdk(vdi_disk_file) machine.provider.driver.attach_disk(controller.name, defined_disk[:port], defined_disk[:device], "hdd", vmdk_disk_file) rescue ScriptError, SignalException, StandardError LOGGER.warn("Vagrant encountered an error while trying to resize a disk. Vagrant will now attempt to reattach and preserve the original disk...") machine.ui.error(I18n.t("vagrant.cap.configure_disks.recovery_from_resize", location: original_disk[:location], name: machine.name)) recover_from_resize(machine, defined_disk, backup_disk_location, original_disk, vdi_disk_file, controller) raise ensure # Remove backup disk file if all goes well FileUtils.remove(backup_disk_location, force: true) end # Remove cloned resized volume format machine.provider.driver.close_medium(vdi_disk_file) # Get new updated disk UUID for vagrant disk_meta file storage_controllers = machine.provider.driver.read_storage_controllers updated_controller = storage_controllers.get_controller(controller.name) new_disk_info = updated_controller.attachments.detect { |h| h[:location] == defined_disk[:location] } defined_disk = new_disk_info else machine.provider.driver.resize_disk(defined_disk[:location], disk_config.size.to_i) end disk_metadata = { uuid: defined_disk[:uuid], name: disk_config.name, controller: controller.name, port: defined_disk[:port], device: defined_disk[:device] } disk_metadata end # Recovery method for when an exception occurs during the process of resizing disks # # It attempts to move back the backup disk into place, and reattach it to the guest before # raising the original error # # @param [Vagrant::Machine] machine # @param [Hash] disk_info - The disk device and port number to attach back to # @param [String] backup_disk_location - The place on disk where vagrant made a backup of the original disk being resized # @param [Hash] original_disk - The disk information from VirtualBox # @param [String] vdi_disk_file - The place on disk where vagrant made a clone of the original disk being resized # @param [VagrantPlugins::ProviderVirtualBox::Model::StorageController] controller - the storage controller to use def self.recover_from_resize(machine, disk_info, backup_disk_location, original_disk, vdi_disk_file, controller) begin # move backup to original name FileUtils.mv(backup_disk_location, original_disk[:location], force: true) # Attach disk machine.provider.driver.attach_disk(controller.name, disk_info[:port], disk_info[:device], "hdd", original_disk[:location]) # Remove cloned disk if still hanging around if vdi_disk_file machine.provider.driver.close_medium(vdi_disk_file) end # We recovered! machine.ui.warn(I18n.t("vagrant.cap.configure_disks.recovery_attached_disks")) rescue => e LOGGER.error("Vagrant encountered an error while trying to recover. It will now show the original error and continue...") LOGGER.error(e) end end end end end end ================================================ FILE: plugins/providers/virtualbox/cap/mount_options.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../synced_folders/unix_mount_helpers" module VagrantPlugins module ProviderVirtualBox module Cap module MountOptions extend VagrantPlugins::SyncedFolder::UnixMountHelpers VB_MOUNT_TYPE = "vboxsf".freeze # Returns mount options for a virtual box synced folder # # @param [Machine] machine # @param [String] name of mount # @param [String] path of mount on guest # @param [Hash] hash of mount options def self.mount_options(machine, name, guest_path, options) mount_options = options.fetch(:mount_options, []) detected_ids = detect_owner_group_ids(machine, guest_path, mount_options, options) mount_uid = detected_ids[:uid] mount_gid = detected_ids[:gid] mount_options << "uid=#{mount_uid}" mount_options << "gid=#{mount_gid}" mount_options << "_netdev" mount_options = mount_options.join(',') return mount_options, mount_uid, mount_gid end def self.mount_type(machine) return VB_MOUNT_TYPE end def self.mount_name(machine, name, data) name.gsub(/[\s\/\\]/,'_').sub(/^_/, '') end end end end end ================================================ FILE: plugins/providers/virtualbox/cap/public_address.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Cap module PublicAddress def self.public_address(machine) return nil if machine.state.id != :running ssh_info = machine.ssh_info return nil if !ssh_info ssh_info[:host] end end end end end ================================================ FILE: plugins/providers/virtualbox/cap/validate_disk_ext.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module ProviderVirtualBox module Cap module ValidateDiskExt LOGGER = Log4r::Logger.new("vagrant::plugins::virtualbox::validate_disk_ext") # The default set of disk formats that VirtualBox supports DEFAULT_DISK_EXT_LIST = ["vdi", "vmdk", "vhd"].map(&:freeze).freeze DEFAULT_DISK_EXT = "vdi".freeze # @param [Vagrant::Machine] machine # @param [String] disk_ext # @return [Bool] def self.validate_disk_ext(machine, disk_ext) DEFAULT_DISK_EXT_LIST.include?(disk_ext) end # @param [Vagrant::Machine] machine # @return [Array] def self.default_disk_exts(machine) DEFAULT_DISK_EXT_LIST end # @param [Vagrant::Machine] machine # @return [String] def self.set_default_disk_ext(machine) DEFAULT_DISK_EXT end end end end end ================================================ FILE: plugins/providers/virtualbox/cap.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Cap # Reads the forwarded ports that currently exist on the machine # itself. This raises an exception if the machine isn't running. # # This also may not match up with configured forwarded ports, because # Vagrant auto port collision fixing may have taken place. # # @return [Hash] Host => Guest port mappings. def self.forwarded_ports(machine) return nil if machine.state.id != :running {}.tap do |result| machine.provider.driver.read_forwarded_ports.each do |_, _, h, g| result[h] = g end end end # Reads the network interface card MAC addresses and returns them. # # @return [Hash] Adapter => MAC address 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) return [] if machine.id.nil? machine.provider.driver.list_snapshots(machine.id) end end end end ================================================ FILE: plugins/providers/virtualbox/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox class Config < Vagrant.plugin("2", :config) # Vagrant by default will make "smart" decisions to enable/disable # the NAT DNS proxy. If this is set to `true`, then the DNS proxy # will not be enabled, and it is up to the end user to do it. # # @return [Boolean] attr_accessor :auto_nat_dns_proxy # If true, will check if guest additions are installed and up to # date. By default, this is true. # # @return [Boolean] attr_accessor :check_guest_additions # An array of customizations to make on the VM prior to booting it. # # @return [Array] attr_reader :customizations # Set the default type of NIC hardware to be used for network # devices. By default this is `nil` and VirtualBox's default # will be used. # # @return [String] attr_accessor :default_nic_type # If true, unused network interfaces will automatically be deleted. # This defaults to false because the detection does not work across # multiple users, and because on Windows this operation requires # administrative privileges. # # @return [Boolean] attr_accessor :destroy_unused_network_interfaces # If set to `true`, then VirtualBox will be launched with a GUI. # # @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 :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. # # @return [String] attr_accessor :name # Whether or not this VM has a functional vboxsf filesystem module. # This defaults to true. If you set this to false, then the "virtualbox" # synced folder type won't be valid. # # @return [Boolean] attr_accessor :functional_vboxsf # The defined network adapters. # # @return [Hash] attr_reader :network_adapters def initialize @auto_nat_dns_proxy = UNSET_VALUE @check_guest_additions = UNSET_VALUE @customizations = [] @default_nic_type = UNSET_VALUE @destroy_unused_network_interfaces = UNSET_VALUE @functional_vboxsf = UNSET_VALUE @name = UNSET_VALUE @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) end # Customize the VM by calling `VBoxManage` with the given # arguments. # # When called multiple times, the customizations will be applied # in the order given. # # The special `:name` parameter in the command will be replaced with # the unique ID or name of the virtual machine. This is useful for # parameters to `modifyvm` and the like. # # @param [Array] command An array of arguments to pass to # VBoxManage. def customize(*command) event = command.first.is_a?(String) ? command.shift : "pre-boot" command = command[0] @customizations << [event, command] end # This defines a network adapter that will be added to the VirtualBox # virtual machine in the given slot. # # @param [Integer] slot The slot for this network adapter. # @param [Symbol] type The type of adapter. def network_adapter(slot, type, **opts) @network_adapters[slot] = [type, opts] end # Shortcut for setting memory size for the virtual machine. # Calls #customize internally. # # @param size [Integer, String] the memory size in MB def memory=(size) customize("pre-boot", ["modifyvm", :id, "--memory", size.to_s]) end # Shortcut for setting CPU count for the virtual machine. # Calls #customize internally. # # @param count [Integer, String] the count of CPUs def cpus=(count) customize("pre-boot", ["modifyvm", :id, "--cpus", count.to_i]) end def merge(other) super.tap do |result| c = customizations.dup c += other.customizations result.instance_variable_set(:@customizations, c) end end # This is the hook that is called to finalize the object before it # is put into use. def finalize! # Default is to auto the DNS proxy @auto_nat_dns_proxy = true if @auto_nat_dns_proxy == UNSET_VALUE if @check_guest_additions == UNSET_VALUE @check_guest_additions = true end if @destroy_unused_network_interfaces == UNSET_VALUE @destroy_unused_network_interfaces = false end if @functional_vboxsf == UNSET_VALUE @functional_vboxsf = true end # Default is to not show a GUI @gui = false if @gui == UNSET_VALUE # 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 @default_nic_type = nil if @default_nic_type == UNSET_VALUE end def validate(machine) errors = _detected_errors valid_events = ["pre-import", "pre-boot", "post-boot", "post-comm"] @customizations.each do |event, _| if !valid_events.include?(event) errors << I18n.t( "vagrant.virtualbox.config.invalid_event", event: event.to_s, valid_events: valid_events.join(", ")) end end @customizations.each do |event, command| if event == "pre-import" && command.index(:id) errors << I18n.t("vagrant.virtualbox.config.id_in_pre_import") end end # Verify that internal networks are only on private networks. machine.config.vm.networks.each do |type, data| if data[:virtualbox__intnet] && type != :private_network errors << I18n.t("vagrant.virtualbox.config.intnet_on_bad_type") break end end { "VirtualBox Provider" => errors } end def to_s "VirtualBox" end end end end ================================================ FILE: plugins/providers/virtualbox/driver/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' require 'vagrant/util/busy' require 'vagrant/util/platform' require 'vagrant/util/retryable' require 'vagrant/util/subprocess' require 'vagrant/util/which' module VagrantPlugins module ProviderVirtualBox module Driver # Base class for all VirtualBox drivers. # # This class provides useful tools for things such as executing # VBoxManage and handling SIGINTs and so on. class Base # Include this so we can use `Subprocess` more easily. include Vagrant::Util::Retryable def initialize @logger = Log4r::Logger.new("vagrant::provider::virtualbox::base") # This flag is used to keep track of interrupted state (SIGINT) @interrupted = false if Vagrant::Util::Platform.windows? || Vagrant::Util::Platform.cygwin? @logger.debug("Windows, checking for VBoxManage on PATH first") @vboxmanage_path = Vagrant::Util::Which.which("VBoxManage") # On Windows, we use the VBOX_INSTALL_PATH environmental # variable to find VBoxManage. if !@vboxmanage_path && (ENV.key?("VBOX_INSTALL_PATH") || ENV.key?("VBOX_MSI_INSTALL_PATH")) @logger.debug("Windows. Trying VBOX_INSTALL_PATH for VBoxManage") # Get the path. path = ENV["VBOX_INSTALL_PATH"] || ENV["VBOX_MSI_INSTALL_PATH"] @logger.debug("VBOX_INSTALL_PATH value: #{path}") # There can actually be multiple paths in here, so we need to # split by the separator ";" and see which is a good one. path.split(";").each do |single| # Make sure it ends with a \ single += "\\" if !single.end_with?("\\") # If the executable exists, then set it as the main path # and break out vboxmanage = "#{single}VBoxManage.exe" if File.file?(vboxmanage) @vboxmanage_path = Vagrant::Util::Platform.cygwin_windows_path(vboxmanage) break end end end # If we still don't have one, try to find it using common locations drive = ENV["SYSTEMDRIVE"] || "C:" [ "#{drive}/Program Files/Oracle/VirtualBox", "#{drive}/Program Files (x86)/Oracle/VirtualBox", "#{ENV["PROGRAMFILES"]}/Oracle/VirtualBox" ].each do |maybe| path = File.join(maybe, "VBoxManage.exe") if File.file?(path) @vboxmanage_path = path break end end elsif Vagrant::Util::Platform.wsl? if !Vagrant::Util::Platform.wsl_windows_access? @logger.error("No user Windows access defined for the Windows Subsystem for Linux. This is required for VirtualBox.") raise Vagrant::Errors::WSLVirtualBoxWindowsAccessError end @logger.debug("Linux platform detected but executing within WSL. Locating VBoxManage.") @vboxmanage_path = Vagrant::Util::Which.which("VBoxManage") || Vagrant::Util::Which.which("VBoxManage.exe") if !@vboxmanage_path # If we still don't have one, try to find it using common locations drive = "/mnt/c" [ "#{drive}/Program Files/Oracle/VirtualBox", "#{drive}/Program Files (x86)/Oracle/VirtualBox" ].each do |maybe| path = File.join(maybe, "VBoxManage.exe") if File.file?(path) @vboxmanage_path = path break end end end end # Fall back to hoping for the PATH to work out @vboxmanage_path ||= "VBoxManage" @logger.info("VBoxManage path: #{@vboxmanage_path}") end # Clears the forwarded ports that have been set on the virtual machine. def clear_forwarded_ports end # Clears the shared folders that have been set on the virtual machine. def clear_shared_folders end # Creates a DHCP server for a host only network. # # @param [String] network Name of the host-only network. # @param [Hash] options Options for the DHCP server. def create_dhcp_server(network, options) end # Creates a host only network with the given options. # # @param [Hash] options Options to create the host only network. # @return [Hash] The details of the host only network, including # keys `:name`, `:ip`, and `:netmask` def create_host_only_network(options) end # Deletes the virtual machine references by this driver. def delete end # Deletes any host only networks that aren't being used for anything. def delete_unused_host_only_networks end # Discards any saved state associated with this VM. def discard_saved_state end # Enables network adapters on the VM. # # The format of each adapter specification should be like so: # # { # type: :hostonly, # hostonly: "vboxnet0", # mac_address: "tubes" # } # # This must support setting up both host only and bridged networks. # # @param [Array] adapters Array of adapters to enable. def enable_adapters(adapters) end # Execute a raw command straight through to VBoxManage. # # Accepts a retryable: true option if the command should be retried # upon failure. # # Raises a VBoxManage error if it fails. # # @param [Array] command Command to execute. def execute_command(command) end # Exports the virtual machine to the given path. # # @param [String] path Path to the OVF file. # @yield [progress] Yields the block with the progress of the export. def export(path) end # Forwards a set of ports for a VM. # # This will not affect any previously set forwarded ports, # so be sure to delete those if you need to. # # The format of each port hash should be the following: # # { # name: "foo", # hostport: 8500, # guestport: 80, # adapter: 1, # protocol: "tcp" # } # # Note that "adapter" and "protocol" are optional and will default # to 1 and "tcp" respectively. # # @param [Array] ports An array of ports to set. See documentation # for more information on the format. def forward_ports(ports) end # Halts the virtual machine (pulls the plug). def halt end # Imports the VM from an OVF file. # # @param [String] ovf Path to the OVF file. # @return [String] UUID of the imported VM. def import(ovf) end # Returns the maximum number of network adapters. def max_network_adapters 8 end # Returns a list of forwarded ports for a VM. # # @param [String] uuid UUID of the VM to read from, or `nil` if this # VM. # @param [Boolean] active_only If true, only VMs that are running will # be checked. # @return [Array] def read_forwarded_ports(uuid=nil, active_only=false) end # Returns a list of bridged interfaces. # # @return [Hash] def read_bridged_interfaces end # Returns a list of configured DHCP servers # # Each DHCP server is represented as a Hash with the following details: # # { # :network => String, # name of the associated network interface as # # parsed from the NetworkName, e.g. "vboxnet0" # :ip => String, # IP address of the DHCP server, e.g. "172.28.128.2" # :lower => String, # lower IP address of the DHCP lease range, e.g. "172.28.128.3" # :upper => String, # upper IP address of the DHCP lease range, e.g. "172.28.128.254" # } # # @return [Array] See comment above for details def read_dhcp_servers end # Returns the guest additions version that is installed on this VM. # # @return [String] def read_guest_additions_version end # Returns the value of a guest property on the current VM. # # @param [String] property the name of the guest property to read # @return [String] value of the guest property # @raise [VirtualBoxGuestPropertyNotFound] if the guest property does not have a value def read_guest_property(property) end # Returns a list of available host only interfaces. # # Each interface is represented as a Hash with the following details: # # { # :name => String, # interface name, e.g. "vboxnet0" # :ip => String, # IP address of the interface, e.g. "172.28.128.1" # :netmask => String, # netmask associated with the interface, e.g. "255.255.255.0" # :status => String, # status of the interface, e.g. "Up", "Down" # :display_name => String, # user friendly display name if available # } # # @return [Array] See comment above for details def read_host_only_interfaces end # Returns the MAC address of the first network interface. # # @return [String] def read_mac_address end # Returns the folder where VirtualBox places it's VMs. # # @return [String] def read_machine_folder end # Returns a list of network interfaces of the VM. # # @return [Hash] def read_network_interfaces end # Returns the current state of this VM. # # @return [Symbol] def read_state end # Returns a list of all forwarded ports in use by active # virtual machines. # # @return [Array] def read_used_ports end # Returns a list of all UUIDs of virtual machines currently # known by VirtualBox. # # @return [Array] 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 # with the DHCP server to be removed, e.g. "HostInterfaceNetworking-vboxnet0" def remove_dhcp_server(network_name) end # Sets the MAC address of the first network adapter. # # @param [String] mac MAC address without any spaces/hyphens. def set_mac_address(mac) end # Share a set of folders on this VM. # # @param [Array] folders def share_folders(folders) end # Reads the SSH port of this VM. # # @param [Integer] expected Expected guest port of SSH. def ssh_port(expected) end # Starts the virtual machine. # # @param [String] mode Mode to boot the VM. Either "headless" # or "gui" def start(mode) end # Suspend the virtual machine. def suspend end # Unshare folders. def unshare_folders(names) end # Verifies that the driver is ready to accept work. # # This should raise a VagrantError if things are not ready. def verify! end # Verifies that an image can be imported properly. # # @param [String] path Path to an OVF file. # @return [Boolean] def verify_image(path) end # Checks if a VM with the given UUID exists. # # @return [Boolean] def vm_exists?(uuid) end # Returns a hash of information about a given virtual machine # # @param [String] uuid # @return [Hash] info def show_vm_info info = {} execute('showvminfo', @uuid, '--machinereadable', retryable: true).split("\n").each do |line| parts = line.partition('=') key = parts.first.gsub('"', '') value = parts.last.gsub('"', '') info[key] = value end info end # Execute the given subcommand for VBoxManage and return the output. def execute(*command, &block) # Get the options hash if it exists opts = {} opts = command.pop if command.last.is_a?(Hash) tries = 0 tries = 3 if opts[:retryable] # Variable to store our execution result r = nil retryable(on: Vagrant::Errors::VBoxManageError, tries: tries, sleep: 1) do # If there is an error with VBoxManage, this gets set to true errored = false # Execute the command r = raw(*command, &block) # If the command was a failure, then raise an exception that is # nicely handled by Vagrant. if r.exit_code != 0 if @interrupted @logger.info("Exit code != 0, but interrupted. Ignoring.") elsif r.exit_code == 126 # This exit code happens if VBoxManage is on the PATH, # but another executable it tries to execute is missing. # This is usually indicative of a corrupted VirtualBox install. raise Vagrant::Errors::VBoxManageNotFoundError else errored = true end else # Sometimes, VBoxManage fails but doesn't actual return a non-zero # exit code. For this we inspect the output and determine if an error # occurred. if r.stderr =~ /failed to open \/dev\/vboxnetctl/i # This catches an error message that only shows when kernel # drivers aren't properly installed. @logger.error("Error message about unable to open vboxnetctl") raise Vagrant::Errors::VirtualBoxKernelModuleNotLoaded end if r.stderr =~ /VBoxManage([.a-z]+?): error:/ # This catches the generic VBoxManage error case. @logger.info("VBoxManage error text found, assuming error.") errored = true end end # If there was an error running VBoxManage, show the error and the # output. if errored raise Vagrant::Errors::VBoxManageError, command: command.inspect, stderr: r.stderr.to_s.force_encoding("UTF-8"), stdout: r.stdout.to_s.force_encoding("UTF-8") end end # Return the output, making sure to replace any Windows-style # newlines with Unix-style. r.stdout.gsub("\r\n", "\n") end # Executes a command and returns the raw result object. def raw(*command, &block) int_callback = lambda do @interrupted = true # We have to execute this in a thread due to trap contexts # and locks. Thread.new { @logger.info("Interrupted.") }.join end # Append in the options for subprocess # NOTE: We include the LANG env var set to C to prevent command output # from being localized command << { notify: [:stdout, :stderr], env: env_lang} Vagrant::Util::Busy.busy(int_callback) do Vagrant::Util::Subprocess.execute(@vboxmanage_path, *command, &block) end rescue Vagrant::Util::Subprocess::LaunchError => e raise Vagrant::Errors::VBoxManageLaunchError, message: e.to_s end private # List of LANG values to attempt to use LANG_VARIATIONS = %w(C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8 C POSIX).map(&:freeze).freeze # By default set the LANG to C. If the host has the locale command # available, check installed locales and verify C is included (or # use C variant if available). def env_lang # If already set, just return immediately return @env_lang if @env_lang # Default the LANG to C @env_lang = {LANG: "C"} # If the locale command is not available, return default return @env_lang if !Vagrant::Util::Which.which("locale") if defined?(@@env_lang) return @env_lang = @@env_lang end @logger.debug("validating LANG value for virtualbox cli commands") # Get list of available locales on the system result = Vagrant::Util::Subprocess.execute("locale", "-a") # If the command results in an error, just log the error # and return the default value if result.exit_code != 0 @logger.warn("locale command failed (exit code: #{result.exit_code}): #{result.stderr}") return @env_lang end available = result.stdout.lines.map(&:chomp).find_all { |l| l == "C" || l == "POSIX" || l.start_with?("C.") || l.start_with?("en_US.") } @logger.debug("list of available C locales: #{available.inspect}") # Attempt to find a valid LANG from locale list lang = LANG_VARIATIONS.detect { |l| available.include?(l) } if lang @logger.debug("valid variation found for LANG value: #{lang}") @env_lang[:LANG] = lang @@env_lang = @env_lang end @logger.debug("LANG value set: #{@env_lang[:LANG].inspect}") @env_lang end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/meta.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "forwardable" require "thread" require "log4r" require "vagrant/util/retryable" require File.expand_path("../base", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver class Meta < Base # This is raised if the VM is not found when initializing a driver # with a UUID. class VMNotFound < StandardError; end # We use forwardable to do all our driver forwarding extend Forwardable # We cache the read VirtualBox version here once we have one, # since during the execution of Vagrant, it likely doesn't change. @@version = nil @@version_lock = Mutex.new # The UUID of the virtual machine we represent attr_reader :uuid # The version of virtualbox that is running. attr_reader :version include Vagrant::Util::Retryable def initialize(uuid=nil) # Setup the base super() @logger = Log4r::Logger.new("vagrant::provider::virtualbox::meta") @uuid = uuid @@version_lock.synchronize do if !@@version # Read and assign the version of VirtualBox we know which # specific driver to instantiate. begin @@version = read_version rescue Vagrant::Errors::CommandUnavailable, Vagrant::Errors::CommandUnavailableWindows # This means that VirtualBox was not found, so we raise this # error here. raise Vagrant::Errors::VirtualBoxNotDetected end end end # Instantiate the proper version driver for VirtualBox @logger.debug("Finding driver for VirtualBox version: #{@@version}") driver_map = { "4.0" => Version_4_0, "4.1" => Version_4_1, "4.2" => Version_4_2, "4.3" => Version_4_3, "5.0" => Version_5_0, "5.1" => Version_5_1, "5.2" => Version_5_2, "6.0" => Version_6_0, "6.1" => Version_6_1, "7.0" => Version_7_0, "7.1" => Version_7_1, "7.2" => Version_7_2, } if @@version.start_with?("4.2.14") # VirtualBox 4.2.14 just doesn't work with Vagrant, so show error raise Vagrant::Errors::VirtualBoxBrokenVersion040214 end driver_klass = nil driver_map.each do |key, klass| if @@version.start_with?(key) driver_klass = klass break end end if !driver_klass supported_versions = driver_map.keys.sort.join(", ") raise Vagrant::Errors::VirtualBoxInvalidVersion, supported_versions: supported_versions end @logger.info("Using VirtualBox driver: #{driver_klass}") @driver = driver_klass.new(@uuid) @version = @@version if @uuid # Verify the VM exists, and if it doesn't, then don't worry # about it (mark the UUID as nil) raise VMNotFound if !@driver.vm_exists?(@uuid) end end def_delegators :@driver, :attach_disk, :clear_forwarded_ports, :clear_shared_folders, :clone_disk, :clonevm, :close_medium, :create_dhcp_server, :create_disk, :create_host_only_network, :create_snapshot, :delete, :delete_snapshot, :delete_unused_host_only_networks, :discard_saved_state, :enable_adapters, :execute_command, :export, :forward_ports, :get_port_and_device, :get_storage_controller, :halt, :import, :list_snapshots, :list_hdds, :read_forwarded_ports, :read_bridged_interfaces, :read_dhcp_servers, :read_guest_additions_version, :read_guest_ip, :read_guest_property, :read_host_only_interfaces, :read_host_only_networks, :read_mac_address, :read_mac_addresses, :read_machine_folder, :read_network_interfaces, :read_state, :read_storage_controllers, :read_used_ports, :read_vms, :reconfig_host_only, :remove_dhcp_server, :remove_disk, :resize_disk, :restore_snapshot, :resume, :set_mac_address, :set_name, :share_folders, :show_medium_info, :ssh_port, :start, :suspend, :vdi_to_vmdk, :verify!, :verify_image, :vm_exists?, :vmdk_to_vdi protected # This returns the version of VirtualBox that is running. # # @return [String] def read_version # The version string is usually in one of the following formats: # # * 4.1.8r1234 # * 4.1.8r1234_OSE # * 4.1.8_MacPortsr1234 # # Below accounts for all of these. # Note: We split this into multiple lines because apparently "".split("_") # is [], so we have to check for an empty array in between. output = "" retryable(on: Vagrant::Errors::VirtualBoxVersionEmpty, tries: 3, sleep: 1) do output = execute("--version") if output =~ /vboxdrv kernel module is not loaded/ || output =~ /VirtualBox kernel modules are not loaded/i raise Vagrant::Errors::VirtualBoxKernelModuleNotLoaded elsif output =~ /Please install/ # Check for installation incomplete warnings, for example: # "WARNING: The character device /dev/vboxdrv does not # exist. Please install the virtualbox-ose-dkms package and # the appropriate headers, most likely linux-headers-generic." raise Vagrant::Errors::VirtualBoxInstallIncomplete elsif output.chomp == "" # This seems to happen on Windows for uncertain reasons. # Raise an error otherwise the error is that they have an # incompatible version of VirtualBox which isn't true. raise Vagrant::Errors::VirtualBoxVersionEmpty, vboxmanage: @vboxmanage_path.to_s end end version_line = output.each_line.find do |line| !line.start_with?("WARNING:") end parts = version_line.to_s.split("_") return nil if parts.empty? parts[0].split("r")[0] end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_4_0.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' require "vagrant/util/platform" require File.expand_path("../base", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 4.0.x class Version_4_0 < Base def initialize(uuid) super() @logger = Log4r::Logger.new("vagrant::provider::virtualbox_4_0") @uuid = uuid end def clear_forwarded_ports args = [] read_forwarded_ports(@uuid).each do |nic, name, _, _| args.concat(["--natpf#{nic}", "delete", name]) end execute("modifyvm", @uuid, *args) if !args.empty? end def clear_shared_folders info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if name = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] execute("sharedfolder", "remove", @uuid, "--name", name) end end end def create_dhcp_server(network, options) execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], "--netmask", options[:netmask], "--lowerip", options[:dhcp_lower], "--upperip", options[:dhcp_upper], "--enable") end def create_host_only_network(options) # Create the interface interface = execute("hostonlyif", "create") name = interface[/^Interface '(.+?)' was successfully created$/, 1] # 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 { name: name, ip: options[:adapter_ip], netmask: options[:netmask], dhcp: nil } end def delete execute("unregistervm", @uuid, "--delete") end def delete_unused_host_only_networks networks = [] execute("list", "hostonlyifs").split("\n").each do |line| if network_name = line[/^Name:\s+(.+?)$/, 1] networks << network_name end end execute("list", "vms").split("\n").each do |line| if vm_name = line[/^".+?"\s+\{(.+?)\}$/, 1] begin info = execute("showvminfo", vm_name, "--machinereadable", retryable: true) info.split("\n").each do |line| if network_name = line[/^hostonlyadapter\d+="(.+?)"$/, 1] networks.delete(network_name) end end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end networks.each do |name| # First try to remove any DHCP servers attached. We use `raw` because # it is okay if this fails. It usually means that a DHCP server was # never attached. raw("dhcpserver", "remove", "--ifname", name) # Delete the actual host only network interface. execute("hostonlyif", "remove", name) end end def discard_saved_state execute("discardstate", @uuid) end def enable_adapters(adapters) args = [] adapters.each do |adapter| args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) if adapter[:bridge] args.concat(["--bridgeadapter#{adapter[:adapter]}", adapter[:bridge], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:hostonly] args.concat(["--hostonlyadapter#{adapter[:adapter]}", adapter[:hostonly]]) end if adapter[:mac_address] args.concat(["--macaddress#{adapter[:adapter]}", adapter[:mac_address]]) end if adapter[:nic_type] args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) end end execute("modifyvm", @uuid, *args) end def execute_command(command) execute(*command) end def export(path) execute("export", @uuid, "--output", path.to_s) end def forward_ports(ports) args = [] ports.each do |options| pf_builder = [options[:name], options[:protocol] || "tcp", options[:hostip] || "", options[:hostport], options[:guestip] || "", options[:guestport]] args.concat(["--natpf#{options[:adapter] || 1}", pf_builder.join(",")]) end execute("modifyvm", @uuid, *args) if !args.empty? end def halt execute("controlvm", @uuid, "poweroff") end def import(ovf) ovf = Vagrant::Util::Platform.cygwin_windows_path(ovf) output = "" total = "" last = 0 execute("import", ovf) do |type, data| if type == :stdout # Keep track of the stdout so that we can get the VM name output << data elsif type == :stderr # Append the data so we can see the full view total << data # Break up the lines. We can't get the progress until we see an "OK" lines = total.split("\n") if lines.include?("OK.") # The progress of the import will be in the last line. Do a greedy # regular expression to find what we're looking for. if current = lines.last[/.+(\d{2})%/, 1] current = current.to_i if current > last last = current yield current if block_given? end end end end end # Find the name of the VM name name = output[/Suggested VM name "(.+?)"/, 1] if !name @logger.error("Couldn't find VM name in the output.") return nil end output = execute("list", "vms") if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] return existing_vm end nil end def read_forwarded_ports(uuid=nil, active_only=false) uuid ||= @uuid @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") results = [] current_nic = nil info = execute("showvminfo", uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| # This is how we find the nic that a FP is attached to, # since this comes first. if nic = line[/^nic(\d+)=".+?"$/, 1] current_nic = nic.to_i end # If we care about active VMs only, then we check the state # to verify the VM is running. if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") return [] end # Parse out the forwarded port information if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] @logger.debug(" - #{result.inspect}") results << result end end results end def read_bridged_interfaces execute("list", "bridgedifs").split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if name = line[/^Name:\s+(.+?)$/, 1] info[:name] = name elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask elsif status = line[/^Status:\s+(.+?)$/, 1] info[:status] = status end end # Return the info to build up the results info end end def read_dhcp_servers execute("list", "dhcpservers", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] info[:network] = network info[:network_name] = "HostInterfaceNetworking-#{network}" elsif ip = line[/^IP:\s+(.+?)$/, 1] info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] info[:lower] = lower elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] info[:upper] = upper end end info end end def read_guest_additions_version output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", retryable: true) if value = output[/^Value: (.+?)$/, 1] # Split the version by _ since some distro versions modify it # to look like this: 4.1.2_ubuntu, and the distro part isn't # too important. return value.split("_").first end return nil end def read_guest_ip(adapter_number) ip = read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") if !valid_ip_address?(ip) raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: "/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP" end return ip end def read_guest_property(property) output = execute("guestproperty", "get", @uuid, property) if output =~ /^Value: (.+?)$/ $1.to_s else raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: property end end def read_host_only_interfaces execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if name = line[/^Name:\s+(.+?)$/, 1] info[:name] = name elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] 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 elsif line =~ /^VBoxNetworkName:\s+(.+?)$/ info[:display_name] = $1.to_s end end info end end def read_mac_address info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if mac = line[/^macaddress1="(.+?)"$/, 1] return mac end end nil end def read_mac_addresses macs = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i mac = matcher[2].to_s macs[adapter] = mac end end macs end def read_machine_folder execute("list", "systemproperties", retryable: true).split("\n").each do |line| if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] return folder end end nil end def read_network_interfaces nics = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if matcher = /^nic(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i type = matcher[2].to_sym nics[adapter] ||= {} nics[adapter][:type] = type elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i network = matcher[2].to_s nics[adapter] ||= {} nics[adapter][:hostonly] = network elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i network = matcher[2].to_s nics[adapter] ||= {} nics[adapter][:bridge] = network end end nics end def read_state output = execute("showvminfo", @uuid, "--machinereadable", retryable: true) if output =~ /^name=""$/ return :inaccessible elsif state = output[/^VMState="(.+?)"$/, 1] return state.to_sym end nil end def read_used_ports ports = [] execute("list", "vms", retryable: true).split("\n").each do |line| if uuid = line[/^".+?" \{(.+?)\}$/, 1] # Ignore our own used ports next if uuid == @uuid begin read_forwarded_ports(uuid, true).each do |_, _, hostport, _| ports << hostport end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end ports end def read_vms results = {} execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^"(.+?)" \{(.+?)\}$/ results[$1.to_s] = $2.to_s end end 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 def set_mac_address(mac) execute("modifyvm", @uuid, "--macaddress1", mac) end def set_name(name) execute("modifyvm", @uuid, "--name", name) end def share_folders(folders) folders.each do |folder| args = ["--name", folder[:name], "--hostpath", folder[:hostpath]] args << "--transient" if folder.key?(:transient) && folder[:transient] execute("sharedfolder", "add", @uuid, *args) end end def ssh_port(expected_port) @logger.debug("Searching for SSH port: #{expected_port.inspect}") # Look for the forwarded port only by comparing the guest port read_forwarded_ports.each do |_, _, hostport, guestport| return hostport if guestport == expected_port end nil end def resume @logger.debug("Resuming paused VM...") execute("controlvm", @uuid, "resume") end def start(mode) command = ["startvm", @uuid, "--type", mode.to_s] r = raw(*command) if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ # Some systems return an exit code 1 for some reason. For that # we depend on the output. return true end # If we reached this point then it didn't work out. raise Vagrant::Errors::VBoxManageError, command: command.inspect, stderr: r.stderr end def suspend execute("controlvm", @uuid, "savestate") end def unshare_folders(names) names.each do |name| begin execute( "sharedfolder", "remove", @uuid, "--name", name, "--transient") execute( "setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{name}") rescue Vagrant::Errors::VBoxManageError => e if e.extra_data[:stderr].include?("VBOX_E_FILE_ERROR") # The folder doesn't exist. ignore. else raise end end end end def valid_ip_address?(ip) # Filter out invalid IP addresses # GH-4658 VirtualBox can report an IP address of 0.0.0.0 for FreeBSD guests. if ip == "0.0.0.0" return false else return true end end def verify! # This command sometimes fails if kernel drivers aren't properly loaded # so we just run the command and verify that it succeeded. execute("list", "hostonlyifs") end def verify_image(path) r = raw("import", path.to_s, "--dry-run") return r.exit_code == 0 end def vm_exists?(uuid) raw("showvminfo", uuid).exit_code == 0 end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_4_1.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' require "vagrant/util/platform" require File.expand_path("../base", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 4.1.x class Version_4_1 < Base def initialize(uuid) super() @logger = Log4r::Logger.new("vagrant::provider::virtualbox_4_1") @uuid = uuid end def clear_forwarded_ports args = [] read_forwarded_ports(@uuid).each do |nic, name, _, _| args.concat(["--natpf#{nic}", "delete", name]) end execute("modifyvm", @uuid, *args) if !args.empty? end def clear_shared_folders info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if folder = line[/^SharedFolderNameMachineMapping\d+="(.+?)"$/, 1] execute("sharedfolder", "remove", @uuid, "--name", folder) end end end 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 execute("clonevm", master_id, *args) return get_machine_id(machine_name) end def create_dhcp_server(network, options) execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], "--netmask", options[:netmask], "--lowerip", options[:dhcp_lower], "--upperip", options[:dhcp_upper], "--enable") end def create_host_only_network(options) # Create the interface interface = execute("hostonlyif", "create") name = interface[/^Interface '(.+?)' was successfully created$/, 1] # 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 { name: name, ip: options[:adapter_ip], netmask: options[:netmask], dhcp: nil } 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 d = e.extra_data return [] if d[:stderr].include?("does not have") || d[: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 def delete_unused_host_only_networks networks = [] execute("list", "hostonlyifs").split("\n").each do |line| if network = line[/^Name:\s+(.+?)$/, 1] networks << network end end execute("list", "vms").split("\n").each do |line| if vm = line[/^".+?"\s+\{(.+?)\}$/, 1] begin info = execute("showvminfo", vm, "--machinereadable", retryable: true) info.split("\n").each do |line| if adapter = line[/^hostonlyadapter\d+="(.+?)"$/, 1] networks.delete(adapter) end end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end networks.each do |name| # First try to remove any DHCP servers attached. We use `raw` because # it is okay if this fails. It usually means that a DHCP server was # never attached. raw("dhcpserver", "remove", "--ifname", name) # Delete the actual host only network interface. execute("hostonlyif", "remove", name) end end def discard_saved_state execute("discardstate", @uuid) end def enable_adapters(adapters) args = [] adapters.each do |adapter| args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) if adapter[:bridge] args.concat(["--bridgeadapter#{adapter[:adapter]}", adapter[:bridge], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:hostonly] args.concat(["--hostonlyadapter#{adapter[:adapter]}", adapter[:hostonly]]) end if adapter[:intnet] args.concat(["--intnet#{adapter[:adapter]}", adapter[:intnet]]) end if adapter[:mac_address] args.concat(["--macaddress#{adapter[:adapter]}", adapter[:mac_address]]) end if adapter[:nic_type] args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) end end execute("modifyvm", @uuid, *args) end def execute_command(command) execute(*command) end def export(path) execute("export", @uuid, "--output", path.to_s) end def forward_ports(ports) args = [] ports.each do |options| pf_builder = [options[:name], options[:protocol] || "tcp", options[:hostip] || "", options[:hostport], options[:guestip] || "", options[:guestport]] args.concat(["--natpf#{options[:adapter] || 1}", pf_builder.join(",")]) end 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 def import(ovf) ovf = Vagrant::Util::Platform.cygwin_windows_path(ovf) output = "" total = "" last = 0 execute("import", ovf) do |type, data| if type == :stdout # Keep track of the stdout so that we can get the VM name output << data elsif type == :stderr # Append the data so we can see the full view total << data # Break up the lines. We can't get the progress until we see an "OK" lines = total.split("\n") if lines.include?("OK.") # The progress of the import will be in the last line. Do a greedy # regular expression to find what we're looking for. if current = lines.last[/.+(\d{2})%/, 1] current = current.to_i if current > last last = current yield current if block_given? end end end end end # Find the name of the VM name name = output[/Suggested VM name "(.+?)"/, 1] if !name @logger.error("Couldn't find VM name in the output.") return nil end output = execute("list", "vms") if existing_vm = output[/^"#{Regexp.escape(name)}" \{(.+?)\}$/, 1] return existing_vm end nil end def read_forwarded_ports(uuid=nil, active_only=false) uuid ||= @uuid @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") results = [] current_nic = nil info = execute("showvminfo", uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| # This is how we find the nic that a FP is attached to, # since this comes first. if nic = line[/^nic(\d+)=".+?"$/, 1] current_nic = nic.to_i end # If we care about active VMs only, then we check the state # to verify the VM is running. if active_only && (state = line[/^VMState="(.+?)"$/, 1] and state != "running") return [] end # Parse out the forwarded port information if matcher = /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/.match(line) result = [current_nic, matcher[1], matcher[2].to_i, matcher[3].to_i] @logger.debug(" - #{result.inspect}") results << result end end results end def read_bridged_interfaces execute("list", "bridgedifs").split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if name = line[/^Name:\s+(.+?)$/, 1] info[:name] = name elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask elsif status = line[/^Status:\s+(.+?)$/, 1] info[:status] = status end end # Return the info to build up the results info end end def read_dhcp_servers execute("list", "dhcpservers", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] info[:network] = network info[:network_name] = "HostInterfaceNetworking-#{network}" elsif ip = line[/^IP:\s+(.+?)$/, 1] info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] info[:lower] = lower elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] info[:upper] = upper end end info end end def read_guest_additions_version output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", retryable: true) if value = output[/^Value: (.+?)$/, 1] # Split the version by _ since some distro versions modify it # to look like this: 4.1.2_ubuntu, and the distro part isn't # too important. return value.split("_").first end return nil end def read_guest_ip(adapter_number) ip = read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") if !valid_ip_address?(ip) raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: "/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP" end return ip end def read_guest_property(property) output = execute("guestproperty", "get", @uuid, property) if output =~ /^Value: (.+?)$/ $1.to_s else raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: property end end def read_host_only_interfaces execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if name = line[/^Name:\s+(.+?)$/, 1] info[:name] = name elsif ip = line[/^IPAddress:\s+(.+?)$/, 1] 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 elsif line =~ /^VBoxNetworkName:\s+(.+?)$/ info[:display_name] = $1.to_s end end info end end def read_mac_address info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if mac = line[/^macaddress1="(.+?)"$/, 1] return mac end end nil end def read_mac_addresses macs = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i mac = matcher[2].to_s macs[adapter] = mac end end macs end def read_machine_folder execute("list", "systemproperties", retryable: true).split("\n").each do |line| if folder = line[/^Default machine folder:\s+(.+?)$/i, 1] return folder end end nil end def read_network_interfaces nics = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if matcher = /^nic(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i type = matcher[2].to_sym nics[adapter] ||= {} nics[adapter][:type] = type elsif matcher = /^hostonlyadapter(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i network = matcher[2].to_s nics[adapter] ||= {} nics[adapter][:hostonly] = network elsif matcher = /^bridgeadapter(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i network = matcher[2].to_s nics[adapter] ||= {} nics[adapter][:bridge] = network end end nics end def read_state output = execute("showvminfo", @uuid, "--machinereadable", retryable: true) if output =~ /^name=""$/ return :inaccessible elsif state = output[/^VMState="(.+?)"$/, 1] return state.to_sym end nil end def read_used_ports ports = [] execute("list", "vms", retryable: true).split("\n").each do |line| if uuid = line[/^".+?" \{(.+?)\}$/, 1] # Ignore our own used ports next if uuid == @uuid begin read_forwarded_ports(uuid, true).each do |_, _, hostport, _| ports << hostport end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end ports end def read_vms results = {} execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^"(.+?)" \{(.+?)\}$/ results[$1.to_s] = $2.to_s end end 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 def set_mac_address(mac) execute("modifyvm", @uuid, "--macaddress1", mac) end def set_name(name) execute("modifyvm", @uuid, "--name", name) end def share_folders(folders) folders.each do |folder| args = ["--name", folder[:name], "--hostpath", folder[:hostpath]] args << "--transient" if folder.key?(:transient) && folder[:transient] if folder[:SharedFoldersEnableSymlinksCreate] # Enable symlinks on the shared folder execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1") end # Add the shared folder execute("sharedfolder", "add", @uuid, *args) end end def ssh_port(expected_port) @logger.debug("Searching for SSH port: #{expected_port.inspect}") # Look for the forwarded port only by comparing the guest port read_forwarded_ports.each do |_, _, hostport, guestport| return hostport if guestport == expected_port end nil end def resume @logger.debug("Resuming paused VM...") execute("controlvm", @uuid, "resume") end def start(mode) command = ["startvm", @uuid, "--type", mode.to_s] r = raw(*command) if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ # Some systems return an exit code 1 for some reason. For that # we depend on the output. return true end # If we reached this point then it didn't work out. raise Vagrant::Errors::VBoxManageError, command: command.inspect, stderr: r.stderr end def suspend execute("controlvm", @uuid, "savestate") end def unshare_folders(names) names.each do |name| begin execute( "sharedfolder", "remove", @uuid, "--name", name, "--transient") execute( "setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{name}") rescue Vagrant::Errors::VBoxManageError => e if e.extra_data[:stderr].include?("VBOX_E_FILE_ERROR") # The folder doesn't exist. ignore. else raise end end end end def valid_ip_address?(ip) # Filter out invalid IP addresses # GH-4658 VirtualBox can report an IP address of 0.0.0.0 for FreeBSD guests. if ip == "0.0.0.0" return false else return true end end def verify! # This command sometimes fails if kernel drivers aren't properly loaded # so we just run the command and verify that it succeeded. execute("list", "hostonlyifs") end def verify_image(path) r = raw("import", path.to_s, "--dry-run") return r.exit_code == 0 end def vm_exists?(uuid) 5.times do |i| result = raw("showvminfo", uuid) return true if result.exit_code == 0 # If vboxmanage returned VBOX_E_OBJECT_NOT_FOUND, # then the vm truly does not exist. Any other error might be transient return false if result.stderr.include?("VBOX_E_OBJECT_NOT_FOUND") # Sleep a bit though to give VirtualBox time to fix itself sleep 2 end # If we reach this point, it means that we consistently got the # failure, do a standard vboxmanage now. This will raise an # exception if it fails again. execute("showvminfo", uuid) return true end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_4_2.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' require "vagrant/util/platform" require File.expand_path("../base", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 4.2.x class Version_4_2 < Base def initialize(uuid) super() @logger = Log4r::Logger.new("vagrant::provider::virtualbox_4_2") @uuid = uuid end def clear_forwarded_ports args = [] read_forwarded_ports(@uuid).each do |nic, name, _, _| args.concat(["--natpf#{nic}", "delete", name]) end execute("modifyvm", @uuid, *args) if !args.empty? end def clear_shared_folders info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if line =~ /^SharedFolderNameMachineMapping\d+="(.+?)"$/ execute("sharedfolder", "remove", @uuid, "--name", $1.to_s) end end end def create_dhcp_server(network, options) execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], "--netmask", options[:netmask], "--lowerip", options[:dhcp_lower], "--upperip", options[:dhcp_upper], "--enable") end def create_host_only_network(options) # Create the interface execute("hostonlyif", "create") =~ /^Interface '(.+?)' was successfully created$/ name = $1.to_s # 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 { name: name, ip: options[:adapter_ip], netmask: options[:netmask], dhcp: nil } end def delete execute("unregistervm", @uuid, "--delete") end def delete_unused_host_only_networks networks = [] execute("list", "hostonlyifs").split("\n").each do |line| networks << $1.to_s if line =~ /^Name:\s+(.+?)$/ end execute("list", "vms").split("\n").each do |line| if line =~ /^".+?"\s+\{(.+?)\}$/ begin info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) info.split("\n").each do |inner_line| if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ networks.delete($1.to_s) end end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end networks.each do |name| # First try to remove any DHCP servers attached. We use `raw` because # it is okay if this fails. It usually means that a DHCP server was # never attached. raw("dhcpserver", "remove", "--ifname", name) # Delete the actual host only network interface. execute("hostonlyif", "remove", name) end end def discard_saved_state execute("discardstate", @uuid) end def enable_adapters(adapters) args = [] adapters.each do |adapter| args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) if adapter[:bridge] args.concat(["--bridgeadapter#{adapter[:adapter]}", adapter[:bridge], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:hostonly] args.concat(["--hostonlyadapter#{adapter[:adapter]}", adapter[:hostonly], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:intnet] args.concat(["--intnet#{adapter[:adapter]}", adapter[:intnet], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:mac_address] args.concat(["--macaddress#{adapter[:adapter]}", adapter[:mac_address]]) end if adapter[:nic_type] args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) end end execute("modifyvm", @uuid, *args) end def execute_command(command) execute(*command) end def export(path) execute("export", @uuid, "--output", path.to_s) end def forward_ports(ports) args = [] ports.each do |options| pf_builder = [options[:name], options[:protocol] || "tcp", options[:hostip] || "", options[:hostport], options[:guestip] || "", options[:guestport]] args.concat(["--natpf#{options[:adapter] || 1}", pf_builder.join(",")]) end execute("modifyvm", @uuid, *args) if !args.empty? end def halt execute("controlvm", @uuid, "poweroff") end def import(ovf) ovf = Vagrant::Util::Platform.cygwin_windows_path(ovf) output = "" total = "" last = 0 output = execute("import", "-n", ovf) output =~ /Suggested VM name "(.+?)"/ suggested_name = $1.to_s specified_name = "#{suggested_name}_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" #Millisecond + Random #Build the specified name param list name_params = Array.new name_params << "--vsys" << "0" << "--vmname" << specified_name #Extract the disks list and build the disk target params disk_params = Array.new disks = output.scan(/(\d+): Hard disk image: source image=.+, target path=(.+),/) disks.each do |unit_num, path| disk_params << "--vsys" disk_params << "0" #Derive vsys num .. do we support OVF's with multiple machines? disk_params << "--unit" disk_params << unit_num disk_params << "--disk" if Vagrant::Util::Platform.windows? # we use the block form of sub here to ensure that if the specified_name happens to end with a number (which is fairly likely) then # we won't end up having the character sequence of a \ followed by a number be interpreted as a back reference. For example, if # specified_name were "abc123", then "\\abc123\\".reverse would be "\\321cba\\", and the \3 would be treated as a back reference by the sub disk_params << path.reverse.sub("\\#{suggested_name}\\".reverse) { "\\#{specified_name}\\".reverse }.reverse # Replace only last occurrence else disk_params << path.reverse.sub("/#{suggested_name}/".reverse, "/#{specified_name}/".reverse).reverse # Replace only last occurrence end end execute("import", ovf , *name_params, *disk_params) do |type, data| if type == :stdout # Keep track of the stdout so that we can get the VM name output << data elsif type == :stderr # Append the data so we can see the full view total << data # Break up the lines. We can't get the progress until we see an "OK" lines = total.split("\n") if lines.include?("OK.") # The progress of the import will be in the last line. Do a greedy # regular expression to find what we're looking for. if lines.last =~ /.+(\d{2})%/ current = $1.to_i if current > last last = current yield current if block_given? end end end end end output = execute("list", "vms") if output =~ /^"#{Regexp.escape(specified_name)}" \{(.+?)\}$/ return $1.to_s end nil end def max_network_adapters 8 end def read_forwarded_ports(uuid=nil, active_only=false) uuid ||= @uuid @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") results = [] current_nic = nil info = execute("showvminfo", uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| # This is how we find the nic that a FP is attached to, # since this comes first. current_nic = $1.to_i if line =~ /^nic(\d+)=".+?"$/ # If we care about active VMs only, then we check the state # to verify the VM is running. if active_only && line =~ /^VMState="(.+?)"$/ && $1.to_s != "running" return [] end # Parse out the forwarded port information if line =~ /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/ result = [current_nic, $1.to_s, $2.to_i, $3.to_i] @logger.debug(" - #{result.inspect}") results << result end end results end def read_bridged_interfaces execute("list", "bridgedifs").split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if line =~ /^Name:\s+(.+?)$/ info[:name] = $1.to_s elsif line =~ /^IPAddress:\s+(.+?)$/ info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end end # Return the info to build up the results info end end def read_dhcp_servers execute("list", "dhcpservers", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] info[:network] = network info[:network_name] = "HostInterfaceNetworking-#{network}" elsif ip = line[/^IP:\s+(.+?)$/, 1] info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] info[:lower] = lower elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] info[:upper] = upper end end info end end def read_guest_additions_version output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", retryable: true) if output =~ /^Value: (.+?)$/ # Split the version by _ since some distro versions modify it # to look like this: 4.1.2_ubuntu, and the distro part isn't # too important. value = $1.to_s return value.split("_").first end # If we can't get the guest additions version by guest property, try # to get it from the VM info itself. info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| return $1.to_s if line =~ /^GuestAdditionsVersion="(.+?)"$/ end return nil end def read_guest_ip(adapter_number) ip = read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") if !valid_ip_address?(ip) raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: "/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP" end return ip end def read_guest_property(property) output = execute("guestproperty", "get", @uuid, property) if output =~ /^Value: (.+?)$/ $1.to_s else raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: property end end def read_host_only_interfaces execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if line =~ /^Name:\s+(.+?)$/ info[:name] = $1.to_s elsif line =~ /^IPAddress:\s+(.+?)$/ 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 elsif line =~ /^VBoxNetworkName:\s+(.+?)$/ info[:display_name] = $1.to_s end end info end end def read_mac_address info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| return $1.to_s if line =~ /^macaddress1="(.+?)"$/ end nil end def read_mac_addresses macs = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i mac = matcher[2].to_s macs[adapter] = mac end end macs end def read_machine_folder execute("list", "systemproperties", retryable: true).split("\n").each do |line| if line =~ /^Default machine folder:\s+(.+?)$/i return $1.to_s end end nil end def read_network_interfaces nics = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if line =~ /^nic(\d+)="(.+?)"$/ adapter = $1.to_i type = $2.to_sym nics[adapter] ||= {} nics[adapter][:type] = type elsif line =~ /^hostonlyadapter(\d+)="(.+?)"$/ adapter = $1.to_i network = $2.to_s nics[adapter] ||= {} nics[adapter][:hostonly] = network elsif line =~ /^bridgeadapter(\d+)="(.+?)"$/ adapter = $1.to_i network = $2.to_s nics[adapter] ||= {} nics[adapter][:bridge] = network end end nics end def read_state output = execute("showvminfo", @uuid, "--machinereadable", retryable: true) if output =~ /^name=""$/ return :inaccessible elsif output =~ /^VMState="(.+?)"$/ return $1.to_sym end nil end def read_used_ports ports = [] execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^".+?" \{(.+?)\}$/ uuid = $1.to_s # Ignore our own used ports next if uuid == @uuid begin read_forwarded_ports(uuid, true).each do |_, _, hostport, _| ports << hostport end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end ports end def read_vms results = {} execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^"(.+?)" \{(.+?)\}$/ results[$1.to_s] = $2.to_s end end 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 def set_mac_address(mac) execute("modifyvm", @uuid, "--macaddress1", mac) end def set_name(name) execute("modifyvm", @uuid, "--name", name, retryable: true) end def share_folders(folders) folders.each do |folder| args = ["--name", folder[:name], "--hostpath", folder[:hostpath]] args << "--transient" if folder.key?(:transient) && folder[:transient] if folder[:SharedFoldersEnableSymlinksCreate] # Enable symlinks on the shared folder execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1") end # Add the shared folder execute("sharedfolder", "add", @uuid, *args) end end def ssh_port(expected_port) @logger.debug("Searching for SSH port: #{expected_port.inspect}") # Look for the forwarded port only by comparing the guest port read_forwarded_ports.each do |_, _, hostport, guestport| return hostport if guestport == expected_port end nil end def resume @logger.debug("Resuming paused VM...") execute("controlvm", @uuid, "resume") end def start(mode) command = ["startvm", @uuid, "--type", mode.to_s] r = raw(*command) if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ # Some systems return an exit code 1 for some reason. For that # we depend on the output. return true end # If we reached this point then it didn't work out. raise Vagrant::Errors::VBoxManageError, command: command.inspect, stderr: r.stderr end def suspend execute("controlvm", @uuid, "savestate") end def unshare_folders(names) names.each do |name| begin execute( "sharedfolder", "remove", @uuid, "--name", name, "--transient") execute( "setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{name}") rescue Vagrant::Errors::VBoxManageError => e if e.extra_data[:stderr].include?("VBOX_E_FILE_ERROR") # The folder doesn't exist. ignore. else raise end end end end def valid_ip_address?(ip) # Filter out invalid IP addresses # GH-4658 VirtualBox can report an IP address of 0.0.0.0 for FreeBSD guests. if ip == "0.0.0.0" return false else return true end end def verify! # This command sometimes fails if kernel drivers aren't properly loaded # so we just run the command and verify that it succeeded. execute("list", "hostonlyifs") end def verify_image(path) r = raw("import", path.to_s, "--dry-run") return r.exit_code == 0 end def vm_exists?(uuid) 5.times do |i| result = raw("showvminfo", uuid) return true if result.exit_code == 0 # If vboxmanage returned VBOX_E_OBJECT_NOT_FOUND, # then the vm truly does not exist. Any other error might be transient return false if result.stderr.include?("VBOX_E_OBJECT_NOT_FOUND") # Sleep a bit though to give VirtualBox time to fix itself sleep 2 end # If we reach this point, it means that we consistently got the # failure, do a standard vboxmanage now. This will raise an # exception if it fails again. 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 d = e.extra_data return [] if d[:stderr].include?("does not have") || d[: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 end ================================================ FILE: plugins/providers/virtualbox/driver/version_4_3.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'ipaddr' require 'log4r' require "vagrant/util/platform" require File.expand_path("../base", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 4.3.x class Version_4_3 < Base def initialize(uuid) super() @logger = Log4r::Logger.new("vagrant::provider::virtualbox_4_3") @uuid = uuid end def clear_forwarded_ports args = [] read_forwarded_ports(@uuid).each do |nic, name, _, _| args.concat(["--natpf#{nic}", "delete", name]) end execute("modifyvm", @uuid, *args) if !args.empty? end def clear_shared_folders info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if line =~ /^SharedFolderNameMachineMapping\d+="(.+?)"$/ execute("sharedfolder", "remove", @uuid, "--name", $1.to_s) end end end 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 execute("clonevm", master_id, *args) return get_machine_id(machine_name) end def create_dhcp_server(network, options) execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], "--netmask", options[:netmask], "--lowerip", options[:dhcp_lower], "--upperip", options[:dhcp_upper], "--enable") end def create_host_only_network(options) # Create the interface execute("hostonlyif", "create") =~ /^Interface '(.+?)' was successfully created$/ name = $1.to_s # 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 { name: name, ip: options[:adapter_ip], netmask: options[:netmask], dhcp: nil } end def reconfig_host_only(interface) execute("hostonlyif", "ipconfig", interface[:name], "--ipv6", interface[:ipv6]) 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 d = e.extra_data return [] if d[:stderr].include?("does not have") || d[: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 def delete_unused_host_only_networks networks = [] execute("list", "hostonlyifs", retryable: true).split("\n").each do |line| networks << $1.to_s if line =~ /^Name:\s+(.+?)$/ end execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^".+?"\s+\{(.+?)\}$/ begin info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) info.split("\n").each do |inner_line| if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ networks.delete($1.to_s) end end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end networks.each do |name| # First try to remove any DHCP servers attached. We use `raw` because # it is okay if this fails. It usually means that a DHCP server was # never attached. raw("dhcpserver", "remove", "--ifname", name) # Delete the actual host only network interface. execute("hostonlyif", "remove", name) end end def discard_saved_state execute("discardstate", @uuid) end def enable_adapters(adapters) args = [] adapters.each do |adapter| args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) if adapter[:bridge] args.concat(["--bridgeadapter#{adapter[:adapter]}", adapter[:bridge], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:hostonly] args.concat(["--hostonlyadapter#{adapter[:adapter]}", adapter[:hostonly], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:intnet] args.concat(["--intnet#{adapter[:adapter]}", adapter[:intnet], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:mac_address] args.concat(["--macaddress#{adapter[:adapter]}", adapter[:mac_address]]) end if adapter[:nic_type] args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) end end execute("modifyvm", @uuid, *args) end def execute_command(command) execute(*command) end def export(path) execute("export", @uuid, "--output", path.to_s) end def forward_ports(ports) args = [] ports.each do |options| pf_builder = [options[:name], options[:protocol] || "tcp", options[:hostip] || "", options[:hostport], options[:guestip] || "", options[:guestport]] args.concat(["--natpf#{options[:adapter] || 1}", pf_builder.join(",")]) end 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 def import(ovf) ovf = Vagrant::Util::Platform.cygwin_windows_path(ovf) output = "" total = "" last = 0 # Dry-run the import to get the suggested name and path @logger.debug("Doing dry-run import to determine parallel-safe name...") output = execute("import", "-n", ovf) result = /Suggested VM name "(.+?)"/.match(output) if !result raise Vagrant::Errors::VirtualBoxNoName, output: output end suggested_name = result[1].to_s # Append millisecond plus a random to the path in case we're # importing the same box elsewhere. specified_name = "#{suggested_name}_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" @logger.debug("-- Parallel safe name: #{specified_name}") # Build the specified name param list name_params = [ "--vsys", "0", "--vmname", specified_name, ] # Extract the disks list and build the disk target params disk_params = [] disks = output.scan(/(\d+): Hard disk image: source image=.+, target path=(.+),/) disks.each do |unit_num, path| disk_params << "--vsys" disk_params << "0" disk_params << "--unit" disk_params << unit_num disk_params << "--disk" if Vagrant::Util::Platform.windows? # we use the block form of sub here to ensure that if the specified_name happens to end with a number (which is fairly likely) then # we won't end up having the character sequence of a \ followed by a number be interpreted as a back reference. For example, if # specified_name were "abc123", then "\\abc123\\".reverse would be "\\321cba\\", and the \3 would be treated as a back reference by the sub disk_params << path.reverse.sub("\\#{suggested_name}\\".reverse) { "\\#{specified_name}\\".reverse }.reverse # Replace only last occurrence else disk_params << path.reverse.sub("/#{suggested_name}/".reverse, "/#{specified_name}/".reverse).reverse # Replace only last occurrence end end execute("import", ovf , *name_params, *disk_params) do |type, data| if type == :stdout # Keep track of the stdout so that we can get the VM name output << data elsif 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") if lines.include?("OK.") # 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 return get_machine_id specified_name end def max_network_adapters 8 end def read_forwarded_ports(uuid=nil, active_only=false) uuid ||= @uuid @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") results = [] current_nic = nil info = execute("showvminfo", uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| # This is how we find the nic that a FP is attached to, # since this comes first. current_nic = $1.to_i if line =~ /^nic(\d+)=".+?"$/ # If we care about active VMs only, then we check the state # to verify the VM is running. if active_only && line =~ /^VMState="(.+?)"$/ && $1.to_s != "running" return [] end # Parse out the forwarded port information if line =~ /^Forwarding.+?="(.+?),.+?,.*?,(.+?),.*?,(.+?)"$/ result = [current_nic, $1.to_s, $2.to_i, $3.to_i] @logger.debug(" - #{result.inspect}") results << result end end results end def read_bridged_interfaces execute("list", "bridgedifs").split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if line =~ /^Name:\s+(.+?)$/ info[:name] = $1.to_s elsif line =~ /^IPAddress:\s+(.+?)$/ info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end end # Return the info to build up the results info end end def read_dhcp_servers execute("list", "dhcpservers", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] info[:network] = network info[:network_name] = "HostInterfaceNetworking-#{network}" elsif ip = line[/^IP:\s+(.+?)$/, 1] info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] info[:lower] = lower elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] info[:upper] = upper end end info end end def read_guest_additions_version output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", retryable: true) if output =~ /^Value: (.+?)$/ # Split the version by _ since some distro versions modify it # to look like this: 4.1.2_ubuntu, and the distro part isn't # too important. value = $1.to_s return value.split("_").first end # If we can't get the guest additions version by guest property, try # to get it from the VM info itself. info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| return $1.to_s if line =~ /^GuestAdditionsVersion="(.+?)"$/ end return nil end def read_guest_ip(adapter_number) ip = read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") if !valid_ip_address?(ip) raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: "/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP" end return ip end def read_guest_property(property) output = execute("guestproperty", "get", @uuid, property) if output =~ /^Value: (.+?)$/ $1.to_s else raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: property end end def read_host_only_interfaces execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if line =~ /^Name:\s+(.+?)$/ info[:name] = $1.to_s elsif line =~ /^IPAddress:\s+(.+?)$/ 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 elsif line =~ /^VBoxNetworkName:\s+(.+?)$/ info[:display_name] = $1.to_s end end info end end def read_mac_address info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| return $1.to_s if line =~ /^macaddress1="(.+?)"$/ end nil end def read_mac_addresses macs = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i mac = matcher[2].to_s macs[adapter] = mac end end macs end def read_machine_folder execute("list", "systemproperties", retryable: true).split("\n").each do |line| if line =~ /^Default machine folder:\s+(.+?)$/i return $1.to_s end end nil end def read_network_interfaces nics = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if line =~ /^nic(\d+)="(.+?)"$/ adapter = $1.to_i type = $2.to_sym nics[adapter] ||= {} nics[adapter][:type] = type elsif line =~ /^hostonlyadapter(\d+)="(.+?)"$/ adapter = $1.to_i network = $2.to_s nics[adapter] ||= {} nics[adapter][:hostonly] = network elsif line =~ /^bridgeadapter(\d+)="(.+?)"$/ adapter = $1.to_i network = $2.to_s nics[adapter] ||= {} nics[adapter][:bridge] = network end end nics end def read_state output = execute("showvminfo", @uuid, "--machinereadable", retryable: true) if output =~ /^name=""$/ return :inaccessible elsif output =~ /^VMState="(.+?)"$/ return $1.to_sym end nil end def read_used_ports ports = [] execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^".+?" \{(.+?)\}$/ uuid = $1.to_s # Ignore our own used ports next if uuid == @uuid begin read_forwarded_ports(uuid, true).each do |_, _, hostport, _| ports << hostport end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end ports end def read_vms results = {} execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^"(.+?)" \{(.+?)\}$/ results[$1.to_s] = $2.to_s end end results end def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name) end def set_mac_address(mac) execute("modifyvm", @uuid, "--macaddress1", mac) end def set_name(name) execute("modifyvm", @uuid, "--name", name, retryable: true) rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VERR_ALREADY_EXISTS") # We got VERR_ALREADY_EXISTS. This means that we're renaming to # a VM name that already exists. Raise a custom error. raise Vagrant::Errors::VirtualBoxNameExists, stderr: e.extra_data[:stderr] end def share_folders(folders) is_solaris = begin "SunOS" == read_guest_property("/VirtualBox/GuestInfo/OS/Product") rescue false end folders.each do |folder| hostpath = folder[:hostpath] if Vagrant::Util::Platform.windows? && is_solaris hostpath = Vagrant::Util::Platform.windows_unc_path(hostpath) end args = ["--name", folder[:name], "--hostpath", hostpath] args << "--transient" if folder.key?(:transient) && folder[:transient] if folder[:SharedFoldersEnableSymlinksCreate] # Enable symlinks on the shared folder execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1") end # Add the shared folder execute("sharedfolder", "add", @uuid, *args) end end def ssh_port(expected_port) @logger.debug("Searching for SSH port: #{expected_port.inspect}") # Look for the forwarded port only by comparing the guest port read_forwarded_ports.each do |_, _, hostport, guestport| return hostport if guestport == expected_port end nil end def resume @logger.debug("Resuming paused VM...") execute("controlvm", @uuid, "resume") end def start(mode) command = ["startvm", @uuid, "--type", mode.to_s] r = raw(*command) if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ # Some systems return an exit code 1 for some reason. For that # we depend on the output. return true end # If we reached this point then it didn't work out. raise Vagrant::Errors::VBoxManageError, command: command.inspect, stderr: r.stderr end def suspend execute("controlvm", @uuid, "savestate") end def unshare_folders(names) names.each do |name| begin execute( "sharedfolder", "remove", @uuid, "--name", name, "--transient") execute( "setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{name}") rescue Vagrant::Errors::VBoxManageError => e if e.extra_data[:stderr].include?("VBOX_E_FILE_ERROR") # The folder doesn't exist. ignore. else raise end end end end def verify! # This command sometimes fails if kernel drivers aren't properly loaded # so we just run the command and verify that it succeeded. execute("list", "hostonlyifs", retryable: true) end def verify_image(path) r = raw("import", path.to_s, "--dry-run") return r.exit_code == 0 end def vm_exists?(uuid) 5.times do |i| result = raw("showvminfo", uuid) return true if result.exit_code == 0 # If vboxmanage returned VBOX_E_OBJECT_NOT_FOUND, # then the vm truly does not exist. Any other error might be transient return false if result.stderr.include?("VBOX_E_OBJECT_NOT_FOUND") # Sleep a bit though to give VirtualBox time to fix itself sleep 2 end # If we reach this point, it means that we consistently got the # failure, do a standard vboxmanage now. This will raise an # exception if it fails again. execute("showvminfo", uuid) return true end protected def valid_ip_address?(ip) # Filter out invalid IP addresses # GH-4658 VirtualBox can report an IP address of 0.0.0.0 for FreeBSD guests. if ip == "0.0.0.0" return false else return true end end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_5_0.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'log4r' require "vagrant/util/platform" require File.expand_path("../base", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 5.0.x class Version_5_0 < Base def initialize(uuid) super() @logger = Log4r::Logger.new("vagrant::provider::virtualbox_5_0") @uuid = uuid end # Controller-Port-Device looks like: # SATA Controller-ImageUUID-0-0 (sub out ImageUUID) # - Controller: SATA Controller # - Port: 0 # - Device: 0 # # @param [String] controller_name - name of storage controller to attach disk to # @param [String] port - port on device to attach disk to # @param [String] device - device on controller for disk # @param [String] type - type of disk to attach # @param [String] file - disk file path # @param [Hash] opts - additional options def attach_disk(controller_name, port, device, type, file, **opts) comment = "This disk is managed externally by Vagrant. Removing or adjusting settings could potentially cause issues with Vagrant." execute('storageattach', @uuid, '--storagectl', controller_name, '--port', port.to_s, '--device', device.to_s, '--type', type, '--medium', file, '--comment', comment) end def clear_forwarded_ports retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do args = [] read_forwarded_ports(@uuid).each do |nic, name, _, _| args.concat(["--natpf#{nic}", "delete", name]) end execute("modifyvm", @uuid, *args) if !args.empty? end end def clear_shared_folders retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if line =~ /^SharedFolderNameMachineMapping\d+="(.+?)"$/ execute("sharedfolder", "remove", @uuid, "--name", $1.to_s) end end end end # @param [String] source # @param [String] destination # @param [String] disk_format def clone_disk(source, destination, disk_format, **opts) execute("clonemedium", source, destination, '--format', disk_format) end 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 execute("clonevm", master_id, *args, retryable: true) return get_machine_id(machine_name) end # Removes a disk from the given virtual machine # # @param [String] disk_uuid or file path # @param [Hash] opts - additional options def close_medium(disk_uuid) execute("closemedium", disk_uuid, '--delete') end def create_dhcp_server(network, options) retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do begin execute("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], "--netmask", options[:netmask], "--lowerip", options[:dhcp_lower], "--upperip", options[:dhcp_upper], "--enable") rescue Vagrant::Errors::VBoxManageError => e return if e.extra_data[:stderr] == 'VBoxManage: error: DHCP server already exists' raise end end end # Creates a disk. Default format is VDI unless overridden # # @param [String] disk_file # @param [Integer] disk_size - size in bytes # @param [String] disk_format - format of disk, defaults to "VDI" # @param [Hash] opts - additional options def create_disk(disk_file, disk_size, disk_format="VDI", **opts) execute("createmedium", '--filename', disk_file, '--sizebyte', disk_size.to_i.to_s, '--format', disk_format) end def create_host_only_network(options) # Create the interface execute("hostonlyif", "create", retryable: true) =~ /^Interface '(.+?)' was successfully created$/ name = $1.to_s # 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], retryable: true) elsif ip.ipv6? execute("hostonlyif", "ipconfig", name, "--ipv6", options[:adapter_ip], "--netmasklengthv6", options[:netmask].to_s, retryable: true) else raise "BUG: Unknown IP type: #{ip.inspect}" end # Return the details return { name: name, ip: options[:adapter_ip], netmask: options[:netmask], dhcp: nil } end def create_snapshot(machine_id, snapshot_name) execute("snapshot", machine_id, "take", snapshot_name, retryable: true) 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, retryable: true) 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 # Lists all attached harddisks from a given virtual machine. Additionally, # this method adds a new key "Disk Name" based on the disks file path from "Location" # # @return [Array] hdds An array of hashes of harddrive info for a guest def list_hdds hdds = [] tmp_drive = {} execute('list', 'hdds', retryable: true).split("\n").each do |line| if line == "" # separator between disks hdds << tmp_drive tmp_drive = {} next end parts = line.partition(":") key = parts.first.strip value = parts.last.strip tmp_drive[key] = value if key == "Location" tmp_drive["Disk Name"] = File.basename(value, ".*") end end hdds << tmp_drive unless tmp_drive.empty? hdds 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 d = e.extra_data return [] if d[:stderr].include?("does not have") || d[:stdout].include?("does not have") raise end # @param [String] controller_name - controller name to remove disk from # @param [String] port - port on device to attach disk to # @param [String] device - device on controller for disk def remove_disk(controller_name, port, device) execute('storageattach', @uuid, '--storagectl', controller_name, '--port', port.to_s, '--device', device.to_s, '--medium', "none") end # @param [String] disk_file # @param [Integer] disk_size in bytes # @param [Hash] opts - additional options def resize_disk(disk_file, disk_size, **opts) execute("modifymedium", disk_file, '--resizebyte', disk_size.to_i.to_s) 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, retryable: true) 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", retryable: true) end def delete_unused_host_only_networks networks = [] execute("list", "hostonlyifs", retryable: true).split("\n").each do |line| networks << $1.to_s if line =~ /^Name:\s+(.+?)$/ end execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^".+?"\s+\{(.+?)\}$/ begin info = execute("showvminfo", $1.to_s, "--machinereadable", retryable: true) info.split("\n").each do |inner_line| if inner_line =~ /^hostonlyadapter\d+="(.+?)"$/ networks.delete($1.to_s) end end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end networks.each do |name| # First try to remove any DHCP servers attached. We use `raw` because # it is okay if this fails. It usually means that a DHCP server was # never attached. raw("dhcpserver", "remove", "--ifname", name) # Delete the actual host only network interface. execute("hostonlyif", "remove", name, retryable: true) end end def discard_saved_state execute("discardstate", @uuid, retryable: true) end def enable_adapters(adapters) args = [] adapters.each do |adapter| args.concat(["--nic#{adapter[:adapter]}", adapter[:type].to_s]) if adapter[:bridge] args.concat(["--bridgeadapter#{adapter[:adapter]}", adapter[:bridge], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:hostonly] args.concat(["--hostonlyadapter#{adapter[:adapter]}", adapter[:hostonly], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:intnet] args.concat(["--intnet#{adapter[:adapter]}", adapter[:intnet], "--cableconnected#{adapter[:adapter]}", "on"]) end if adapter[:mac_address] args.concat(["--macaddress#{adapter[:adapter]}", adapter[:mac_address]]) end if adapter[:nic_type] args.concat(["--nictype#{adapter[:adapter]}", adapter[:nic_type].to_s]) end end execute("modifyvm", @uuid, *args, retryable: true) end def execute_command(command) execute(*command) end def export(path) retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do begin execute("export", @uuid, "--output", path.to_s) rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VERR_E_FILE_ERROR") # If the file already exists we'll throw a custom error raise Vagrant::Errors::VirtualBoxFileExists, stderr: e.extra_data[:stderr] end end end def forward_ports(ports) args = [] ports.each do |options| pf_builder = [options[:name], options[:protocol] || "tcp", options[:hostip] || "", options[:hostport], options[:guestip] || "", options[:guestport]] args.concat(["--natpf#{options[:adapter] || 1}", pf_builder.join(",")]) end execute("modifyvm", @uuid, *args, retryable: true) 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 # Returns port and device for an attached disk given a disk uuid. Returns # empty hash if disk is not attachd to guest # # @param [String] disk_uuid - the UUID for the disk we are searching for # @return [Hash] disk_info - Contains a device and port number def get_port_and_device(disk_uuid) disk = {} storage_controllers = read_storage_controllers storage_controllers.each do |controller| controller.attachments.each do |attachment| if disk_uuid == attachment[:uuid] disk[:port] = attachment[:port] disk[:device] = attachment[:device] return disk end end end return disk end def halt execute("controlvm", @uuid, "poweroff", retryable: true) end def import(ovf) ovf = Vagrant::Util::Platform.windows_path(ovf) output = "" total = "" last = 0 # Dry-run the import to get the suggested name and path @logger.debug("Doing dry-run import to determine parallel-safe name...") output = execute("import", "-n", ovf) result = /Suggested VM name "(.+?)"/.match(output) if !result raise Vagrant::Errors::VirtualBoxNoName, output: output end suggested_name = result[1].to_s # Append millisecond plus a random to the path in case we're # importing the same box elsewhere. specified_name = "#{suggested_name}_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" @logger.debug("-- Parallel safe name: #{specified_name}") # Build the specified name param list name_params = [ "--vsys", "0", "--vmname", specified_name, ] # Extract the disks list and build the disk target params disk_params = [] disks = output.scan(/(\d+): Hard disk image: source image=.+, target path=(.+),/) disks.each do |unit_num, path| disk_params << "--vsys" disk_params << "0" disk_params << "--unit" disk_params << unit_num disk_params << "--disk" if Vagrant::Util::Platform.windows? # we use the block form of sub here to ensure that if the specified_name happens to end with a number (which is fairly likely) then # we won't end up having the character sequence of a \ followed by a number be interpreted as a back reference. For example, if # specified_name were "abc123", then "\\abc123\\".reverse would be "\\321cba\\", and the \3 would be treated as a back reference by the sub disk_params << path.reverse.sub("\\#{suggested_name}\\".reverse) { "\\#{specified_name}\\".reverse }.reverse # Replace only last occurrence else disk_params << path.reverse.sub("/#{suggested_name}/".reverse, "/#{specified_name}/".reverse).reverse # Replace only last occurrence end end execute("import", ovf , *name_params, *disk_params, retryable: true) do |type, data| if type == :stdout # Keep track of the stdout so that we can get the VM name output << data elsif 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") if lines.include?("OK.") # 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 return get_machine_id(specified_name) end def max_network_adapters 8 end def read_forwarded_ports(uuid=nil, active_only=false) uuid ||= @uuid @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") results = [] current_nic = nil info = execute("showvminfo", uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| # This is how we find the nic that a FP is attached to, # since this comes first. current_nic = $1.to_i if line =~ /^nic(\d+)=".+?"$/ # If we care about active VMs only, then we check the state # to verify the VM is running. if active_only && line =~ /^VMState="(.+?)"$/ && $1.to_s != "running" return [] end # Parse out the forwarded port information # Forwarding(1)="172.22.8.201tcp32977,tcp,172.22.8.201,32977,,3777" # Forwarding(2)="tcp32978,tcp,,32978,,3777" if line =~ /^Forwarding.+?="(.+?),.+?,(.*?),(.+?),.*?,(.+?)"$/ result = [current_nic, $1.to_s, $3.to_i, $4.to_i, $2] @logger.debug(" - #{result.inspect}") results << result end end results end def read_bridged_interfaces execute("list", "bridgedifs").split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if line =~ /^Name:\s+(.+?)$/ info[:name] = $1.to_s elsif line =~ /^IPAddress:\s+(.+?)$/ info[:ip] = $1.to_s elsif line =~ /^NetworkMask:\s+(.+?)$/ info[:netmask] = $1.to_s elsif line =~ /^Status:\s+(.+?)$/ info[:status] = $1.to_s end end # Return the info to build up the results info end end def read_dhcp_servers execute("list", "dhcpservers", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] info[:network] = network info[:network_name] = "HostInterfaceNetworking-#{network}" elsif ip = line[/^IP:\s+(.+?)$/, 1] info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] info[:lower] = lower elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] info[:upper] = upper end end info end end def read_guest_additions_version output = execute("guestproperty", "get", @uuid, "/VirtualBox/GuestAdd/Version", retryable: true) if output =~ /^Value: (.+?)$/ # Split the version by _ since some distro versions modify it # to look like this: 4.1.2_ubuntu, and the distro part isn't # too important. value = $1.to_s return value.split("_").first end # If we can't get the guest additions version by guest property, try # to get it from the VM info itself. info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| return $1.to_s if line =~ /^GuestAdditionsVersion="(.+?)"$/ end return nil end def read_guest_ip(adapter_number) ip = read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") if ip.end_with?(".1") @logger.warn("VBoxManage guest property returned: #{ip}. Result resembles IP of DHCP server and is being ignored.") ip = nil end if !valid_ip_address?(ip) raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: "/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP" end return ip end def read_guest_property(property) output = execute("guestproperty", "get", @uuid, property) if output =~ /^Value: (.+?)$/ $1.to_s else raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: property end end def read_host_only_interfaces execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if line =~ /^Name:\s+(.+?)$/ info[:name] = $1.to_s elsif line =~ /^IPAddress:\s+(.+?)$/ 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 elsif line =~ /^VBoxNetworkName:\s+(.+?)$/ info[:display_name] = $1.to_s end end info end end def read_mac_address info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| return $1.to_s if line =~ /^macaddress1="(.+?)"$/ end nil end def read_mac_addresses macs = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if matcher = /^macaddress(\d+)="(.+?)"$/.match(line) adapter = matcher[1].to_i mac = matcher[2].to_s macs[adapter] = mac end end macs end def read_machine_folder info = execute("list", "systemproperties", retryable: true) info.each_line do |line| match = line.match(/Default machine folder:\s+(?.+?)$/i) next if match.nil? return match[:folder] end @logger.warn("failed to determine machine folder from system properties") @logger.debug("processed output for machine folder lookup:\n#{info}") raise Vagrant::Errors::VirtualBoxMachineFolderNotFound end def read_network_interfaces nics = {} info = execute("showvminfo", @uuid, "--machinereadable", retryable: true) info.split("\n").each do |line| if line =~ /^nic(\d+)="(.+?)"$/ adapter = $1.to_i type = $2.to_sym nics[adapter] ||= {} nics[adapter][:type] = type elsif line =~ /^hostonlyadapter(\d+)="(.+?)"$/ adapter = $1.to_i network = $2.to_s nics[adapter] ||= {} nics[adapter][:hostonly] = network elsif line =~ /^bridgeadapter(\d+)="(.+?)"$/ adapter = $1.to_i network = $2.to_s nics[adapter] ||= {} nics[adapter][:bridge] = network end end nics end def read_state output = execute("showvminfo", @uuid, "--machinereadable", retryable: true) if output =~ /^name=""$/ return :inaccessible elsif output =~ /^VMState="(.+?)"$/ return $1.to_sym end nil end def read_used_ports used_ports = Hash.new{|hash, key| hash[key] = Set.new} execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^".+?" \{(.+?)\}$/ uuid = $1.to_s # Ignore our own used ports next if uuid == @uuid begin read_forwarded_ports(uuid, true).each do |_, _, hostport, _, hostip| hostip = '*' if hostip.nil? || hostip.empty? used_ports[hostport].add?(hostip) end rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") # VirtualBox could not find the vm. It may have been deleted # by another process after we called 'vboxmanage list vms'? Ignore this error. end end end used_ports end def read_vms results = {} execute("list", "vms", retryable: true).split("\n").each do |line| if line =~ /^"(.+?)" \{(.+?)\}$/ results[$1.to_s] = $2.to_s end end results end def reconfig_host_only(interface) execute("hostonlyif", "ipconfig", interface[:name], "--ipv6", interface[:ipv6], retryable: true) end def remove_dhcp_server(network_name) execute("dhcpserver", "remove", "--netname", network_name, retryable: true) end def set_mac_address(mac) mac = "auto" if !mac execute("modifyvm", @uuid, "--macaddress1", mac, retryable: true) end def set_name(name) retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do begin execute("modifyvm", @uuid, "--name", name) rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VERR_ALREADY_EXISTS") # We got VERR_ALREADY_EXISTS. This means that we're renaming to # a VM name that already exists. Raise a custom error. raise Vagrant::Errors::VirtualBoxNameExists, stderr: e.extra_data[:stderr] end end end def share_folders(folders) is_solaris = begin "SunOS" == read_guest_property("/VirtualBox/GuestInfo/OS/Product") rescue false end folders.each do |folder| # NOTE: Guest additions on Solaris guests do not properly handle # UNC style paths so prevent conversion (See GH-7264) if is_solaris hostpath = folder[:hostpath] else hostpath = Vagrant::Util::Platform.windows_path(folder[:hostpath]) end args = ["--name", folder[:name], "--hostpath", hostpath] args << "--transient" if folder.key?(:transient) && folder[:transient] args << "--automount" if folder.key?(:automount) && folder[:automount] if folder[:SharedFoldersEnableSymlinksCreate] # Enable symlinks on the shared folder execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1", retryable: true) end # Add the shared folder execute("sharedfolder", "add", @uuid, *args, retryable: true) end end # Returns information for a given disk # # @param [String] disk_type - can be "disk", "dvd", or "floppy" # @param [String] disk_uuid_or_file # @return [Hash] disk def show_medium_info(disk_type, disk_uuid_or_file) disk = {} execute('showmediuminfo', disk_type, disk_uuid_or_file, retryable: true).split("\n").each do |line| parts = line.partition(":") key = parts.first.strip value = parts.last.strip disk[key] = value if key == "Location" disk["Disk Name"] = File.basename(value, ".*") end end disk end def ssh_port(expected_port) @logger.debug("Searching for SSH port: #{expected_port.inspect}") # Look for the forwarded port. Valid based on the guest port, but will do # scoring based matching to determine best value when multiple results are # available. matches = read_forwarded_ports.map do |_, name, hostport, guestport, host_ip| next if guestport != expected_port match = [0, hostport] match[0] += 1 if name == "ssh" match[0] += 1 if name.downcase == "ssh" match[0] += 1 if host_ip == "127.0.0.1" match end.compact result = matches.sort_by(&:first).last result.last if result end def resume @logger.debug("Resuming paused VM...") execute("controlvm", @uuid, "resume") end def start(mode) command = ["startvm", @uuid, "--type", mode.to_s] retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do r = raw(*command) if r.exit_code == 0 || r.stdout =~ /VM ".+?" has been successfully started/ # Some systems return an exit code 1 for some reason. For that # we depend on the output. return true end # If we reached this point then it didn't work out. raise Vagrant::Errors::VBoxManageError, command: command.inspect, stderr: r.stderr end end def suspend execute("controlvm", @uuid, "savestate", retryable: true) end def unshare_folders(names) names.each do |name| retryable(on: Vagrant::Errors::VBoxManageError, tries: 3, sleep: 1) do begin execute( "sharedfolder", "remove", @uuid, "--name", name, "--transient") execute( "setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{name}") rescue Vagrant::Errors::VBoxManageError => e raise if !e.extra_data[:stderr].include?("VBOX_E_FILE_ERROR") end end end end def verify! # This command sometimes fails if kernel drivers aren't properly loaded # so we just run the command and verify that it succeeded. execute("list", "hostonlyifs", retryable: true) end def verify_image(path) r = raw("import", path.to_s, "--dry-run") return r.exit_code == 0 end def vm_exists?(uuid) 5.times do |i| result = raw("showvminfo", uuid) return true if result.exit_code == 0 # If vboxmanage returned VBOX_E_OBJECT_NOT_FOUND, # then the vm truly does not exist. Any other error might be transient return false if result.stderr.include?("VBOX_E_OBJECT_NOT_FOUND") # Sleep a bit though to give VirtualBox time to fix itself sleep 2 end # If we reach this point, it means that we consistently got the # failure, do a standard vboxmanage now. This will raise an # exception if it fails again. execute("showvminfo", uuid) return true end # @param [VagrantPlugins::VirtualboxProvider::Driver] driver # @param [String] defined_disk_path # @return [String] destination - The cloned disk def vmdk_to_vdi(defined_disk_path) source = defined_disk_path destination = File.join(File.dirname(source), File.basename(source, ".*")) + ".vdi" clone_disk(source, destination, 'VDI') destination end # @param [VagrantPlugins::VirtualboxProvider::Driver] driver # @param [String] defined_disk_path # @return [String] destination - The cloned disk def vdi_to_vmdk(defined_disk_path) source = defined_disk_path destination = File.join(File.dirname(source), File.basename(source, ".*")) + ".vmdk" clone_disk(source, destination, 'VMDK') destination end # Helper method to get a list of storage controllers added to the # current VM # # @return [VagrantPlugins::ProviderVirtualBox::Model::StorageControllerArray] def read_storage_controllers vm_info = show_vm_info count = vm_info.count { |key, value| key.match(/^storagecontrollername\d+$/) } all_disks = list_hdds storage_controllers = Model::StorageControllerArray.new (0..count - 1).each do |n| # basic controller metadata name = vm_info["storagecontrollername#{n}"] type = vm_info["storagecontrollertype#{n}"] maxportcount = vm_info["storagecontrollermaxportcount#{n}"].to_i # build attachments array attachments = [] vm_info.each do |k, v| if /^#{name}-ImageUUID-(\d+)-(\d+)$/ =~ k port = $1.to_s device = $2.to_s uuid = v location = vm_info["#{name}-#{port}-#{device}"] extra_disk_data = all_disks.detect { |d| d["UUID"] == uuid } attachment = { port: port, device: device, uuid: uuid, location: location } extra_disk_data&.each do |dk,dv| # NOTE: We convert the keys from VirtualBox to symbols # to be consistent with the other keys attachment[dk.downcase.gsub(' ', '_').to_sym] = dv end attachments << attachment end end storage_controllers << Model::StorageController.new(name, type, maxportcount, attachments) end storage_controllers end protected def valid_ip_address?(ip) # Filter out invalid IP addresses # GH-4658 VirtualBox can report an IP address of 0.0.0.0 for FreeBSD guests. if ip == "0.0.0.0" || ip.nil? return false else return true end end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_5_1.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../version_5_0", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 5.1.x class Version_5_1 < Version_5_0 def initialize(uuid) super @logger = Log4r::Logger.new("vagrant::provider::virtualbox_5_1") end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_5_2.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../version_5_0", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 5.2.x class Version_5_2 < Version_5_0 def initialize(uuid) super @logger = Log4r::Logger.new("vagrant::provider::virtualbox_5_2") end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_6_0.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../version_5_0", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 6.0.x class Version_6_0 < Version_5_0 def initialize(uuid) super @logger = Log4r::Logger.new("vagrant::provider::virtualbox_6_0") end def import(ovf) ovf = Vagrant::Util::Platform.windows_path(ovf) output = "" total = "" last = 0 # Dry-run the import to get the suggested name and path @logger.debug("Doing dry-run import to determine parallel-safe name...") output = execute("import", "-n", ovf) result = /Suggested VM name "(.+?)"/.match(output) if !result raise Vagrant::Errors::VirtualBoxNoName, output: output end suggested_name = result[1].to_s # Append millisecond plus a random to the path in case we're # importing the same box elsewhere. specified_name = "#{suggested_name}_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}" @logger.debug("-- Parallel safe name: #{specified_name}") # Build the specified name param list name_params = [ "--vsys", "0", "--vmname", specified_name, ] # Target path for disks is no longer a full path. Extract the path for the # settings file to determine the base directory which we can then use to # build the disk paths result = /Suggested VM settings file name "(?.+?)"/.match(output) if !result @logger.warn("Failed to locate base path for disks. Using current working directory.") base_path = "." else base_path = result[:settings_path] if Vagrant::Util::Platform.windows? || Vagrant::Util::Platform.wsl? base_path.gsub!('\\', '/') end base_path = File.dirname(base_path) end @logger.info("Base path for disk import: #{base_path}") # Extract the disks list and build the disk target params disk_params = [] disks = output.scan(/(\d+): Hard disk image: source image=.+, target path=(.+),/) disks.each do |unit_num, path| path = File.join(base_path, File.basename(path)) disk_params << "--vsys" disk_params << "0" disk_params << "--unit" disk_params << unit_num disk_params << "--disk" disk_params << path.reverse.sub("/#{suggested_name}/".reverse, "/#{specified_name}/".reverse).reverse # Replace only last occurrence end execute("import", ovf , *name_params, *disk_params, retryable: true) do |type, data| if type == :stdout # Keep track of the stdout so that we can get the VM name output << data elsif 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") if lines.include?("OK.") # 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 return get_machine_id specified_name end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_6_1.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../version_6_0", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 6.1.x class Version_6_1 < Version_6_0 def initialize(uuid) super @logger = Log4r::Logger.new("vagrant::provider::virtualbox_6_1") end def read_dhcp_servers execute("list", "dhcpservers", retryable: true).split("\n\n").collect do |block| info = {} block.split("\n").each do |line| if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] info[:network] = network info[:network_name] = "HostInterfaceNetworking-#{network}" elsif ip = line[/^Dhcpd IP:\s+(.+?)$/, 1] info[:ip] = ip elsif netmask = line[/^NetworkMask:\s+(.+?)$/, 1] info[:netmask] = netmask elsif lower = line[/^LowerIPAddress:\s+(.+?)$/, 1] info[:lower] = lower elsif upper = line[/^UpperIPAddress:\s+(.+?)$/, 1] info[:upper] = upper end end info end end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_7_0.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "rexml" require File.expand_path("../version_6_1", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 7.0.x class Version_7_0 < Version_6_1 # VirtualBox version requirement for using host only networks # instead of host only interfaces HOSTONLY_NET_REQUIREMENT=Gem::Requirement.new(">= 7") # Prefix of name used for host only networks HOSTONLY_NAME_PREFIX="vagrantnet-vbox" DEFAULT_NETMASK="255.255.255.0" def initialize(uuid) super @logger = Log4r::Logger.new("vagrant::provider::virtualbox_7_0") end def read_bridged_interfaces ifaces = super return ifaces if !use_host_only_nets? # Get a list of all subnets which are in use for hostonly networks hostonly_ifaces = read_host_only_networks.map do |net| IPAddr.new(net[:lowerip]).mask(net[:networkmask]) end # Prune any hostonly interfaces in the list ifaces.delete_if { |i| addr = begin IPAddr.new(i[:ip]).mask(i[:netmask]) rescue IPAddr::Error => err @logger.warn("skipping bridged interface due to parse error #{err} (#{i}) ") nil end addr.nil? || hostonly_ifaces.include?(addr) } ifaces end def delete_unused_host_only_networks return super if !use_host_only_nets? # First get the list of existing host only network names network_names = read_host_only_networks.map { |net| net[:name] } # Prune the network names to only include ones we manage network_names.delete_if { |name| !name.start_with?(HOSTONLY_NAME_PREFIX) } @logger.debug("managed host only network names: #{network_names}") return if network_names.empty? # Next get the list of host only networks currently in use inuse_names = [] execute("list", "vms", retryable: true).split("\n").each do |line| match = line.match(/^".+?"\s+\{(?.+?)\}$/) next if match.nil? begin info = execute("showvminfo", match[:vmid].to_s, "--machinereadable", retryable: true) info.split("\n").each do |vmline| if vmline.start_with?("hostonly-network") net_name = vmline.split("=", 2).last.to_s.gsub('"', "") inuse_names << net_name end end rescue Vagrant::Errors::VBoxManageError => err raise if !err.extra_data[:stderr].include?("VBOX_E_OBJECT_NOT_FOUND") end end @logger.debug("currently in use network names: #{inuse_names}") # Now remove all the networks not in use (network_names - inuse_names).each do |name| execute("hostonlynet", "remove", "--name", name, retryable: true) end end def enable_adapters(adapters) return super if !use_host_only_nets? hostonly_adapters = adapters.find_all { |adapter| adapter[:hostonly] } other_adapters = adapters - hostonly_adapters super(other_adapters) if !other_adapters.empty? if !hostonly_adapters.empty? args = [] hostonly_adapters.each do |adapter| args.concat(["--nic#{adapter[:adapter]}", "hostonlynet"]) args.concat(["--host-only-net#{adapter[:adapter]}", adapter[:hostonly], "--cableconnected#{adapter[:adapter]}", "on"]) end execute("modifyvm", @uuid, *args, retryable: true) end end def create_host_only_network(options) # If we are not on macOS, just setup the hostonly interface return super if !use_host_only_nets? opts = { netmask: options.fetch(:netmask, DEFAULT_NETMASK), } if options[:type] == :dhcp opts[:lower] = options[:dhcp_lower] opts[:upper] = options[:dhcp_upper] else addr = IPAddr.new(options[:adapter_ip]) opts[:upper] = opts[:lower] = addr.mask(opts[:netmask]).to_range.first.to_s end name_idx = read_host_only_networks.map { |hn| next if !hn[:name].start_with?(HOSTONLY_NAME_PREFIX) hn[:name].sub(HOSTONLY_NAME_PREFIX, "").to_i }.compact.max.to_i + 1 opts[:name] = HOSTONLY_NAME_PREFIX + name_idx.to_s execute("hostonlynet", "add", "--name", opts[:name], "--netmask", opts[:netmask], "--lower-ip", opts[:lower], "--upper-ip", opts[:upper], retryable: true) { name: opts[:name], ip: options[:adapter_ip], netmask: opts[:netmask], } end # Disabled when host only nets are in use def reconfig_host_only(options) return super if !use_host_only_nets? end # Disabled when host only nets are in use since # the host only nets will provide the dhcp server def remove_dhcp_server(*_, **_) super if !use_host_only_nets? end # Disabled when host only nets are in use since # the host only nets will provide the dhcp server def create_dhcp_server(*_, **_) super if !use_host_only_nets? end def read_host_only_interfaces return super if !use_host_only_nets? # When host only nets are in use, read them and # reformat the information to line up with how # the interfaces is structured read_host_only_networks.map do |net| addr = begin IPAddr.new(net[:lowerip]) rescue IPAddr::Error => err @logger.warn("invalid host only network lower IP encountered: #{err} (#{net})") next end # Address of the interface will be the lower bound of the range or # the first available address in the subnet if addr == addr.mask(net[:networkmask]) addr = addr.succ end net[:netmask] = net[:networkmask] if addr.ipv4? net[:ip] = addr.to_s net[:ipv6] = "" else net[:ip] = "" net[:ipv6] = addr.to_s net[:ipv6_prefix] = net[:netmask] end net[:status] = net[:state] == "Enabled" ? "Up" : "Down" net end.compact end def read_network_interfaces return super if !use_host_only_nets? {}.tap do |nics| execute("showvminfo", @uuid, "--machinereadable", retryable: true).each_line do |line| if m = line.match(/nic(?\d+)="(?.+?)"$/) nics[m[:adapter].to_i] ||= {} if m[:type] == "hostonlynetwork" nics[m[:adapter].to_i][:type] = :hostonly else nics[m[:adapter].to_i][:type] = m[:type].to_sym end elsif m = line.match(/^bridgeadapter(?\d+)="(?.+?)"$/) nics[m[:adapter].to_i] ||= {} nics[m[:adapter].to_i][:bridge] = m[:network] elsif m = line.match(/^hostonly-network(?\d+)="(?.+?)"$/) nics[m[:adapter].to_i] ||= {} nics[m[:adapter].to_i][:hostonly] = m[:network] end end end end # The initial VirtualBox 7.0 release has an issue with displaying port # forward information. When a single port forward is defined, the forwarding # information can be found in the `showvminfo` output. Once more than a # single port forward is defined, no forwarding information is provided # in the `showvminfo` output. To work around this we grab the VM configuration # file from the `showvminfo` output and extract the port forward information # from there instead. def read_forwarded_ports(uuid=nil, active_only=false) # Only use this override for the 7.0.0 release. return super if get_version.to_s != "7.0.0" uuid ||= @uuid @logger.debug("read_forward_ports: uuid=#{uuid} active_only=#{active_only}") results = [] info = execute("showvminfo", uuid, "--machinereadable", retryable: true) result = info.match(/CfgFile="(?.+?)"/) if result.nil? raise Vagrant::Errors::VirtualBoxConfigNotFound, uuid: uuid end File.open(result[:path], "r") do |f| doc = REXML::Document.new(f) networks = REXML::XPath.each(doc.root, "Machine/Hardware/Network/Adapter") networks.each do |net| REXML::XPath.each(doc.root, net.xpath + "/NAT/Forwarding") do |fwd| # Result Array values: # [NIC Slot, Name, Host Port, Guest Port, Host IP] result = [ net.attribute("slot").value.to_i + 1, fwd.attribute("name")&.value.to_s, fwd.attribute("hostport")&.value.to_i, fwd.attribute("guestport")&.value.to_i, fwd.attribute("hostip")&.value.to_s ] @logger.debug(" - #{result.inspect}") results << result end end end results end protected # Generate list of host only networks # NOTE: This is darwin specific def read_host_only_networks networks = [] current = nil execute("list", "hostonlynets", retryable: true).split("\n").each do |line| line.chomp! next if line.empty? key, value = line.split(":", 2).map(&:strip) key = key.downcase if key == "name" networks.push(current) if !current.nil? current = Vagrant::Util::HashWithIndifferentAccess.new end current[key] = value end networks.push(current) if !current.nil? networks end private # Returns if hostonlynets are enabled on the current # host platform # # @return [Boolean] def use_host_only_nets? Vagrant::Util::Platform.darwin? && HOSTONLY_NET_REQUIREMENT.satisfied_by?(get_version) end # VirtualBox version in use # # @return [Gem::Version] def get_version return @version if @version @version = Gem::Version.new(Meta.new.version) end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_7_1.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../version_7_0", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 7.1.x class Version_7_1 < Version_7_0 def initialize(uuid) super @logger = Log4r::Logger.new("vagrant::provider::virtualbox_7_1") end end end end end ================================================ FILE: plugins/providers/virtualbox/driver/version_7_2.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../version_7_0", __FILE__) module VagrantPlugins module ProviderVirtualBox module Driver # Driver for VirtualBox 7.2.x class Version_7_2 < Version_7_1 def initialize(uuid) super @logger = Log4r::Logger.new("vagrant::provider::virtualbox_7_2") end end end end end ================================================ FILE: plugins/providers/virtualbox/model/forwarded_port.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Model # Represents a single forwarded port for VirtualBox. This has various # helpers and defaults for a forwarded port. class ForwardedPort # The NAT adapter on which to attach the forwarded port. # # @return [Integer] attr_reader :adapter # If true, this port should be auto-corrected. # # @return [Boolean] attr_reader :auto_correct # The unique ID for the forwarded port. # # @return [String] attr_reader :id # The protocol to forward. # # @return [String] attr_reader :protocol # The IP that the forwarded port will connect to on the guest machine. # # @return [String] attr_reader :guest_ip # The port on the guest to be exposed on the host. # # @return [Integer] attr_reader :guest_port # The IP that the forwarded port will bind to on the host machine. # # @return [String] attr_reader :host_ip # The port on the host used to access the port on the guest. # # @return [Integer] attr_reader :host_port def initialize(id, host_port, guest_port, options) @id = id @guest_port = guest_port @host_port = host_port options ||= {} @auto_correct = false @auto_correct = options[:auto_correct] if options.key?(:auto_correct) @adapter = (options[:adapter] || 1).to_i @guest_ip = options[:guest_ip] || nil @host_ip = options[:host_ip] || nil @protocol = options[:protocol] || "tcp" end # This corrects the host port and changes it to the given new port. # # @param [Integer] new_port The new port def correct_host_port(new_port) @host_port = new_port end end end end end ================================================ FILE: plugins/providers/virtualbox/model/storage_controller.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ProviderVirtualBox module Model # Represents a storage controller for VirtualBox. Storage controllers # have a type, a name, and can have hard disks or optical drives attached. class StorageController IDE_CONTROLLER_TYPES = ["PIIX4", "PIIX3", "ICH6"].map(&:freeze).freeze SATA_CONTROLLER_TYPES = ["IntelAhci"].map(&:freeze).freeze SCSI_CONTROLLER_TYPES = ["LsiLogic", "LsiLogicSas", "BusLogic", "VirtioSCSI"].map(&:freeze).freeze IDE_DEVICES_PER_PORT = 2.freeze SATA_DEVICES_PER_PORT = 1.freeze SCSI_DEVICES_PER_PORT = 1.freeze IDE_BOOT_PRIORITY = 1.freeze SATA_BOOT_PRIORITY = 2.freeze SCSI_BOOT_PRIORITY = 3.freeze # The name of the storage controller. # # @return [String] attr_reader :name # The specific type of controller. # # @return [String] attr_reader :type # The maximum number of avilable ports for the storage controller. # # @return [Integer] attr_reader :maxportcount # The number of devices that can be attached to each port. For SATA # controllers, this will usually be 1, and for IDE controllers this # will usually be 2. # @return [Integer] attr_reader :devices_per_port # The maximum number of individual disks that can be attached to the # storage controller. For SATA controllers, this equals the maximum # number of ports. For IDE controllers, this will be twice the max # number of ports (primary/secondary). # # @return [Integer] attr_reader :limit # The boot priority of the storage controller. This does not seem to # depend on the controller number returned by `showvminfo`. # Experimentation has determined that VirtualBox will try to boot from # the first controller it finds with a hard disk, in this order: # IDE, SATA, SCSI # # @return [Integer] attr_reader :boot_priority # The list of disks/ISOs attached to each storage controller. # # @return [Array] attr_reader :attachments def initialize(name, type, maxportcount, attachments) @name = name @type = type @maxportcount = maxportcount.to_i if IDE_CONTROLLER_TYPES.include?(@type) @storage_bus = :ide @devices_per_port = IDE_DEVICES_PER_PORT @boot_priority = IDE_BOOT_PRIORITY elsif SATA_CONTROLLER_TYPES.include?(@type) @storage_bus = :sata @devices_per_port = SATA_DEVICES_PER_PORT @boot_priority = SATA_BOOT_PRIORITY elsif SCSI_CONTROLLER_TYPES.include?(@type) @storage_bus = :scsi @devices_per_port = SCSI_DEVICES_PER_PORT @boot_priority = SCSI_BOOT_PRIORITY else @storage_bus = :unknown @devices_per_port = 1 end @limit = @maxportcount * @devices_per_port attachments ||= [] @attachments = attachments end # Get a single storage device, either by port/device address or by # UUID. # # @param [Hash] opts - A hash of options to match # @return [Hash] attachment - Attachment information def get_attachment(opts = {}) if opts[:port] && opts[:device] @attachments.detect { |a| a[:port] == opts[:port] && a[:device] == opts[:device] } elsif opts[:uuid] @attachments.detect { |a| a[:uuid] == opts[:uuid] } end end # Returns true if the storage controller has a supported type. # # @return [Boolean] def supported? [:ide, :sata, :scsi].include?(@storage_bus) end # Returns true if the storage controller is a IDE type controller. # # @return [Boolean] def ide? @storage_bus == :ide end # Returns true if the storage controller is a SATA type controller. # # @return [Boolean] def sata? @storage_bus == :sata end # Returns true if the storage controller is a SCSI type controller. # # @return [Boolean] def scsi? @storage_bus == :scsi end end end end end ================================================ FILE: plugins/providers/virtualbox/model/storage_controller_array.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../cap/validate_disk_ext" module VagrantPlugins module ProviderVirtualBox module Model # A collection of storage controllers. Includes finder methods to look # up a storage controller by given attributes. class StorageControllerArray < Array # Returns a storage controller with the given name. Raises an # exception if a matching controller can't be found. # # @param [String] name - The name of the storage controller # @return [VagrantPlugins::ProviderVirtualBox::Model::StorageController] def get_controller(name) controller = detect { |c| c.name == name } if !controller raise Vagrant::Errors::VirtualBoxDisksControllerNotFound, name: name end controller end # Find the controller containing the primary disk (i.e. the boot # disk). This is used to determine which controller virtual disks # should be attached to. # # Raises an exception if no supported controllers are found. # # @return [VagrantPlugins::ProviderVirtualBox::Model::StorageController] def get_primary_controller ordered = find_all(&:supported?).sort_by(&:boot_priority) controller = ordered.detect { |c| c.attachments.any? { |a| hdd?(a) } } if !controller raise Vagrant::Errors::VirtualBoxDisksNoSupportedControllers, supported_types: supported_types.join(", ") end controller end # Find the attachment representing the primary disk (i.e. the boot # disk). We can't rely on the order of #list_hdds, as they will not # always come in port order, but primary is always Port 0 Device 0. # # @return [Hash] attachment - Primary disk attachment information def get_primary_attachment attachment = nil controller = get_primary_controller attachment = controller.get_attachment(port: "0", device: "0") if !attachment raise Vagrant::Errors::VirtualBoxDisksPrimaryNotFound end attachment end # Returns the first supported storage controller for attaching dvds. # Will raise an exception if no suitable controller can be found. # # @return [VagrantPlugins::ProviderVirtualBox::Model::StorageController] def get_dvd_controller ordered = find_all(&:supported?).sort_by(&:boot_priority) controller = ordered.first if !controller raise Vagrant::Errors::VirtualBoxDisksNoSupportedControllers, supported_types: supported_types.join(", ") end controller end private # Determine whether the given attachment is a hard disk. # # @param [Hash] attachment - Attachment information # @return [Boolean] def hdd?(attachment) if !attachment false else ext = File.extname(attachment[:location].to_s).downcase.split('.').last VagrantPlugins::ProviderVirtualBox::Cap::ValidateDiskExt.validate_disk_ext(nil, ext) end end # Returns a list of all the supported controller types. # # @return [Array] def supported_types StorageController::SATA_CONTROLLER_TYPES + StorageController::IDE_CONTROLLER_TYPES + StorageController::SCSI_CONTROLLER_TYPES end end end end end ================================================ FILE: plugins/providers/virtualbox/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module ProviderVirtualBox class Plugin < Vagrant.plugin("2") name "VirtualBox provider" description <<-EOF The VirtualBox provider allows Vagrant to manage and control VirtualBox-based virtual machines. EOF provider(:virtualbox, priority: 6) do require File.expand_path("../provider", __FILE__) Provider end config(:virtualbox, :provider) do require File.expand_path("../config", __FILE__) Config end synced_folder(:virtualbox) do require File.expand_path("../synced_folder", __FILE__) SyncedFolder end provider_capability(:virtualbox, :forwarded_ports) do require_relative "cap" Cap end provider_capability(:virtualbox, :nic_mac_addresses) do require_relative "cap" Cap end provider_capability(:virtualbox, :public_address) do require_relative "cap/public_address" Cap::PublicAddress end provider_capability(:virtualbox, :configure_disks) do require_relative "cap/configure_disks" Cap::ConfigureDisks end provider_capability(:virtualbox, :cleanup_disks) do require_relative "cap/cleanup_disks" Cap::CleanupDisks end provider_capability(:virtualbox, :validate_disk_ext) do require_relative "cap/validate_disk_ext" Cap::ValidateDiskExt end provider_capability(:virtualbox, :default_disk_exts) do require_relative "cap/validate_disk_ext" Cap::ValidateDiskExt end provider_capability(:virtualbox, :set_default_disk_ext) do require_relative "cap/validate_disk_ext" Cap::ValidateDiskExt end provider_capability(:virtualbox, :snapshot_list) do require_relative "cap" Cap end synced_folder_capability(:virtualbox, "mount_options") do require_relative "cap/mount_options" Cap::MountOptions end synced_folder_capability(:virtualbox, "mount_type") do require_relative "cap/mount_options" Cap::MountOptions end synced_folder_capability(:virtualbox, "mount_name") do require_relative "cap/mount_options" Cap::MountOptions end end autoload :Action, File.expand_path("../action", __FILE__) # Drop some autoloads in here to optimize the performance of loading # our drivers only when they are needed. module Driver autoload :Meta, File.expand_path("../driver/meta", __FILE__) autoload :Version_4_0, File.expand_path("../driver/version_4_0", __FILE__) autoload :Version_4_1, File.expand_path("../driver/version_4_1", __FILE__) autoload :Version_4_2, File.expand_path("../driver/version_4_2", __FILE__) autoload :Version_4_3, File.expand_path("../driver/version_4_3", __FILE__) autoload :Version_5_0, File.expand_path("../driver/version_5_0", __FILE__) autoload :Version_5_1, File.expand_path("../driver/version_5_1", __FILE__) autoload :Version_5_2, File.expand_path("../driver/version_5_2", __FILE__) autoload :Version_6_0, File.expand_path("../driver/version_6_0", __FILE__) autoload :Version_6_1, File.expand_path("../driver/version_6_1", __FILE__) autoload :Version_7_0, File.expand_path("../driver/version_7_0", __FILE__) autoload :Version_7_1, File.expand_path("../driver/version_7_1", __FILE__) autoload :Version_7_2, File.expand_path("../driver/version_7_2", __FILE__) end module Model autoload :ForwardedPort, File.expand_path("../model/forwarded_port", __FILE__) autoload :StorageController, File.expand_path("../model/storage_controller", __FILE__) autoload :StorageControllerArray, File.expand_path("../model/storage_controller_array", __FILE__) end module Util autoload :CompileForwardedPorts, File.expand_path("../util/compile_forwarded_ports", __FILE__) end end end ================================================ FILE: plugins/providers/virtualbox/provider.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module ProviderVirtualBox class Provider < Vagrant.plugin("2", :provider) attr_reader :driver def self.installed? Driver::Meta.new true rescue Vagrant::Errors::VirtualBoxInvalidVersion, Vagrant::Errors::VirtualBoxNotDetected, Vagrant::Errors::VirtualBoxKernelModuleNotLoaded, Vagrant::Errors::VirtualBoxInstallIncomplete 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 Driver::Meta.new true rescue Vagrant::Errors::VirtualBoxInvalidVersion, Vagrant::Errors::VirtualBoxNotDetected, Vagrant::Errors::VirtualBoxKernelModuleNotLoaded, Vagrant::Errors::VirtualBoxInstallIncomplete, Vagrant::Errors::VBoxManageNotFoundError raise if raise_error return false end def initialize(machine) @logger = Log4r::Logger.new("vagrant::provider::virtualbox") @machine = machine # This method will load in our driver, so we call it now to # initialize it. machine_id_changed end # @see Vagrant::Plugin::V1::Provider#action def action(name) # Attempt to get the action method from the Action class if it # exists, otherwise return nil to show that we don't support the # given action. action_method = "action_#{name}" return Action.send(action_method) if Action.respond_to?(action_method) nil end # If the machine ID changed, then we need to rebuild our underlying # driver. def machine_id_changed id = @machine.id begin @logger.debug("Instantiating the driver for machine ID: #{@machine.id.inspect}") @driver = Driver::Meta.new(id) rescue Driver::Meta::VMNotFound # The virtual machine doesn't exist, so we probably have a stale # ID. Just clear the id out of the machine and reload it. @logger.debug("VM not found! Clearing saved machine ID and reloading.") id = nil retry end end # Returns the SSH info for accessing the VirtualBox VM. def ssh_info # If the VM is not running that we can't possibly SSH into it return nil if state.id != :running # 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) } end # Return the state of VirtualBox virtual machine by actually # querying VBoxManage. # # @return [Symbol] def state # We have to check if the UID matches to avoid issues with # VirtualBox. if Vagrant::Util::Platform.wsl_windows_access_bypass?(@machine.data_dir) @logger.warn("Skipping UID check on machine by user request for WSL Windows access.") else uid = @machine.uid if uid && uid.to_s != Process.uid.to_s raise Vagrant::Errors::VirtualBoxUserMismatch, original_uid: uid.to_s, uid: Process.uid.to_s end end # Determine the ID of the state here. state_id = nil state_id = :not_created if !@driver.uuid state_id = @driver.read_state if !state_id state_id = :unknown if !state_id # Translate into short/long descriptions short = state_id.to_s.gsub("_", " ") long = I18n.t("vagrant.commands.status.#{state_id}") # If we're not created, then specify the special ID flag if state_id == :not_created state_id = Vagrant::MachineState::NOT_CREATED_ID end # Return the state Vagrant::MachineState.new(state_id, short, long) end # Returns a human-friendly string version of this provider which # includes the machine's ID that this provider represents, if it # has one. # # @return [String] def to_s id = @machine.id ? @machine.id : "new VM" "VirtualBox (#{id})" end end end end ================================================ FILE: plugins/providers/virtualbox/synced_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "vagrant/util/platform" module VagrantPlugins module ProviderVirtualBox class SyncedFolder < Vagrant.plugin("2", :synced_folder) def usable?(machine, raise_errors=false) # These synced folders only work if the provider if VirtualBox return false if machine.provider_name != :virtualbox # This only happens with `vagrant package --base`. Sigh. return true if !machine.provider_config machine.provider_config.functional_vboxsf end def prepare(machine, folders, _opts) share_folders(machine, folders, false) end def enable(machine, folders, _opts) share_folders(machine, folders, true) # short guestpaths first, so we don't step on ourselves folders = folders.sort_by do |id, data| if data[:guestpath] data[:guestpath].length else # A long enough path to just do this at the end. 10000 end end # Go through each folder and mount machine.ui.output(I18n.t("vagrant.actions.vm.share_folders.mounting")) fstab_folders = [] folders.each do |id, data| if data[:guestpath] # Guest path specified, so mount the folder to specified point machine.ui.detail(I18n.t("vagrant.actions.vm.share_folders.mounting_entry", guestpath: data[:guestpath], hostpath: data[:hostpath])) # Dup the data so we can pass it to the guest API data = data.dup # Calculate the owner and group ssh_info = machine.ssh_info data[:owner] ||= ssh_info[:username] data[:group] ||= ssh_info[:username] # Mount the actual folder machine.guest.capability( :mount_virtualbox_shared_folder, os_friendly_id(id), data[:guestpath], data) else # If no guest path is specified, then automounting is disabled machine.ui.detail(I18n.t("vagrant.actions.vm.share_folders.nomount_entry", hostpath: data[:hostpath])) end end end def disable(machine, folders, _opts) if machine.guest.capability?(:unmount_virtualbox_shared_folder) folders.each do |id, data| machine.guest.capability( :unmount_virtualbox_shared_folder, data[:guestpath], data) end end # Remove the shared folders from the VM metadata names = folders.map { |id, _data| os_friendly_id(id) } driver(machine).unshare_folders(names) end def cleanup(machine, opts) driver(machine).clear_shared_folders if machine.id && machine.id != "" end protected # This is here so that we can stub it for tests def driver(machine) machine.provider.driver end def os_friendly_id(id) id.gsub(/[\s\/\\]/,'_').sub(/^_/, '') end # share_folders sets up the shared folder definitions on the # VirtualBox VM. # # The transient parameter determines if we're FORCING transient # or not. If this is false, then any shared folders will be # shared as non-transient unless they've specifically asked for # transient. def share_folders(machine, folders, transient) defs = [] warn_user_symlink = false folders.each do |id, data| hostpath = data[:hostpath] if !data[:hostpath_exact] hostpath = Vagrant::Util::Platform.cygwin_windows_path(hostpath) end enable_symlink_create = true if ENV['VAGRANT_DISABLE_VBOXSYMLINKCREATE'] enable_symlink_create = false end unless data[:SharedFoldersEnableSymlinksCreate].nil? enable_symlink_create = data[:SharedFoldersEnableSymlinksCreate] end warn_user_symlink ||= enable_symlink_create # Only setup the shared folders that match our transient level if (!!data[:transient]) == transient defs << { name: os_friendly_id(id), hostpath: hostpath.to_s, transient: transient, SharedFoldersEnableSymlinksCreate: enable_symlink_create, automount: !!data[:automount] } end end if warn_user_symlink display_symlink_create_warning(machine.env) end driver(machine).share_folders(defs) end def display_symlink_create_warning(env) d_file = env.data_dir.join("vbox_symlink_create_warning") if !d_file.exist? FileUtils.touch(d_file.to_path) env.ui.warn(I18n.t("vagrant.virtualbox.warning.shared_folder_symlink_create")) end end end end end ================================================ FILE: plugins/providers/virtualbox/util/compile_forwarded_ports.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/scoped_hash_override" module VagrantPlugins module ProviderVirtualBox module Util module CompileForwardedPorts include Vagrant::Util::ScopedHashOverride # This method compiles the forwarded ports into {ForwardedPort} # models. def compile_forwarded_ports(config) mappings = {} config.vm.networks.each do |type, options| if type == :forwarded_port guest_port = options[:guest] host_port = options[:host] host_ip = options[:host_ip] protocol = options[:protocol] || "tcp" options = scoped_hash_override(options, :virtualbox) id = options[:id] # If the forwarded port was marked as disabled, ignore. next if options[:disabled] key = "#{host_ip}#{protocol}#{host_port}" mappings[key] = Model::ForwardedPort.new(id, host_port, guest_port, options) end end mappings.values end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/alpine/ansible_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../facts" require_relative "../pip/pip" module VagrantPlugins module Ansible module Cap module Guest module Alpine module AnsibleInstall def self.ansible_install(machine, install_mode, ansible_version, pip_args, pip_install_cmd = "") case install_mode when :pip pip_setup machine, pip_install_cmd Pip::pip_install machine, "ansible", ansible_version, pip_args, true when :pip_args_only pip_setup machine, pip_install_cmd Pip::pip_install machine, "", "", pip_args, false else ansible_apk_install machine end end private def self.ansible_apk_install(machine) machine.communicate.sudo "apk add --update --no-cache python3 ansible" machine.communicate.sudo "if [ ! -e /usr/bin/python ]; then ln -sf python3 /usr/bin/python ; fi" machine.communicate.sudo "if [ ! -e /usr/bin/pip ]; then ln -sf pip3 /usr/bin/pip ; fi" end def self.pip_setup(machine, pip_install_cmd = "") machine.communicate.sudo "apk add --update --no-cache python3" machine.communicate.sudo "if [ ! -e /usr/bin/python ]; then ln -sf python3 /usr/bin/python ; fi" machine.communicate.sudo "apk add --update --no-cache --virtual .build-deps python3-dev libffi-dev openssl-dev build-base" Pip::get_pip machine, pip_install_cmd end end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/arch/ansible_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../errors" require_relative "../pip/pip" module VagrantPlugins module Ansible module Cap module Guest module Arch module AnsibleInstall def self.ansible_install(machine, install_mode, ansible_version, pip_args, pip_install_cmd = "") case install_mode when :pip pip_setup machine, pip_install_cmd Pip::pip_install machine, "ansible", ansible_version, pip_args, true when :pip_args_only pip_setup machine, pip_install_cmd Pip::pip_install machine, "", "", pip_args, false else machine.communicate.sudo "pacman -Syy --noconfirm" machine.communicate.sudo "pacman -S --noconfirm ansible" end end private def self.pip_setup(machine, pip_install_cmd = "") machine.communicate.sudo "pacman -Syy --noconfirm" machine.communicate.sudo "pacman -S --noconfirm base-devel curl git python" Pip::get_pip machine, pip_install_cmd end end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/debian/ansible_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../pip/pip" module VagrantPlugins module Ansible module Cap module Guest module Debian module AnsibleInstall def self.ansible_install(machine, install_mode, ansible_version, pip_args, pip_install_cmd = "") case install_mode when :pip pip_setup machine, pip_install_cmd Pip::pip_install machine, "ansible", ansible_version, pip_args, true when :pip_args_only pip_setup machine, pip_install_cmd Pip::pip_install machine, "", "", pip_args, false else ansible_apt_install machine end end private def self.ansible_apt_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 "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --option \"Dpkg::Options::=--force-confold\" ansible" end def self.pip_setup(machine, pip_install_cmd = "") machine.communicate.sudo "apt-get update -y -qq" python_dev_pkg = "python-dev" if machine.communicate.test "apt-cache show python-dev-is-python3" python_dev_pkg = "python-dev-is-python3" end machine.communicate.sudo "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --option \"Dpkg::Options::=--force-confold\" build-essential curl git libssl-dev libffi-dev #{python_dev_pkg}" Pip::get_pip machine, pip_install_cmd end end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/facts.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Ansible module Cap module Guest module Facts def self.dnf?(machine) machine.communicate.test "/usr/bin/which -s dnf" end def self.yum?(machine) machine.communicate.test "/usr/bin/which -s yum" end def self.rpm_package_manager(machine) dnf?(machine) ? "dnf" : "yum" end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/fedora/ansible_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../facts" require_relative "../pip/pip" module VagrantPlugins module Ansible module Cap module Guest module Fedora module AnsibleInstall def self.ansible_install(machine, install_mode, ansible_version, pip_args, pip_install_cmd = "") case install_mode when :pip pip_setup machine, pip_install_cmd Pip::pip_install machine, "ansible", ansible_version, pip_args, true when :pip_args_only pip_setup machine, pip_install_cmd Pip::pip_install machine, "", "", pip_args, false else rpm_package_manager = Facts::rpm_package_manager(machine) machine.communicate.sudo "#{rpm_package_manager} -y install ansible" end end private def self.pip_setup(machine, pip_install_cmd = "") rpm_package_manager = Facts::rpm_package_manager(machine) machine.communicate.sudo "#{rpm_package_manager} install -y curl gcc gmp-devel libffi-devel openssl-devel python-crypto python-devel python-dnf python-setuptools redhat-rpm-config" Pip::get_pip machine, pip_install_cmd end end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/freebsd/ansible_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../errors" module VagrantPlugins module Ansible module Cap module Guest module FreeBSD module AnsibleInstall def self.ansible_install(machine, install_mode, ansible_version, pip_args, pip_install_cmd = "") if install_mode != :default raise Ansible::Errors::AnsiblePipInstallIsNotSupported else machine.communicate.sudo "pkg install -qy py37-ansible" end end end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/pip/pip.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Ansible module Cap module Guest module Pip DEFAULT_PIP_INSTALL_CMD = "curl https://bootstrap.pypa.io/get-pip.py | sudo python".freeze def self.pip_install(machine, package = "", version = "", pip_args = "", upgrade = true) upgrade_arg = "--upgrade" if upgrade version_arg = "" if !version.to_s.empty? && version.to_s.to_sym != :latest version_arg = "==#{version}" end args_array = [pip_args, upgrade_arg, "#{package}#{version_arg}"] args_array.reject! { |a| a.nil? || a.empty? } pip_install = "pip install" pip_install += " #{args_array.join(' ')}" unless args_array.empty? machine.communicate.sudo pip_install end def self.get_pip(machine, pip_install_cmd = DEFAULT_PIP_INSTALL_CMD) # The objective here is to get pip either by default # or by the argument passed in. The objective is not # to circumvent the pip setup by passing in nothing. # Thus, we stick with the default on an empty string. # Typecast added in the check for safety. if pip_install_cmd.to_s.empty? pip_install_cmd = DEFAULT_PIP_INSTALL_CMD end machine.ui.detail I18n.t("vagrant.provisioners.ansible.installing_pip") machine.communicate.execute pip_install_cmd end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/posix/ansible_installed.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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)"' unless version.empty? command << "&& [[ $(python3 -c \"import importlib.metadata; print(importlib.metadata.version('ansible'))\") == \"#{version}\" ]]" end machine.communicate.test command, sudo: false end end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/redhat/ansible_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../facts" require_relative "../pip/pip" module VagrantPlugins module Ansible module Cap module Guest module RedHat module AnsibleInstall def self.ansible_install(machine, install_mode, ansible_version, pip_args, pip_install_cmd = "") case install_mode when :pip pip_setup machine, pip_install_cmd Pip::pip_install machine, "ansible", ansible_version, pip_args, true when :pip_args_only pip_setup machine, pip_install_cmd Pip::pip_install machine, "", "", pip_args, false else ansible_rpm_install machine end end private def self.ansible_rpm_install(machine) rpm_package_manager = Facts::rpm_package_manager(machine) epel = machine.communicate.execute "#{rpm_package_manager} repolist epel | grep -q epel", error_check: false if epel != 0 machine.communicate.sudo "sudo rpm -i #{ansible_epel_download_url(machine)}" end machine.communicate.sudo "#{rpm_package_manager} -y --enablerepo=epel install ansible" end def self.pip_setup(machine, pip_install_cmd = "") rpm_package_manager = Facts::rpm_package_manager(machine) # Use other packages for RHEL > 7 and set alternatives for RHEL 8 machine.communicate.sudo(%Q{ source /etc/os-release MAJOR=$(echo $VERSION_ID | cut -d. -f1) if [ $MAJOR -ge 8 ]; then #{rpm_package_manager} -y install curl gcc libffi-devel openssl-devel python3-cryptography python3-devel python3-setuptools else #{rpm_package_manager} -y install curl gcc libffi-devel openssl-devel python-crypto python-devel python-setuptools fi if [ $MAJOR -eq 8 ]; then alternatives --set python /usr/bin/python3 fi }) # pip is already installed as dependency for RHEL > 7 if machine.communicate.test("test ! -f /usr/bin/pip3") Pip::get_pip machine, pip_install_cmd end # Set pip-alternative for RHEL 8 machine.communicate.sudo(%Q{ source /etc/os-release MAJOR=$(echo $VERSION_ID | cut -d. -f1) if [ $MAJOR -eq 8 ]; then alternatives --install /usr/bin/pip pip /usr/local/bin/pip 1 fi }) end def self.ansible_epel_download_url(machine) dist = "" machine.communicate.execute("rpm -E %dist") do |type, data| dist << data if type == :stdout end dist.strip! dist_major_version = dist.match(/.*el(\d+).*/)&.captures&.first "https://dl.fedoraproject.org/pub/epel/epel-release-latest-#{dist_major_version}.noarch.rpm" end end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/suse/ansible_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../errors" module VagrantPlugins module Ansible module Cap module Guest module SUSE module AnsibleInstall def self.ansible_install(machine, install_mode, ansible_version, pip_args, pip_install_cmd = "") if install_mode != :default raise Ansible::Errors::AnsiblePipInstallIsNotSupported else machine.communicate.sudo("zypper --non-interactive --quiet install ansible") end end end end end end end end ================================================ FILE: plugins/provisioners/ansible/cap/guest/ubuntu/ansible_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../debian/ansible_install" module VagrantPlugins module Ansible module Cap module Guest module Ubuntu module AnsibleInstall def self.ansible_install(machine, install_mode, ansible_version, pip_args, pip_install_cmd = "") if install_mode != :default Debian::AnsibleInstall::ansible_install machine, install_mode, ansible_version, pip_args, pip_install_cmd else ansible_apt_install machine end end private def self.ansible_apt_install(machine) unless machine.communicate.test("test -x \"$(which add-apt-repository)\"") machine.communicate.sudo """ apt-get update -y -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq software-properties-common --option \"Dpkg::Options::=--force-confold\" """ end machine.communicate.sudo """ add-apt-repository ppa:ansible/ansible -y && \ apt-get update -y -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ansible --option \"Dpkg::Options::=--force-confold\" """ end end end end end end end ================================================ FILE: plugins/provisioners/ansible/config/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../constants" module VagrantPlugins module Ansible module Config class Base < Vagrant.plugin("2", :config) GALAXY_COMMAND_DEFAULT = "ansible-galaxy install --role-file=%{role_file} --roles-path=%{roles_path} --force".freeze PLAYBOOK_COMMAND_DEFAULT = "ansible-playbook".freeze attr_accessor :become attr_accessor :become_user attr_accessor :compatibility_mode attr_accessor :config_file attr_accessor :extra_vars attr_accessor :galaxy_role_file attr_accessor :galaxy_roles_path attr_accessor :galaxy_command attr_accessor :groups attr_accessor :host_vars attr_accessor :inventory_path attr_accessor :limit attr_accessor :playbook attr_accessor :playbook_command attr_accessor :raw_arguments attr_accessor :skip_tags attr_accessor :start_at_task attr_accessor :tags attr_accessor :vault_password_file attr_accessor :verbose attr_accessor :version # # Deprecated options # alias :sudo :become def sudo=(value) show_deprecation_info 'sudo', 'become' @become = value end alias :sudo_user :become_user def sudo_user=(value) show_deprecation_info 'sudo_user', 'become_user' @become_user = value end def initialize @become = UNSET_VALUE @become_user = UNSET_VALUE @compatibility_mode = Ansible::COMPATIBILITY_MODE_AUTO @config_file = UNSET_VALUE @extra_vars = UNSET_VALUE @galaxy_role_file = UNSET_VALUE @galaxy_roles_path = UNSET_VALUE @galaxy_command = UNSET_VALUE @groups = UNSET_VALUE @host_vars = UNSET_VALUE @inventory_path = UNSET_VALUE @limit = UNSET_VALUE @playbook = UNSET_VALUE @playbook_command = UNSET_VALUE @raw_arguments = UNSET_VALUE @skip_tags = UNSET_VALUE @start_at_task = UNSET_VALUE @tags = UNSET_VALUE @vault_password_file = UNSET_VALUE @verbose = UNSET_VALUE @version = UNSET_VALUE end def finalize! @become = false if @become != true @become_user = nil if @become_user == UNSET_VALUE @compatibility_mode = nil unless Ansible::COMPATIBILITY_MODES.include?(@compatibility_mode) @config_file = nil if @config_file == UNSET_VALUE @extra_vars = nil if @extra_vars == UNSET_VALUE @galaxy_role_file = nil if @galaxy_role_file == UNSET_VALUE @galaxy_roles_path = nil if @galaxy_roles_path == UNSET_VALUE @galaxy_command = GALAXY_COMMAND_DEFAULT if @galaxy_command == UNSET_VALUE @groups = {} if @groups == UNSET_VALUE @host_vars = {} if @host_vars == UNSET_VALUE @inventory_path = nil if @inventory_path == UNSET_VALUE @limit = nil if @limit == UNSET_VALUE @playbook = nil if @playbook == UNSET_VALUE @playbook_command = PLAYBOOK_COMMAND_DEFAULT if @playbook_command == 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 @tags = nil if @tags == UNSET_VALUE @vault_password_file = nil if @vault_password_file == UNSET_VALUE @verbose = false if @verbose == UNSET_VALUE @version = "" if @version == UNSET_VALUE end # Just like the normal configuration "validate" method except that # it returns an array of errors that should be merged into some # other error accumulator. def validate(machine) @errors = _detected_errors # Validate that a compatibility mode was provided if !compatibility_mode @errors << I18n.t("vagrant.provisioners.ansible.errors.no_compatibility_mode", valid_modes: Ansible::COMPATIBILITY_MODES.map { |s| "'#{s}'" }.join(', ')) end # Validate that a playbook path was provided if !playbook @errors << I18n.t("vagrant.provisioners.ansible.errors.no_playbook") end # Validate that extra_vars is either a Hash or a String (for a file path) 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 = '@' + extra_vars_path end if !extra_vars_is_valid @errors << I18n.t( "vagrant.provisioners.ansible.errors.extra_vars_invalid", type: extra_vars.class.to_s, value: extra_vars.to_s) end end if raw_arguments if raw_arguments.kind_of?(String) @raw_arguments = [raw_arguments] elsif !raw_arguments.kind_of?(Array) @errors << I18n.t( "vagrant.provisioners.ansible.errors.raw_arguments_invalid", type: raw_arguments.class.to_s, value: raw_arguments.to_s) end end end protected def show_deprecation_info(deprecated_option, new_option) puts "DEPRECATION: The '#{deprecated_option}' option for the Ansible provisioner is deprecated." puts "Please use the '#{new_option}' option instead." puts "The '#{deprecated_option}' option will be removed in a future release of Vagrant.\n\n" end end end end end ================================================ FILE: plugins/provisioners/ansible/config/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "base" require_relative "../helpers" module VagrantPlugins module Ansible module Config class Guest < Base attr_accessor :provisioning_path attr_accessor :tmp_path attr_accessor :install attr_accessor :install_mode attr_accessor :pip_args attr_accessor :pip_install_cmd def initialize super @install = UNSET_VALUE @install_mode = UNSET_VALUE @pip_args = UNSET_VALUE @pip_install_cmd = UNSET_VALUE @provisioning_path = UNSET_VALUE @tmp_path = UNSET_VALUE end def finalize! super @install = true if @install == UNSET_VALUE @install_mode = :default if @install_mode == UNSET_VALUE @pip_args = "" if @pip_args == UNSET_VALUE @pip_install_cmd = "" if @pip_install_cmd == UNSET_VALUE @provisioning_path = "/vagrant" if provisioning_path == UNSET_VALUE @tmp_path = "/tmp/vagrant-ansible" if tmp_path == UNSET_VALUE end def validate(machine) super case @install_mode.to_s.to_sym when :pip @install_mode = :pip when :pip_args_only @install_mode = :pip_args_only else @install_mode = :default end { "ansible local provisioner" => @errors } end end end end end ================================================ FILE: plugins/provisioners/ansible/config/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "base" module VagrantPlugins module Ansible module Config class Host < Base attr_accessor :ask_become_pass attr_accessor :ask_vault_pass attr_accessor :force_remote_user attr_accessor :host_key_checking attr_accessor :raw_ssh_args # # Deprecated options # alias :ask_sudo_pass :ask_become_pass def ask_sudo_pass=(value) show_deprecation_info 'ask_sudo_pass', 'ask_become_pass' @ask_become_pass = value end def initialize super @ask_become_pass = false @ask_vault_pass = false @force_remote_user = true @host_key_checking = false @raw_ssh_args = UNSET_VALUE end def finalize! super @ask_become_pass = false if @ask_become_pass != true @ask_vault_pass = false if @ask_vault_pass != true @force_remote_user = true if @force_remote_user != false @host_key_checking = false if @host_key_checking != true @raw_ssh_args = nil if @raw_ssh_args == UNSET_VALUE end def validate(machine) super if raw_ssh_args if raw_ssh_args.kind_of?(String) @raw_ssh_args = [raw_ssh_args] elsif !raw_ssh_args.kind_of?(Array) @errors << I18n.t( "vagrant.provisioners.ansible.errors.raw_ssh_args_invalid", type: raw_ssh_args.class.to_s, value: raw_ssh_args.to_s) end end { "ansible remote provisioner" => @errors } end end end end end ================================================ FILE: plugins/provisioners/ansible/constants.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Ansible COMPATIBILITY_MODE_AUTO = "auto".freeze COMPATIBILITY_MODE_V1_8 = "1.8".freeze COMPATIBILITY_MODE_V2_0 = "2.0".freeze SAFE_COMPATIBILITY_MODE = COMPATIBILITY_MODE_V1_8 COMPATIBILITY_MODES = [ COMPATIBILITY_MODE_AUTO, COMPATIBILITY_MODE_V1_8, COMPATIBILITY_MODE_V2_0, ].freeze end end ================================================ FILE: plugins/provisioners/ansible/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Ansible module Errors class AnsibleError < Vagrant::Errors::VagrantError error_namespace("vagrant.provisioners.ansible.errors") end class AnsibleCommandFailed < AnsibleError error_key(:ansible_command_failed) end class AnsibleCompatibilityModeConflict < AnsibleError error_key(:ansible_compatibility_mode_conflict) end class AnsibleNotFoundOnGuest < AnsibleError error_key(:ansible_not_found_on_guest) end class AnsibleNotFoundOnHost < AnsibleError error_key(:ansible_not_found_on_host) end class AnsiblePipInstallIsNotSupported < AnsibleError error_key(:cannot_support_pip_install) end class AnsibleProgrammingError < AnsibleError error_key(:ansible_programming_error) end class AnsibleVersionMismatch < AnsibleError error_key(:ansible_version_mismatch) end end end end ================================================ FILE: plugins/provisioners/ansible/helpers.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Ansible class Helpers def self.expand_path_in_unix_style(path, base_dir) # Remove the possible drive letter, which is added # by `File.expand_path` when running on a Windows host File.expand_path(path, base_dir).sub(/^[a-zA-Z]:/, "") end def self.as_list_argument(v) v.kind_of?(Array) ? v.join(',') : v end end end end ================================================ FILE: plugins/provisioners/ansible/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Ansible class Plugin < Vagrant.plugin("2") name "ansible" description <<-DESC 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_relative "config/host" Config::Host end 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(:alpine, :ansible_install) do require_relative "cap/guest/alpine/ansible_install" Cap::Guest::Alpine::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(:fedora, :ansible_install) do require_relative "cap/guest/fedora/ansible_install" Cap::Guest::Fedora::AnsibleInstall end guest_capability(:redhat, :ansible_install) do require_relative "cap/guest/redhat/ansible_install" Cap::Guest::RedHat::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 ================================================ FILE: plugins/provisioners/ansible/provisioner/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../constants" 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) RANGE_PATTERN = %r{(?:\[[a-z]:[a-z]\]|\[[0-9]+?:[0-9]+?\])}.freeze ANSIBLE_PARAMETER_NAMES = { Ansible::COMPATIBILITY_MODE_V1_8 => { ansible_host: "ansible_ssh_host", ansible_password: "ansible_ssh_pass", ansible_port: "ansible_ssh_port", ansible_user: "ansible_ssh_user", ask_become_pass: "ask-sudo-pass", become: "sudo", become_user: "sudo-user", }, Ansible::COMPATIBILITY_MODE_V2_0 => { ansible_host: "ansible_host", ansible_password: "ansible_password", ansible_port: "ansible_port", ansible_user: "ansible_user", ask_become_pass: "ask-become-pass", become: "become", become_user: "become-user", } } protected def initialize(machine, config) super @control_machine = nil @command_arguments = [] @environment_variables = {} @inventory_machines = {} @inventory_path = nil @gathered_version_stdout = nil @gathered_version_major = nil @gathered_version = nil @ansible_package_version_map = {} end def set_and_check_compatibility_mode begin set_gathered_ansible_version(gather_ansible_version("ansible")) rescue StandardError => e # Nothing to do here, as the fallback on safe compatibility_mode is done below @logger.error("Error while gathering the ansible version: #{e.to_s}") end begin set_gathered_ansible_package_version("ansible-core", gather_ansible_version("ansible-core")) rescue StandardError => e @logger.error("Error while gathering the ansible-core version: #{e}") end if @gathered_version_major if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO detect_compatibility_mode elsif @gathered_version_major.to_i < 2 && config.compatibility_mode == Ansible::COMPATIBILITY_MODE_V2_0 # A better version comparator will be needed # when more compatibility modes come... but so far let's keep it simple! raise Ansible::Errors::AnsibleCompatibilityModeConflict, ansible_version: @gathered_version, system: @control_machine, compatibility_mode: config.compatibility_mode end end if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO config.compatibility_mode = Ansible::SAFE_COMPATIBILITY_MODE @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.compatibility_mode_not_detected", compatibility_mode: config.compatibility_mode, gathered_version: @gathered_version_stdout) + "\n") end unless Ansible::COMPATIBILITY_MODES.slice(1..-1).include?(config.compatibility_mode) raise Ansible::Errors::AnsibleProgrammingError, message: "The config.compatibility_mode must be correctly set at this stage!", details: "config.compatibility_mode: '#{config.compatibility_mode}'" end @lexicon = ANSIBLE_PARAMETER_NAMES[config.compatibility_mode] end def check_files_existence check_path_is_a_file(config.playbook, :playbook) check_path_exists(config.inventory_path, :inventory_path) if config.inventory_path check_path_is_a_file(config.config_file, :config_file) if config.config_file check_path_is_a_file(config.extra_vars[1..-1], :extra_vars) if has_an_extra_vars_file_argument check_path_is_a_file(config.galaxy_role_file, :galaxy_role_file) if config.galaxy_role_file check_path_is_a_file(config.vault_password_file, :vault_password_file) if config.vault_password_file end def get_environment_variables_for_shell_execution shell_env_vars = [] @environment_variables.each_pair do |k, v| if k =~ /ANSIBLE_SSH_ARGS|ANSIBLE_ROLES_PATH|ANSIBLE_CONFIG/ shell_env_vars << "#{k}='#{v}'" else shell_env_vars << "#{k}=#{v}" end end shell_env_vars end def ansible_galaxy_command_for_shell_execution command_values = { role_file: "'#{get_galaxy_role_file}'", roles_path: "'#{get_galaxy_roles_path}'" } shell_command = get_environment_variables_for_shell_execution shell_command << config.galaxy_command % command_values shell_command.flatten.join(' ') end def ansible_playbook_command_for_shell_execution shell_command = get_environment_variables_for_shell_execution shell_command << config.playbook_command shell_args = [] @command_arguments.each do |arg| if arg =~ /(--start-at-task|--limit)=(.+)/ shell_args << %Q(#{$1}="#{$2}") elsif arg =~ /(--extra-vars)=(.+)/ shell_args << %Q(%s=%s) % [$1, $2.shellescape] else shell_args << arg end end shell_command << shell_args # Add the raw arguments at the end, to give them the highest precedence shell_command << config.raw_arguments if config.raw_arguments shell_command << config.playbook shell_command.flatten.join(' ') 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 << get_inventory_argument(inventory_path) @command_arguments << "--extra-vars=#{extra_vars_argument}" if config.extra_vars @command_arguments << "--#{@lexicon[:become]}" if config.become @command_arguments << "--#{@lexicon[:become_user]}=#{config.become_user}" if config.become_user @command_arguments << "#{verbosity_argument}" if verbosity_is_enabled? @command_arguments << "--vault-password-file=#{config.vault_password_file}" if config.vault_password_file @command_arguments << "--tags=#{Helpers::as_list_argument(config.tags)}" if config.tags @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 end def get_inventory_argument(inventory_path) ansible_core_version = @ansible_package_version_map["ansible-core"] # Default to --inventory-file if version info is not available return "--inventory-file=#{inventory_path}" if ansible_core_version.nil? || ansible_core_version.empty? major = ansible_core_version[:major].to_i minor = ansible_core_version[:minor].to_i # Use --inventory for ansible-core >= 2.19 if major > 2 || (major == 2 && minor >= 19) "--inventory=#{inventory_path}" else "--inventory-file=#{inventory_path}" end 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? # Use ANSIBLE_ROLES_PATH to tell ansible-playbook where to look for roles # (there is no equivalent command line argument in ansible-playbook) @environment_variables["ANSIBLE_ROLES_PATH"] = get_galaxy_roles_path if config.galaxy_roles_path prepare_ansible_config_environment_variable end def prepare_ansible_config_environment_variable @environment_variables["ANSIBLE_CONFIG"] = config.config_file if config.config_file 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 get_inventory_host_vars_string(machine_name) # In Ruby, Symbol and String values are different, but # Vagrant has to unify them for better user experience. vars = config.host_vars[machine_name.to_sym] if !vars vars = config.host_vars[machine_name.to_s] end s = nil if vars.is_a?(Hash) s = vars.each.collect { |k, v| if v.is_a?(String) && v.include?(' ') && !v.match(/^('|")[^'"]+('|")$/) v = %Q('#{v}') end "#{k}=#{v}" }.join(" ") elsif vars.is_a?(Array) s = vars.join(" ") elsif vars.is_a?(String) s = vars end if s and !s.empty? then s else nil 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. def generate_inventory_groups groups_of_groups = {} defined_groups = [] group_vars = {} inventory_groups = "" # Verify if host range patterns exist and warn if config.groups.any? { |gm| gm.to_s[RANGE_PATTERN] } @machine.ui.warn(I18n.t("vagrant.provisioners.ansible.ansible_host_pattern_detected")) end config.groups.each_pair do |gname, gmembers| if gname.is_a?(Symbol) gname = gname.to_s end if gmembers.is_a?(String) gmembers = gmembers.split(/\s+/) elsif gmembers.is_a?(Hash) gmembers = gmembers.each.collect{ |k, v| "#{k}=#{v}" } elsif !gmembers.is_a?(Array) gmembers = [] end if gname.end_with?(":children") groups_of_groups[gname] = gmembers defined_groups << gname.sub(/:children$/, '') elsif gname.end_with?(":vars") group_vars[gname] = gmembers else defined_groups << gname inventory_groups += "\n[#{gname}]\n" gmembers.each do |gm| # TODO : Expand and validate host range patterns # against @inventory_machines list before adding them # otherwise abort with an error message if gm[RANGE_PATTERN] inventory_groups += "#{gm}\n" end 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 group_vars.each_pair do |gname, gmembers| if defined_groups.include?(gname.sub(/:vars$/, "")) || gname == "all:vars" inventory_groups += "\n[#{gname}]\n" + gmembers.join("\n") + "\n" end end return inventory_groups end def has_an_extra_vars_file_argument config.extra_vars && config.extra_vars.kind_of?(String) && config.extra_vars =~ /^@.+$/ end def extra_vars_argument if has_an_extra_vars_file_argument # 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 get_galaxy_role_file Helpers::expand_path_in_unix_style(config.galaxy_role_file, get_provisioning_working_directory) end def get_galaxy_roles_path base_dir = get_provisioning_working_directory if config.galaxy_roles_path Helpers::expand_path_in_unix_style(config.galaxy_roles_path, base_dir) else playbook_path = Helpers::expand_path_in_unix_style(config.playbook, base_dir) File.join(Pathname.new(playbook_path).parent, 'roles') end end def ui_running_ansible_command(name, command) @machine.ui.detail I18n.t("vagrant.provisioners.ansible.running_#{name}") if verbosity_is_enabled? # Show the ansible command in use @machine.env.ui.detail command end end def verbosity_is_enabled? config.verbose && !config.verbose.to_s.empty? end def verbosity_argument if config.verbose.to_s =~ /^-?(v+)$/ "-#{$+}" else # safe default, in case input strays '-v' end end private def detect_compatibility_mode if !@gathered_version_major || config.compatibility_mode != Ansible::COMPATIBILITY_MODE_AUTO raise Ansible::Errors::AnsibleProgrammingError, message: "The detect_compatibility_mode() function shouldn't have been called!", details: %Q(config.compatibility_mode: '#{config.compatibility_mode}' gathered version major number: '#{@gathered_version_major}' gathered version stdout version: #{@gathered_version_stdout}) end if @gathered_version_major.to_i <= 1 config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V1_8 else config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V2_0 end @logger.info(I18n.t("vagrant.provisioners.ansible.compatibility_mode_warning", compatibility_mode: config.compatibility_mode, ansible_version: @gathered_version) + "\n") end def set_gathered_ansible_version(stdout_output) @gathered_version_stdout = stdout_output if !@gathered_version_stdout.empty? first_line = @gathered_version_stdout.lines[0] ansible_version_pattern = first_line.match(/(^ansible\s+)(.+)$/) if ansible_version_pattern _, @gathered_version, _ = ansible_version_pattern.captures @gathered_version.strip! if @gathered_version @gathered_version_major = @gathered_version.match(/(\d+)\..+$/).captures[0].to_i end end end end def set_gathered_ansible_package_version(ansible_package, stdout_output) if !stdout_output.empty? first_line = stdout_output.lines[0] ansible_version_pattern = first_line.match(/^#{ansible_package}\s+(.+)$/) if ansible_version_pattern gathered_version = ansible_version_pattern.captures.first gathered_version.strip! if !gathered_version.empty? gathered_version_major = gathered_version.match(/(\d+)\..+$/).captures[0].to_i gathered_version_minor = gathered_version.match(/\d+\.(\d+)\..+$/).captures[0].to_i @ansible_package_version_map[ansible_package] = { version: gathered_version, major: gathered_version_major, minor: gathered_version_minor } end end end end end end end end ================================================ FILE: plugins/provisioners/ansible/provisioner/guest.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "base" module VagrantPlugins module Ansible module Provisioner class Guest < Base include Vagrant::Util def initialize(machine, config) super @control_machine = "guest" @logger = Log4r::Logger.new("vagrant::provisioners::ansible_guest") end def provision check_files_existence check_and_install_ansible execute_ansible_galaxy_on_guest if config.galaxy_role_file execute_ansible_playbook_on_guest end 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. # The compatibility mode checks are also performed here in order to fetch the # Ansible version information only once. # # Current limitations: # - The installation of a specific Ansible version is only supported by # the "pip" install_mode. Note that "pip" installation also takes place # via a default command. If pip needs to be installed differently then # the command can be overwritten by supplying "pip_install_cmd" in the # config settings. # - There is no absolute guarantee that the automated installation will replace # a previous Ansible installation (although it works fine in many cases) # def check_and_install_ansible @logger.info("Checking for Ansible installation...") # 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.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, config.install_mode, config.version, config.pip_args, config.pip_install_cmd) end # This step will also fetch the Ansible version data into related instance variables set_and_check_compatibility_mode # Check if requested ansible version is available if (!config.version.empty? && config.version.to_s.to_sym != :latest && config.version != @gathered_version) raise Ansible::Errors::AnsibleVersionMismatch, system: @control_machine, required_version: config.version, current_version: @gathered_version end end def gather_ansible_version(package = 'ansible') raw_output = "" result = @machine.communicate.execute( "python3 -c \"import importlib.metadata; print('#{package} ' + importlib.metadata.version('#{package}'))\"", error_class: Ansible::Errors::AnsibleNotFoundOnGuest, error_key: :ansible_not_found_on_guest) do |type, output| if type == :stdout && output.lines[0] raw_output = output.lines[0] end end if result != 0 raw_output = "" end raw_output end def get_provisioning_working_directory config.provisioning_path end def execute_ansible_galaxy_on_guest prepare_ansible_config_environment_variable execute_ansible_command_on_guest "galaxy", ansible_galaxy_command_for_shell_execution end def execute_ansible_playbook_on_guest prepare_common_command_arguments prepare_common_environment_variables execute_ansible_command_on_guest "playbook", ansible_playbook_command_for_shell_execution end def execute_ansible_command_on_guest(name, command) remote_command = "cd #{config.provisioning_path} && #{command}" ui_running_ansible_command name, remote_command result = execute_on_guest(remote_command) raise Ansible::Errors::AnsibleCommandFailed 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") @machine.communicate.sudo("mkdir -p #{inventory_basedir}") @machine.communicate.sudo("chown -R -h #{@machine.ssh_info[:username]} #{config.tmp_path}") @machine.communicate.sudo("rm -f #{inventory_path}", error_check: false) Tempfile.open("vagrant-ansible-local-inventory-#{@machine.name}") do |f| f.binmode f.write(inventory_content) f.fsync f.close @machine.communicate.upload(f.path, inventory_path) end return inventory_basedir 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 host_vars = get_inventory_host_vars_string(machine_name) machines.sub!(/\n$/, " #{host_vars}\n") if host_vars end end return machines end def check_path(path, test_args, option_name) # Checks for the existence of given file (or directory) on the guest system, # and error if it doesn't exist. remote_path = Helpers::expand_path_in_unix_style(path, config.provisioning_path) command = "test #{test_args} '#{remote_path}'" @machine.communicate.execute( command, error_class: Ansible::Errors::AnsibleError, error_key: :config_file_not_found, config_option: option_name, path: remote_path, system: @control_machine ) end def check_path_is_a_file(path, error_message_key) check_path(path, "-f", error_message_key) end def check_path_exists(path, error_message_key) check_path(path, "-e", error_message_key) end end end end end ================================================ FILE: plugins/provisioners/ansible/provisioner/host.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "thread" require_relative "base" module VagrantPlugins module Ansible module Provisioner class Host < Base @@lock = Mutex.new def initialize(machine, config) super @control_machine = "host" @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 check_files_existence check_ansible_version_and_compatibility execute_ansible_galaxy_from_host if config.galaxy_role_file execute_ansible_playbook_from_host end protected VAGRANT_ARG_SEPARATOR = 'VAGRANT_ARG_SEP' 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") + "\n") end end def check_ansible_version_and_compatibility # This step will also fetch the Ansible version data into related instance variables set_and_check_compatibility_mode # Skip this check when not required, nor possible if !@gathered_version || config.version.empty? || config.version.to_s.to_sym == :latest return end if config.version != @gathered_version raise Ansible::Errors::AnsibleVersionMismatch, system: @control_machine, required_version: config.version, current_version: @gathered_version end end def prepare_command_arguments # 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" if !config.force_remote_user # Pass the vagrant ssh username as Ansible default remote user, because # the ansible_ssh_user/ansible_user parameter won't be added to the auto-generated inventory. @command_arguments << "--user=#{@ssh_info[:username]}" elsif config.inventory_path # Using an extra variable is the only way to ensure that the Ansible remote user # is overridden (as the ansible inventory is not under vagrant control) @command_arguments << "--extra-vars=#{@lexicon[:ansible_user]}='#{@ssh_info[:username]}'" end @command_arguments << "--#{@lexicon[:ask_become_pass]}" if config.ask_become_pass @command_arguments << "--ask-vault-pass" if config.ask_vault_pass prepare_common_command_arguments 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_command_from_host(command) begin result = Vagrant::Util::Subprocess.execute(*command) do |type, data| if type == :stdout || type == :stderr @machine.env.ui.detail(data, new_line: false, prefix: false) end end raise Ansible::Errors::AnsibleCommandFailed if result.exit_code != 0 rescue Vagrant::Errors::CommandUnavailable raise Ansible::Errors::AnsibleNotFoundOnHost end end def gather_ansible_version(package = 'ansible') raw_output = '' command = ['python3', '-c', "import importlib.metadata; print('#{package} ' + importlib.metadata.version('#{package}'))"] command << { notify: [:stdout, :stderr] } begin result = Vagrant::Util::Subprocess.execute(*command) do |type, output| if type == :stdout && output.lines[0] raw_output = output end end if result.exit_code != 0 raw_output = "" end rescue Vagrant::Errors::CommandUnavailable raise Ansible::Errors::AnsibleNotFoundOnHost end raw_output end def execute_ansible_galaxy_from_host prepare_ansible_config_environment_variable command_values = { role_file: get_galaxy_role_file, roles_path: get_galaxy_roles_path } command_template = config.galaxy_command.gsub(' ', VAGRANT_ARG_SEPARATOR) str_command = command_template % command_values command = str_command.split(VAGRANT_ARG_SEPARATOR) command << { env: @environment_variables, # Write stdout and stderr data, since it's the regular Ansible output notify: [:stdout, :stderr], workdir: @machine.env.root_path.to_s } ui_running_ansible_command "galaxy", ansible_galaxy_command_for_shell_execution execute_command_from_host command end def execute_ansible_playbook_from_host prepare_environment_variables prepare_command_arguments # Assemble the full ansible-playbook command command = [config.playbook_command] << @command_arguments # Add the raw arguments at the end, to give them the highest precedence command << config.raw_arguments if config.raw_arguments command << config.playbook command = command.flatten command << { env: @environment_variables, # Write stdout and stderr data, since it's the regular Ansible output notify: [:stdout, :stderr], workdir: @machine.env.root_path.to_s } ui_running_ansible_command "playbook", ansible_playbook_command_for_shell_execution execute_command_from_host command 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.exist?(inventory_file) or inventory_content != File.read(inventory_file) begin # ansible dir inventory will ignore files starting with '.' inventory_tmpfile = Tempfile.new('.vagrant_ansible_inventory', inventory_path) inventory_tmpfile.write(inventory_content) inventory_tmpfile.close File.rename(inventory_tmpfile.path, inventory_file) ensure inventory_tmpfile.close inventory_tmpfile.unlink 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) # Call only once the SSH and WinRM info computation # Note that machines configured with WinRM communicator, also have a "partial" ssh_info. m_ssh_info = m.ssh_info host_vars = get_inventory_host_vars_string(m.name) if m.config.vm.communicator == :winrm m_winrm_net_info = CommunicatorWinRM::Helper.winrm_info(m) # can raise a WinRMNotReady exception... machines += get_inventory_winrm_machine(m, m_winrm_net_info) machines.sub!(/\n$/, " #{host_vars}\n") if host_vars @inventory_machines[m.name] = m elsif !m_ssh_info.nil? machines += get_inventory_ssh_machine(m, m_ssh_info) machines.sub!(/\n$/, " #{host_vars}\n") if host_vars @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, CommunicatorWinRM::Errors::WinRMNotReady => 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 get_provisioning_working_directory machine.env.root_path end def get_inventory_ssh_machine(machine, ssh_info) forced_remote_user = "" if config.force_remote_user forced_remote_user = "#{@lexicon[:ansible_user]}='#{ssh_info[:username]}' " end "#{machine.name} #{@lexicon[:ansible_host]}=#{ssh_info[:host]} #{@lexicon[:ansible_port]}=#{ssh_info[:port]} #{forced_remote_user}ansible_ssh_private_key_file='#{ssh_info[:private_key_path][0]}'\n" end def get_inventory_winrm_machine(machine, winrm_net_info) forced_remote_user = "" if config.force_remote_user forced_remote_user = "#{@lexicon[:ansible_user]}='#{machine.config.winrm.username}' " end "#{machine.name} ansible_connection=winrm #{@lexicon[:ansible_host]}=#{winrm_net_info[:host]} #{@lexicon[:ansible_port]}=#{winrm_net_info[:port]} #{forced_remote_user}#{@lexicon[:ansible_password]}='#{machine.config.winrm.password}'\n" end def ansible_ssh_args @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 }'" # TODO ssh_options << "-o ProxyCommand=\"#{ proxy_cmd }\"" end # Use an SSH ProxyCommand when corresponding Vagrant setting is defined if @machine.ssh_info[:proxy_command] proxy_cmd = @machine.ssh_info[:proxy_command] 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 # Compare to lib/vagrant/util/ssh.rb ssh_options << "-o IdentitiesOnly=yes" if !Vagrant::Util::Platform.solaris? && @ssh_info[:keys_only] # 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=%s" % [key.gsub('%', '%%')]] end end # SSH Forwarding ssh_options << "-o ForwardAgent=yes" if @ssh_info[:forward_agent] # Unchecked SSH Parameters ssh_options.concat(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 check_path(path, path_test_method, option_name) # Checks for the existence of given file (or directory) on the host system, # and error if it doesn't exist. expanded_path = Pathname.new(path).expand_path(@machine.env.root_path) if !expanded_path.public_send(path_test_method) raise Ansible::Errors::AnsibleError, _key: :config_file_not_found, config_option: option_name, path: expanded_path, system: @control_machine end end def check_path_is_a_file(path, option_name) check_path(path, "file?", option_name) end def check_path_exists(path, option_name) check_path(path, "exist?", option_name) end end end end end ================================================ FILE: plugins/provisioners/cfengine/cap/debian/cfengine_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CFEngine module Cap module Debian module CFEngineInstall def self.cfengine_install(machine, config) machine.communicate.tap do |comm| comm.sudo("mkdir -p #{File.dirname(config.deb_repo_file)} && /bin/echo #{config.deb_repo_line} > #{config.deb_repo_file}") comm.sudo("GPGFILE=`tempfile`; wget -O $GPGFILE #{config.repo_gpg_key_url} && apt-key add $GPGFILE; rm -f $GPGFILE") comm.sudo("apt-get install -y --force-yes apt-transport-https"); comm.sudo("apt-get update") comm.sudo("apt-get install -y #{config.package_name}") end end end end end end end ================================================ FILE: plugins/provisioners/cfengine/cap/linux/cfengine_installed.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CFEngine module Cap module Linux module CFEngineInstalled def self.cfengine_installed(machine) machine.communicate.test( "test -d /var/cfengine && test -x /var/cfengine/bin/cf-agent", sudo: true) end end end end end end ================================================ FILE: plugins/provisioners/cfengine/cap/linux/cfengine_needs_bootstrap.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module CFEngine module Cap module Linux module CFEngineNeedsBootstrap def self.cfengine_needs_bootstrap(machine, config) logger = Log4r::Logger.new("vagrant::plugins::cfengine::cap_linux_cfengine_bootstrap") machine.communicate.tap do |comm| # We hardcode fixing the permissions on /var/cfengine/ppkeys/, if it exists, # because otherwise CFEngine will fail to bootstrap. if comm.test("test -d /var/cfengine/ppkeys", sudo: true) logger.debug("Fixing permissions on /var/cfengine/ppkeys") comm.sudo("chmod -R 600 /var/cfengine/ppkeys") end logger.debug("Checking if CFEngine is bootstrapped...") bootstrapped = comm.test("test -f /var/cfengine/policy_server.dat", sudo: true) if bootstrapped && !config.force_bootstrap logger.info("CFEngine already bootstrapped, no need to do it again") return false end logger.info("CFEngine needs bootstrap.") return true end end end end end end end ================================================ FILE: plugins/provisioners/cfengine/cap/redhat/cfengine_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module CFEngine module Cap module RedHat module CFEngineInstall def self.cfengine_install(machine, config) logger = Log4r::Logger.new("vagrant::plugins::cfengine::cap_redhat_cfengine_install") machine.communicate.tap do |comm| logger.info("Adding the CFEngine repository to #{config.yum_repo_file}") comm.sudo("mkdir -p #{File.dirname(config.yum_repo_file)} && (echo '[cfengine-repository]'; echo 'name=CFEngine Community Yum Repository'; echo 'baseurl=#{config.yum_repo_url}'; echo 'enabled=1'; echo 'gpgcheck=1') > #{config.yum_repo_file}") 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") 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 end end ================================================ FILE: plugins/provisioners/cfengine/cap/suse/cfengine_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CFEngine module Cap module SUSE module CFEngineInstall def self.cfengine_install(machine, config) machine.communicate.tap do |comm| comm.sudo("rpm --import #{config.repo_gpg_key_url}") comm.sudo("zypper addrepo -t YUM #{config.yum_repo_url} CFEngine") comm.sudo("zypper se #{config.package_name} && zypper -n install #{config.package_name}") end end end end end end end ================================================ FILE: plugins/provisioners/cfengine/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" module VagrantPlugins module CFEngine class Config < Vagrant.plugin("2", :config) attr_accessor :am_policy_hub attr_accessor :extra_agent_args attr_accessor :classes attr_accessor :deb_repo_file attr_accessor :deb_repo_line attr_accessor :files_path attr_accessor :force_bootstrap attr_accessor :install attr_accessor :mode attr_accessor :policy_server_address attr_accessor :repo_gpg_key_url attr_accessor :run_file attr_accessor :upload_path attr_accessor :yum_repo_file attr_accessor :yum_repo_url attr_accessor :package_name def initialize @am_policy_hub = UNSET_VALUE @classes = UNSET_VALUE @deb_repo_file = UNSET_VALUE @deb_repo_line = UNSET_VALUE @extra_agent_args = UNSET_VALUE @files_path = UNSET_VALUE @force_bootstrap = UNSET_VALUE @install = UNSET_VALUE @mode = UNSET_VALUE @policy_server_address = UNSET_VALUE @repo_gpg_key_url = UNSET_VALUE @run_file = UNSET_VALUE @upload_path = UNSET_VALUE @yum_repo_file = UNSET_VALUE @yum_repo_url = UNSET_VALUE @package_name = UNSET_VALUE end def finalize! @am_policy_hub = false if @am_policy_hub == UNSET_VALUE @classes = nil if @classes == UNSET_VALUE if @deb_repo_file == UNSET_VALUE @deb_repo_file = "/etc/apt/sources.list.d/cfengine-community.list" end if @deb_repo_line == UNSET_VALUE @deb_repo_line = "deb https://cfengine.com/pub/apt/packages stable main" end @extra_agent_args = nil if @extra_agent_args == UNSET_VALUE @files_path = nil if @files_path == UNSET_VALUE @force_bootstrap = false if @force_bootstrap == UNSET_VALUE @install = true if @install == UNSET_VALUE @install = @install.to_sym if @install.respond_to?(:to_sym) @mode = :bootstrap if @mode == UNSET_VALUE @mode = @mode.to_sym @run_file = nil if @run_file == UNSET_VALUE @policy_server_address = nil if @policy_server_address == UNSET_VALUE if @repo_gpg_key_url == UNSET_VALUE @repo_gpg_key_url = "https://cfengine.com/pub/gpg.key" end @upload_path = "/tmp/vagrant-cfengine-file" if @upload_path == UNSET_VALUE if @yum_repo_file == UNSET_VALUE @yum_repo_file = "/etc/yum.repos.d/cfengine-community.repo" end if @yum_repo_url == UNSET_VALUE @yum_repo_url = "https://cfengine.com/pub/yum/$basearch" end if @package_name == UNSET_VALUE @package_name = "cfengine-community" end end def validate(machine) errors = _detected_errors valid_modes = [:bootstrap, :single_run] errors << I18n.t("vagrant.cfengine_config.invalid_mode") if !valid_modes.include?(@mode) if @mode == :bootstrap if !@policy_server_address && !@am_policy_hub errors << I18n.t("vagrant.cfengine_config.policy_server_address") end end if @classes && !@classes.is_a?(Array) errors << I18n.t("vagrant.cfengine_config.classes_array") end if @files_path expanded = Pathname.new(@files_path).expand_path(machine.env.root_path) if !expanded.directory? errors << I18n.t("vagrant.cfengine_config.files_path_not_directory") end end if @run_file expanded = Pathname.new(@run_file).expand_path(machine.env.root_path) if !expanded.file? errors << I18n.t("vagrant.cfengine_config.run_file_not_found") end end { "CFEngine" => errors } end end end end ================================================ FILE: plugins/provisioners/cfengine/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module CFEngine class Plugin < Vagrant.plugin("2") name "CFEngine Provisioner" description <<-DESC Provisions machines with CFEngine. DESC config(:cfengine, :provisioner) do require_relative "config" Config end guest_capability("debian", "cfengine_install") do require_relative "cap/debian/cfengine_install" Cap::Debian::CFEngineInstall end guest_capability("linux", "cfengine_needs_bootstrap") do require_relative "cap/linux/cfengine_needs_bootstrap" Cap::Linux::CFEngineNeedsBootstrap end guest_capability("linux", "cfengine_installed") do require_relative "cap/linux/cfengine_installed" Cap::Linux::CFEngineInstalled end guest_capability("redhat", "cfengine_install") do require_relative "cap/redhat/cfengine_install" Cap::RedHat::CFEngineInstall end guest_capability("suse", "cfengine_install") do require_relative "cap/suse/cfengine_install" Cap::SUSE::CFEngineInstall end provisioner(:cfengine) do require_relative "provisioner" Provisioner end end end end ================================================ FILE: plugins/provisioners/cfengine/provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module CFEngine class Provisioner < Vagrant.plugin("2", :provisioner) def provision if @machine.config.vm.communicator == :winrm raise Vagrant::Errors::ProvisionerWinRMUnsupported, name: "cfengine" end @logger = Log4r::Logger.new("vagrant::plugins::cfengine") @logger.info("Checking for CFEngine installation...") handle_cfengine_installation if @config.files_path @machine.ui.info(I18n.t("vagrant.cfengine_installing_files_path")) install_files(Pathname.new(@config.files_path).expand_path(@machine.env.root_path)) end handle_cfengine_bootstrap if @config.mode == :bootstrap if @config.mode == :single_run # Just let people know @machine.ui.info(I18n.t("vagrant.cfengine_single_run")) end if @config.run_file @machine.ui.info(I18n.t("vagrant.cfengine_single_run_execute")) path = Pathname.new(@config.run_file).expand_path(@machine.env.root_path) machine.communicate.upload(path.to_s, @config.upload_path) cfagent("-KI -f #{@config.upload_path}#{cfagent_classes_args}#{cfagent_extra_args}") end end protected # This runs cf-agent with the given arguments. def cfagent(args, options=nil) options ||= {} command = "/var/cfengine/bin/cf-agent #{args}" @machine.communicate.sudo(command, error_check: options[:error_check]) do |type, data| if [:stderr, :stdout].include?(type) # Output the data with the proper color based on the stream. color = type == :stdout ? :green : :red @machine.ui.info( data, color: color, new_line: false, prefix: false) end end end # Returns the arguments for the classes configuration if they are # set. # # @return [String] def cfagent_classes_args return "" if !@config.classes args = @config.classes.map { |c| "-D#{c}" }.join(" ") return " #{args}" end # Extra arguments for calles to cf-agent. # # @return [String] def cfagent_extra_args return "" if !@config.extra_agent_args return " #{@config.extra_agent_args}" end # This handles checking if the CFEngine installation needs to # be bootstrapped, and bootstraps if it does. def handle_cfengine_bootstrap @logger.info("Bootstrapping CFEngine...") if !@machine.guest.capability(:cfengine_needs_bootstrap, @config) @machine.ui.info(I18n.t("vagrant.cfengine_no_bootstrap")) return end # Needs bootstrap, let's determine the parameters policy_server_address = @config.policy_server_address if !policy_server_address policy_server_address = @machine.guest.capability(:read_ip_address) raise Vagrant::Errors::CFEngineCantAutodetectIP if !policy_server_address @machine.ui.info(I18n.t("vagrant.cfengine_detected_ip", address: policy_server_address)) end @machine.ui.info(I18n.t("vagrant.cfengine_bootstrapping", policy_server: policy_server_address)) result = cfagent("--bootstrap #{policy_server_address}", error_check: false) raise Vagrant::Errors::CFEngineBootstrapFailed if result != 0 # Policy hubs need to do additional things before they're ready # to accept agents. Force that run now... if @config.am_policy_hub @machine.ui.info(I18n.t("vagrant.cfengine_bootstrapping_policy_hub")) cfagent("-KI -f /var/cfengine/masterfiles/failsafe.cf#{cfagent_classes_args}") cfagent("-KI #{cfagent_classes_args}#{cfagent_extra_args}") end end # This handles verifying the CFEngine installation, installing it # if it was requested, and so on. This method will raise exceptions # if things are wrong. def handle_cfengine_installation if !@machine.guest.capability?(:cfengine_installed) @machine.ui.warn(I18n.t("vagrant.cfengine_cant_detect")) return end installed = @machine.guest.capability(:cfengine_installed) if !installed || @config.install == :force raise Vagrant::Errors::CFEngineNotInstalled if !@config.install @machine.ui.info(I18n.t("vagrant.cfengine_installing")) @machine.guest.capability(:cfengine_install, @config) if !@machine.guest.capability(:cfengine_installed) raise Vagrant::Errors::CFEngineInstallFailed end end end # This installs a set of files into the CFEngine folder within # the machine. # # @param [Pathname] local_path def install_files(local_path) @logger.debug("Copying local files to CFEngine: #{local_path}") @machine.communicate.sudo("rm -rf /tmp/cfengine-files") @machine.communicate.upload(local_path.to_s, "/tmp/cfengine-files") @machine.communicate.sudo("cp -R /tmp/cfengine-files/* /var/cfengine") end end end end ================================================ FILE: plugins/provisioners/chef/cap/debian/chef_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../omnibus" module VagrantPlugins module Chef module Cap module Debian module ChefInstall def self.chef_install(machine, project, version, channel, omnibus_url, options = {}) machine.communicate.sudo("apt-get update -y -qq") machine.communicate.sudo("apt-get install -y -qq curl") command = Omnibus.sh_command(project, version, channel, omnibus_url, options) machine.communicate.sudo(command) end end end end end end ================================================ FILE: plugins/provisioners/chef/cap/freebsd/chef_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../omnibus" module VagrantPlugins module Chef module Cap module FreeBSD module ChefInstall def self.chef_install(machine, project, version, channel, omnibus_url, options = {}) machine.communicate.sudo("pkg install -qy curl bash") command = Omnibus.sh_command(project, version, channel, omnibus_url, options) machine.communicate.sudo(command) end end end end end end ================================================ FILE: plugins/provisioners/chef/cap/freebsd/chef_installed.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Chef module Cap module FreeBSD module ChefInstalled # Check if Chef is installed at the given version. # @return [true, false] def self.chef_installed(machine, product, version) product_name = product == 'chef-workstation' ? 'chef-workstation' : 'chef' verify_bin = product_name == 'chef-workstation' ? 'chef' : 'chef-client' verify_path = "/opt/#{product_name}/bin/#{verify_bin}" command = "test -x #{verify_path}" if version != :latest command << "&& #{verify_path} --version | grep '#{version}'" end machine.communicate.test(command, sudo: true) end end end end end end ================================================ FILE: plugins/provisioners/chef/cap/linux/chef_installed.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Chef module Cap module Linux module ChefInstalled # Check if Chef is installed at the given version. # @return [true, false] def self.chef_installed(machine, product, version) product_name = product == 'chef-workstation' ? 'chef-workstation' : 'chef' verify_bin = product_name == 'chef-workstation' ? 'chef' : 'chef-client' verify_path = "/opt/#{product_name}/bin/#{verify_bin}" command = "test -x #{verify_path}" if version != :latest command << "&& #{verify_path} --version | grep '#{version}'" end machine.communicate.test(command, sudo: true) end end end end end end ================================================ FILE: plugins/provisioners/chef/cap/omnios/chef_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../omnibus" module VagrantPlugins module Chef module Cap module OmniOS module ChefInstall def self.chef_install(machine, project, version, channel, omnibus_url, options = {}) su = machine.config.solaris.suexec_cmd machine.communicate.execute("#{su} pkg list --no-refresh web/curl > /dev/null 2>&1 || pkg install -q --accept web/curl") command = Omnibus.sh_command(project, version, channel, omnibus_url, options) machine.communicate.execute("#{su} #{command}") end end end end end end ================================================ FILE: plugins/provisioners/chef/cap/omnios/chef_installed.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Chef module Cap module OmniOS module ChefInstalled # TODO: this is the same code as cap/linux/chef_installed, consider merging # Check if Chef is installed at the given version. # @return [true, false] def self.chef_installed(machine, product, version) product_name = product == 'chef-workstation' ? 'chef-workstation' : 'chef' verify_bin = product_name == 'chef-workstation' ? 'chef' : 'chef-client' verify_path = "/opt/#{product_name}/bin/#{verify_bin}" command = "test -x #{verify_path}" if version != :latest command << "&& #{verify_path} --version | grep '#{version}'" end machine.communicate.test(command, sudo: true) end end end end end end ================================================ FILE: plugins/provisioners/chef/cap/redhat/chef_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../omnibus" module VagrantPlugins module Chef module Cap module Redhat module ChefInstall def self.chef_install(machine, project, version, channel, omnibus_url, options = {}) machine.communicate.sudo <<-EOH.gsub(/^ {14}/, '') if command -v dnf; then dnf -y install curl else yum -y install curl fi EOH command = Omnibus.sh_command(project, version, channel, omnibus_url, options) machine.communicate.sudo(command) end end end end end end ================================================ FILE: plugins/provisioners/chef/cap/suse/chef_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../omnibus" module VagrantPlugins module Chef module Cap module Suse module ChefInstall def self.chef_install(machine, project, version, channel, omnibus_url, options = {}) unless curl?(machine) machine.communicate.sudo("zypper -n -q update") machine.communicate.sudo("zypper -n -q install curl") end command = Omnibus.sh_command(project, version, channel, omnibus_url, options) machine.communicate.sudo(command) end protected def self.curl?(machine) machine.communicate.test("/usr/bin/which -s curl") end end end end end end ================================================ FILE: plugins/provisioners/chef/cap/windows/chef_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../omnibus" module VagrantPlugins module Chef module Cap module Windows module ChefInstall def self.chef_install(machine, project, version, channel, omnibus_url, options = {}) command = Omnibus.ps_command(project, version, channel, omnibus_url, options) machine.communicate.sudo(command) end end end end end end ================================================ FILE: plugins/provisioners/chef/cap/windows/chef_installed.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Chef module Cap module Windows module ChefInstalled # Check if Chef is installed at the given version. # @return [true, false] def self.chef_installed(machine, product, version) verify_bin = product == 'chef-workstation' ? 'chef' : 'chef-client' if version != :latest command = 'if ((&' + verify_bin + ' --version) -Match "' + version.to_s + '"){ exit 0 } else { exit 1 }' else command = 'if ((&' + verify_bin + ' --version) -Match "Chef*"){ exit 0 } else { exit 1 }' end machine.communicate.test(command, sudo: true) end end end end end end ================================================ FILE: plugins/provisioners/chef/command_builder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Chef class CommandBuilder def self.command(type, config, options = {}) new(type, config, options).command end attr_reader :type attr_reader :config attr_reader :options def initialize(type, config, options = {}) @type = type @config = config @options = options.dup if type != :client && type != :solo raise "Unknown type `#{type.inspect}'!" end end def command if config.binary_env "#{config.binary_env} #{binary_path} #{args}" else "#{binary_path} #{args}" end end protected def binary_path binary_path = "chef-#{type}" if config.binary_path binary_path = File.join(config.binary_path, binary_path) if windows? binary_path = windows_friendly_path(binary_path) end end binary_path end def args args = "" args << " --config #{provisioning_path("#{type}.rb")}" args << " --json-attributes #{provisioning_path("dna.json")}" args << " --local-mode" if options[:local_mode] args << " --legacy-mode" if options[:legacy_mode] args << " --log_level #{config.log_level}" if config.log_level args << " --no-color" if !options[:colored] if config.install && (config.version == :latest || config.version.to_s >= "11.0") args << " --force-formatter" end args << " #{config.arguments.strip}" if config.arguments args.strip end def provisioning_path(file) if windows? path = config.provisioning_path || "C:/vagrant-chef" return windows_friendly_path(File.join(path, file)) else path = config.provisioning_path || "/tmp/vagrant-chef" return File.join(path, file) end end def windows_friendly_path(path) path = path.gsub("/", "\\") path = "c:#{path}" if path.start_with?("\\") return path end def windows? !!options[:windows] end end end end ================================================ FILE: plugins/provisioners/chef/config/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/presence" module VagrantPlugins module Chef module Config class Base < Vagrant.plugin("2", :config) include Vagrant::Util::Presence # The path to Chef's bin/ directory. # @return [String] attr_accessor :binary_path # Arbitrary environment variables to set before running the Chef # provisioner command. # @return [String] attr_accessor :binary_env # The name of the Chef project to install. This is "chef" for the Chef # Client or "chefdk" for the Chef Development Kit. Other product names # may be available as well. # @return [String] attr_accessor :product # Install Chef on the system if it does not exist. Default is true. # This is a trinary attribute (it can have three values): # # - true (bool) install Chef # - false (bool) do not install Chef # - "force" (string) install Chef, even if it is already installed at # the proper version # # @return [true, false, String] attr_accessor :install # The Chef log level. See the Chef docs for acceptable values. # @return [String, Symbol] attr_accessor :log_level # The channel from which to download Chef. Currently known values are # "current" and "stable", but more may be added in the future. The # default is "stable". # @return [String] attr_accessor :channel # The version of Chef to install. If Chef is already installed on the # system, the installed version is compared with the requested version. # If they match, no action is taken. If they do not match, version of # the value specified in this attribute will be installed over top of # the existing version (a warning will be displayed). # # You can also specify "latest" (default), which will install the latest # version of Chef on the system. In this case, Chef will use whatever # version is on the system. To force the newest version of Chef to be # installed on every provision, set the {#install} option to "force". # # @return [String] attr_accessor :version # Location of Omnibus installation scripts. # This URL specifies the location of install.sh/install.ps1 for # Linux/Unix and Windows respectively. # # It defaults to https://omnitruck.chef.io/. The full URL is then: # - Linux/Unix: https://omnitruck.chef.io/install.sh # - Windows: https://omnitruck.chef.io/install.ps1 # # If you want to have https://example.com/install.sh as Omnibus script # for your Linux/Unix installations, you should set this option to # https://example.com # # @return [String] attr_accessor :omnibus_url # The path where the Chef installer will be downloaded to. Only valid if # install is true or "force". It defaults to nil, which means that the # omnibus installer will choose the destination and you have no control # over it. # # @return [String] attr_accessor :installer_download_path def initialize super @binary_path = UNSET_VALUE @binary_env = UNSET_VALUE @product = UNSET_VALUE @install = UNSET_VALUE @log_level = UNSET_VALUE @channel = UNSET_VALUE @version = UNSET_VALUE @omnibus_url = UNSET_VALUE @installer_download_path = UNSET_VALUE end def finalize! @binary_path = nil if @binary_path == UNSET_VALUE @binary_env = nil if @binary_env == UNSET_VALUE @product = "chef" if @product == UNSET_VALUE @install = true if @install == UNSET_VALUE @log_level = :info if @log_level == UNSET_VALUE @channel = "stable" if @channel == UNSET_VALUE @version = :latest if @version == UNSET_VALUE @omnibus_url = 'https://omnitruck.chef.io' if @omnibus_url == UNSET_VALUE @installer_download_path = nil if @installer_download_path == UNSET_VALUE # Make sure the install is a symbol if it's not a boolean if @install.respond_to?(:to_sym) @install = @install.to_sym end # Make sure the version is a symbol if it's not a boolean if @version.respond_to?(:to_sym) @version = @version.to_sym end # Make sure the log level is a symbol @log_level = @log_level.to_sym end # Like validate, but returns a list of errors to append. # # @return [Array] def validate_base(machine) errors = _detected_errors if !present?(log_level) errors << I18n.t("vagrant.provisioners.chef.log_level_empty") end errors end end end end end ================================================ FILE: plugins/provisioners/chef/config/base_runner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/counter" require_relative "base" module VagrantPlugins module Chef module Config # This is the config base for Chef provisioners that need a full Chef # Runner object, like chef-solo or chef-client. For provisioners like # chef-apply, these options are not valid class BaseRunner < Base attr_accessor :arguments attr_accessor :attempts attr_accessor :custom_config_path attr_accessor :encrypted_data_bag_secret_key_path attr_accessor :environment attr_accessor :formatter attr_accessor :http_proxy attr_accessor :http_proxy_user attr_accessor :http_proxy_pass attr_accessor :https_proxy attr_accessor :https_proxy_user attr_accessor :https_proxy_pass attr_accessor :json attr_accessor :no_proxy attr_accessor :node_name attr_accessor :provisioning_path attr_accessor :run_list attr_accessor :file_cache_path attr_accessor :file_backup_path attr_accessor :verbose_logging attr_accessor :enable_reporting def initialize super @arguments = UNSET_VALUE @attempts = UNSET_VALUE @custom_config_path = UNSET_VALUE # /etc/chef/client.rb config options @encrypted_data_bag_secret_key_path = UNSET_VALUE @environment = UNSET_VALUE @formatter = UNSET_VALUE @http_proxy = UNSET_VALUE @http_proxy_user = UNSET_VALUE @http_proxy_pass = UNSET_VALUE @https_proxy = UNSET_VALUE @https_proxy_user = UNSET_VALUE @https_proxy_pass = UNSET_VALUE @no_proxy = UNSET_VALUE @node_name = UNSET_VALUE @provisioning_path = UNSET_VALUE @file_cache_path = UNSET_VALUE @file_backup_path = UNSET_VALUE @verbose_logging = UNSET_VALUE @enable_reporting = UNSET_VALUE # Runner options @json = {} @run_list = [] end def finalize! super @arguments = nil if @arguments == UNSET_VALUE @attempts = 1 if @attempts == UNSET_VALUE @custom_config_path = nil if @custom_config_path == UNSET_VALUE @environment = nil if @environment == UNSET_VALUE @formatter = nil if @formatter == UNSET_VALUE @http_proxy = nil if @http_proxy == UNSET_VALUE @http_proxy_user = nil if @http_proxy_user == UNSET_VALUE @http_proxy_pass = nil if @http_proxy_pass == UNSET_VALUE @https_proxy = nil if @https_proxy == UNSET_VALUE @https_proxy_user = nil if @https_proxy_user == UNSET_VALUE @https_proxy_pass = nil if @https_proxy_pass == UNSET_VALUE @no_proxy = nil if @no_proxy == UNSET_VALUE @node_name = nil if @node_name == UNSET_VALUE @provisioning_path = nil if @provisioning_path == UNSET_VALUE @file_backup_path = nil if @file_backup_path == UNSET_VALUE @file_cache_path = nil if @file_cache_path == UNSET_VALUE @verbose_logging = false if @verbose_logging == UNSET_VALUE @enable_reporting = true if @enable_reporting == UNSET_VALUE if @encrypted_data_bag_secret_key_path == UNSET_VALUE @encrypted_data_bag_secret_key_path = nil end end def merge(other) super.tap do |result| result.instance_variable_set(:@json, @json.merge(other.json)) result.instance_variable_set(:@run_list, (@run_list + other.run_list)) end 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_base(machine) errors = super if @custom_config_path expanded = File.expand_path(@custom_config_path, machine.env.root_path) if !File.file?(expanded) errors << I18n.t("vagrant.config.chef.custom_config_path_missing") end end errors end # Adds a recipe to the run list def add_recipe(name) name = "recipe[#{name}]" unless name =~ /^recipe\[(.+?)\]$/ run_list << name end # Adds a role to the run list def add_role(name) name = "role[#{name}]" unless name =~ /^role\[(.+?)\]$/ run_list << name end end end end end ================================================ FILE: plugins/provisioners/chef/config/chef_apply.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/presence" require_relative "base" module VagrantPlugins module Chef module Config class ChefApply < Base include Vagrant::Util::Presence # The raw recipe text (as a string) to execute via chef-apply. # @return [String] attr_accessor :recipe # The path (on the guest) where the uploaded apply recipe should be # written (/tmp/vagrant-chef-apply-#.rb). # @return [String] attr_accessor :upload_path def initialize super @recipe = UNSET_VALUE @upload_path = UNSET_VALUE end def finalize! super @recipe = nil if @recipe == UNSET_VALUE @upload_path = "/tmp/vagrant-chef-apply" if @upload_path == UNSET_VALUE end def validate(machine) errors = validate_base(machine) if !present?(recipe) errors << I18n.t("vagrant.provisioners.chef.recipe_empty") end if !present?(upload_path) errors << I18n.t("vagrant.provisioners.chef.upload_path_empty") end { "chef apply provisioner" => errors } end end end end end ================================================ FILE: plugins/provisioners/chef/config/chef_client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/presence" require "vagrant/util/which" require_relative "base_runner" module VagrantPlugins module Chef module Config class ChefClient < BaseRunner include Vagrant::Util::Presence # The URL endpoint to the Chef Server. # @return [String] attr_accessor :chef_server_url # The path on disk to the Chef client key, # @return [String] attr_accessor :client_key_path # Delete the client key when the VM is destroyed. Default is false. # @return [true, false] attr_accessor :delete_client # Delete the node when the VM is destroyed. Default is false. # @return [true, false] attr_accessor :delete_node # The path to the validation key on disk. # @return [String] attr_accessor :validation_key_path # The name of the validation client. # @return [String] attr_accessor :validation_client_name def initialize super @chef_server_url = UNSET_VALUE @client_key_path = UNSET_VALUE @delete_client = UNSET_VALUE @delete_node = UNSET_VALUE @validation_key_path = UNSET_VALUE @validation_client_name = UNSET_VALUE end def finalize! super @chef_server_url = nil if @chef_server_url == UNSET_VALUE @client_key_path = nil if @client_key_path == UNSET_VALUE @delete_client = false if @delete_client == UNSET_VALUE @delete_node = false if @delete_node == UNSET_VALUE @validation_client_name = "chef-validator" if @validation_client_name == UNSET_VALUE @validation_key_path = nil if @validation_key_path == UNSET_VALUE end def validate(machine) errors = validate_base(machine) if !present?(chef_server_url) errors << I18n.t("vagrant.config.chef.server_url_empty") end if !present?(validation_key_path) errors << I18n.t("vagrant.config.chef.validation_key_path") end { "chef client provisioner" => errors } end end end end end ================================================ FILE: plugins/provisioners/chef/config/chef_solo.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/presence" require_relative "base_runner" module VagrantPlugins module Chef module Config class ChefSolo < BaseRunner include Vagrant::Util::Presence # The path on disk where Chef cookbooks are stored. # Default is "cookbooks". # @return [String] attr_accessor :cookbooks_path # The path where data bags are stored on disk. # @return [String] attr_accessor :data_bags_path # The path where environments are stored on disk. # @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. # # @deprecated # # @return [String] attr_accessor :recipe_url # Enable chef-solo legacy mode rather than local mode. # @return [true, false] attr_accessor :legacy_mode # The path where roles are stored on disk. # @return [String] attr_accessor :roles_path # The type of synced folders to use. # @return [String] attr_accessor :synced_folder_type def initialize super @cookbooks_path = UNSET_VALUE @data_bags_path = UNSET_VALUE @environments_path = UNSET_VALUE @nodes_path = UNSET_VALUE @recipe_url = UNSET_VALUE @legacy_mode = UNSET_VALUE @roles_path = UNSET_VALUE @synced_folder_type = UNSET_VALUE end #------------------------------------------------------------ # Internal methods #------------------------------------------------------------ def finalize! super @recipe_url = nil if @recipe_url == UNSET_VALUE @synced_folder_type = nil if @synced_folder_type == UNSET_VALUE @legacy_mode = false if @legacy_mode == UNSET_VALUE if @cookbooks_path == UNSET_VALUE @cookbooks_path = [] @cookbooks_path << [:host, "cookbooks"] if !@recipe_url @cookbooks_path << [:vm, "cookbooks"] 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 # 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 def validate(machine) errors = validate_base(machine) if !present?(Array(cookbooks_path)) errors << I18n.t("vagrant.config.chef.cookbooks_path_empty") end if environment && !present?(environments_path) errors << I18n.t("vagrant.config.chef.environment_path_required") end environments_path.each do |type, raw_path| next if type != :host path = Pathname.new(raw_path).expand_path(machine.env.root_path) if !path.directory? errors << I18n.t("vagrant.config.chef.environment_path_missing", path: raw_path.to_s ) end end { "chef solo provisioner" => errors } end protected # This takes any of the configurations that take a path or # array of paths and turns it into the proper format. # # @return [Array] def prepare_folders_config(config) # Make sure the path is an array config = [config] if !config.is_a?(Array) || config.first.is_a?(Symbol) return [] if config.flatten.compact.empty? # Make sure all the paths are in the proper format config.map do |path| path = [:host, path] if !path.is_a?(Array) path end end end end end end ================================================ FILE: plugins/provisioners/chef/config/chef_zero.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/presence" require_relative "chef_solo" module VagrantPlugins module Chef module Config class ChefZero < BaseRunner include Vagrant::Util::Presence # The path on disk where Chef cookbooks are stored. # Default is "cookbooks". # @return [String] attr_accessor :cookbooks_path # The path where data bags are stored on disk. # @return [String] attr_accessor :data_bags_path # The path where environments are stored on disk. # @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 # The type of synced folders to use. # @return [String] attr_accessor :synced_folder_type def initialize super @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 def finalize! super @synced_folder_type = nil if @synced_folder_type == UNSET_VALUE if @cookbooks_path == UNSET_VALUE @cookbooks_path = [] @cookbooks_path << [:host, "cookbooks"] if !@recipe_url @cookbooks_path << [:vm, "cookbooks"] 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 # 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 def validate(machine) errors = validate_base(machine) if !present?(Array(cookbooks_path)) errors << I18n.t("vagrant.config.chef.cookbooks_path_empty") end if !present?(Array(nodes_path)) errors << I18n.t("vagrant.config.chef.nodes_path_empty") else missing_paths = Array.new nodes_path.each { |dir| missing_paths << dir[1] if !File.exist? dir[1] } # If it exists at least one path on disk it's ok for Chef provisioning if missing_paths.size == nodes_path.size errors << I18n.t("vagrant.config.chef.nodes_path_missing", path: missing_paths.to_s) end end if environment && environments_path.empty? errors << I18n.t("vagrant.config.chef.environment_path_required") end environments_path.each do |type, raw_path| next if type != :host path = Pathname.new(raw_path).expand_path(machine.env.root_path) if !path.directory? errors << I18n.t("vagrant.config.chef.environment_path_missing", path: raw_path.to_s ) end end { "chef zero provisioner" => errors } end protected # This takes any of the configurations that take a path or # array of paths and turns it into the proper format. # # @return [Array] def prepare_folders_config(config) # Make sure the path is an array config = [config] if !config.is_a?(Array) || config.first.is_a?(Symbol) return [] if config.flatten.compact.empty? # Make sure all the paths are in the proper format config.map do |path| path = [:host, File.expand_path(path)] if !path.is_a?(Array) path end end end end end end ================================================ FILE: plugins/provisioners/chef/installer.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Chef class Installer def initialize(machine, options = {}) @machine = machine @product = options.fetch(:product) @channel = options.fetch(:channel) @version = options.fetch(:version) @force = options.fetch(:force) @omnibus_url = options.fetch(:omnibus_url) @options = options.dup end # This handles verifying the Chef installation, installing it if it was # requested, and so on. This method will raise exceptions if things are # wrong. def ensure_installed # If the guest cannot check if Chef is installed, just exit printing a # warning... if !@machine.guest.capability?(:chef_installed) @machine.ui.warn(I18n.t("vagrant.chef_cant_detect")) return end if !should_install_chef? @machine.ui.info(I18n.t("vagrant.chef_already_installed", version: @version.to_s)) return end @machine.ui.detail(I18n.t("vagrant.chef_installing", version: @version.to_s)) @machine.guest.capability(:chef_install, @product, @version, @channel, @omnibus_url, @options) if !@machine.guest.capability(:chef_installed, @product, @version) raise Provisioner::Base::ChefError, :install_failed end end # Determine if Chef should be installed. Chef is installed if the "force" # option is given or if the guest does not have Chef installed at the # proper version. def should_install_chef? @force || !@machine.guest.capability(:chef_installed, @product, @version) end end end end ================================================ FILE: plugins/provisioners/chef/omnibus.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Chef # Read more about the Omnibus installer here: # # https://docs.chef.io/install_omnibus.html # module Omnibus def sh_command(project, version, channel, omnibus_url, options = {}) command = "curl -sL #{omnibus_url}/install.sh | bash" command << " -s -- -P \"#{project}\" -c \"#{channel}\"" if version != :latest command << " -v \"#{version}\"" end if options[:download_path] command << " -d \"#{options[:download_path]}\"" end command end module_function :sh_command def ps_command(project, version, channel, omnibus_url, options = {}) command = ". { iwr -useb #{omnibus_url}/install.ps1 } | iex; install" command << " -project '#{project}' -channel '#{channel}'" if version != :latest command << " -version '#{version}'" end command end module_function :ps_command end end end ================================================ FILE: plugins/provisioners/chef/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require_relative "command_builder" module VagrantPlugins module Chef class Plugin < Vagrant.plugin("2") name "chef" description <<-DESC Provides support for provisioning your virtual machines with Chef via `chef-solo`, `chef-client`, `chef-zero` or `chef-apply`. DESC config(:chef_apply, :provisioner) do require_relative "config/chef_apply" Config::ChefApply end config(:chef_client, :provisioner) do require_relative "config/chef_client" Config::ChefClient end config(:chef_solo, :provisioner) do require_relative "config/chef_solo" Config::ChefSolo end config(:chef_zero, :provisioner) do require_relative "config/chef_zero" Config::ChefZero end provisioner(:chef_apply) do require_relative "provisioner/chef_apply" Provisioner::ChefApply end provisioner(:chef_client) do require_relative "provisioner/chef_client" Provisioner::ChefClient end provisioner(:chef_solo) do require_relative "provisioner/chef_solo" Provisioner::ChefSolo end provisioner(:chef_zero) do require_relative "provisioner/chef_zero" Provisioner::ChefZero end guest_capability(:debian, :chef_install) do require_relative "cap/debian/chef_install" Cap::Debian::ChefInstall end guest_capability(:freebsd, :chef_install) do require_relative "cap/freebsd/chef_install" Cap::FreeBSD::ChefInstall end guest_capability(:freebsd, :chef_installed) do require_relative "cap/freebsd/chef_installed" Cap::FreeBSD::ChefInstalled end guest_capability(:linux, :chef_installed) do require_relative "cap/linux/chef_installed" Cap::Linux::ChefInstalled end guest_capability(:omnios, :chef_install) do require_relative "cap/omnios/chef_install" Cap::OmniOS::ChefInstall end guest_capability(:omnios, :chef_installed) do require_relative "cap/omnios/chef_installed" Cap::OmniOS::ChefInstalled end guest_capability(:redhat, :chef_install) do require_relative "cap/redhat/chef_install" Cap::Redhat::ChefInstall end guest_capability(:suse, :chef_install) do require_relative "cap/suse/chef_install" Cap::Suse::ChefInstall end guest_capability(:windows, :chef_install) do require_relative "cap/windows/chef_install" Cap::Windows::ChefInstall end guest_capability(:windows, :chef_installed) do require_relative "cap/windows/chef_installed" Cap::Windows::ChefInstalled end end end end ================================================ FILE: plugins/provisioners/chef/provisioner/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../lib/vagrant/util/presence" require_relative "../../../../lib/vagrant/util/template_renderer" require_relative "../installer" module VagrantPlugins module Chef module Provisioner # This class is a base class where the common functionality shared between # chef-solo and chef-client provisioning are stored. This is **not an actual # provisioner**. Instead, {ChefSolo} or {ChefServer} should be used. class Base < Vagrant.plugin("2", :provisioner) include Vagrant::Util include Vagrant::Util::Presence class ChefError < Vagrant::Errors::VagrantError error_namespace("vagrant.provisioners.chef") end def initialize(machine, config) super @logger = Log4r::Logger.new("vagrant::provisioners::chef") if @config.respond_to?(:node_name) && !present?(@config.node_name) # First attempt to get the node name from the hostname, and if that # is not present, generate/retrieve a random hostname. hostname = @machine.config.vm.hostname if present?(hostname) @machine.ui.info I18n.t("vagrant.provisioners.chef.using_hostname_node_name", hostname: hostname, ) @config.node_name = hostname else cache = @machine.data_dir.join("chef_node_name") if !cache.exist? @machine.ui.info I18n.t("vagrant.provisioners.chef.generating_node_name") cache.open("w+") do |f| f.write("vagrant-#{SecureRandom.hex(4)}") end end if cache.file? @logger.info("Loading cached node_name...") @config.node_name = cache.read.strip end end end end def install_chef return if !config.install @logger.info("Checking for Chef installation...") installer = Installer.new(@machine, product: config.product, channel: config.channel, version: config.version, omnibus_url: config.omnibus_url, force: config.install == :force, download_path: config.installer_download_path ) installer.ensure_installed end def verify_binary(binary) # Checks for the existence of chef binary and error if it # doesn't exist. if windows? command = "if ((&'#{binary}' -v) -Match 'Chef*'){ exit 0 } else { exit 1 }" else command = "sh -c 'command -v #{binary}'" end @machine.communicate.sudo( command, error_class: ChefError, error_key: :chef_not_detected, binary: binary, ) end # Returns the path to the Chef binary, taking into account the # `binary_path` configuration option. def chef_binary_path(binary) return binary if !@config.binary_path return File.join(@config.binary_path, binary) end def chown_provisioning_folder paths = [ guest_provisioning_path, guest_file_backup_path, guest_file_cache_path, ] @machine.communicate.tap do |comm| paths.each do |path| if windows? comm.sudo("mkdir ""#{path}"" -f") else comm.sudo("mkdir -p #{path}") comm.sudo("chown -h #{@machine.ssh_info[:username]} #{path}") end end end end def setup_config(template, filename, template_vars) # If we have custom configuration, upload it remote_custom_config_path = nil if @config.custom_config_path expanded = File.expand_path( @config.custom_config_path, @machine.env.root_path) remote_custom_config_path = File.join( guest_provisioning_path, "custom-config.rb") @machine.communicate.upload(expanded, remote_custom_config_path) end config_file = TemplateRenderer.render(template, { custom_configuration: remote_custom_config_path, encrypted_data_bag_secret: guest_encrypted_data_bag_secret_key_path, environment: @config.environment, file_cache_path: guest_file_cache_path, file_backup_path: guest_file_backup_path, log_level: @config.log_level.to_sym, node_name: @config.node_name, verbose_logging: @config.verbose_logging, enable_reporting: @config.enable_reporting, http_proxy: @config.http_proxy, http_proxy_user: @config.http_proxy_user, http_proxy_pass: @config.http_proxy_pass, https_proxy: @config.https_proxy, https_proxy_user: @config.https_proxy_user, https_proxy_pass: @config.https_proxy_pass, no_proxy: @config.no_proxy, formatter: @config.formatter }.merge(template_vars)) # Create a temporary file to store the data so we can upload it. remote_file = File.join(guest_provisioning_path, filename) @machine.communicate.sudo(remove_command(remote_file), error_check: false) Tempfile.open("vagrant-chef-provisioner-config") do |f| f.binmode f.write(config_file) f.fsync f.close @machine.communicate.upload(f.path, remote_file) end end def setup_json @machine.ui.info I18n.t("vagrant.provisioners.chef.json") # Get the JSON that we're going to expose to Chef json = @config.json json[:run_list] = @config.run_list if @config.run_list && !@config.run_list.empty? json = JSON.pretty_generate(json) # Create a temporary file to store the data so we can upload it. remote_file = File.join(guest_provisioning_path, "dna.json") @machine.communicate.sudo(remove_command(remote_file), error_check: false) Tempfile.open("vagrant-chef-provisioner-config") do |f| f.binmode f.write(json) f.fsync f.close @machine.communicate.upload(f.path, remote_file) end end def upload_encrypted_data_bag_secret remote_file = guest_encrypted_data_bag_secret_key_path return if !remote_file @machine.ui.info I18n.t( "vagrant.provisioners.chef.upload_encrypted_data_bag_secret_key") @machine.communicate.sudo(remove_command(remote_file), error_check: false) @machine.communicate.upload(encrypted_data_bag_secret_key_path, remote_file) end def delete_encrypted_data_bag_secret remote_file = guest_encrypted_data_bag_secret_key_path return if remote_file.nil? @machine.communicate.sudo(remove_command(remote_file), error_check: false) end def encrypted_data_bag_secret_key_path File.expand_path(@config.encrypted_data_bag_secret_key_path, @machine.env.root_path) end def guest_encrypted_data_bag_secret_key_path if @config.encrypted_data_bag_secret_key_path File.join(guest_provisioning_path, "encrypted_data_bag_secret_key") end end def guest_provisioning_path if !@config.provisioning_path.nil? return @config.provisioning_path end if windows? "C:/vagrant-chef" else "/tmp/vagrant-chef" end end def guest_file_backup_path if !@config.file_backup_path.nil? return @config.file_backup_path end if windows? "C:/chef/backup" else "/var/chef/backup" end end def guest_file_cache_path if !@config.file_cache_path.nil? return @config.file_cache_path end if windows? "C:/chef/cache" else "/var/chef/cache" end end def remove_command(path) if windows? "if (test-path ""#{path}"") {rm ""#{path}"" -force -recurse}" else "rm -f #{path}" end end def windows? @machine.config.vm.communicator == :winrm end end end end end ================================================ FILE: plugins/provisioners/chef/provisioner/chef_apply.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/md5" require "tempfile" require_relative "base" module VagrantPlugins module Chef module Provisioner class ChefApply < Base def provision install_chef verify_binary(chef_binary_path("chef-apply")) command = "chef-apply" command << " \"#{target_recipe_path}\"" command << " --log_level #{config.log_level}" user = @machine.ssh_info[:username] # Reset upload path permissions for the current ssh user if windows? @machine.communicate.sudo("mkdir ""#{config.upload_path}"" -f") else @machine.communicate.sudo("mkdir -p #{config.upload_path}") @machine.communicate.sudo("chown -R #{user} #{config.upload_path}") end # Upload the recipe upload_recipe @machine.ui.info(I18n.t("vagrant.provisioners.chef.running_apply", script: config.path) ) # Execute it with sudo @machine.communicate.sudo(command) do |type, data| if [:stderr, :stdout].include?(type) # Output the data with the proper color based on the stream. color = (type == :stdout) ? :green : :red # Chomp the data to avoid the newlines that the Chef outputs @machine.env.ui.info(data.chomp, color: color, prefix: false) end end end # The destination (on the guest) where the recipe will live # @return [String] def target_recipe_path key = Digest::MD5.hexdigest(config.recipe) File.join(config.upload_path, "recipe-#{key}.rb") end # Write the raw recipe contents to a tempfile and upload that to the # machine. def upload_recipe # Write the raw recipe contents to a tempfile and upload Tempfile.open(["vagrant-chef-apply", ".rb"]) do |f| f.binmode f.write(config.recipe) f.fsync f.close # Upload the tempfile to the guest @machine.communicate.upload(f.path, target_recipe_path) end end end end end end ================================================ FILE: plugins/provisioners/chef/provisioner/chef_client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'pathname' require 'vagrant' require 'vagrant/util/presence' require 'vagrant/util/subprocess' require_relative "base" module VagrantPlugins module Chef module Provisioner # This class implements provisioning via chef-client, allowing provisioning # with a chef server. class ChefClient < Base include Vagrant::Util::Presence def configure(root_config) raise ChefError, :server_validation_key_required if @config.validation_key_path.nil? raise ChefError, :server_validation_key_doesnt_exist if !File.file?(validation_key_path) raise ChefError, :server_url_required if @config.chef_server_url.nil? end def provision install_chef verify_binary(chef_binary_path("chef-client")) chown_provisioning_folder create_client_key_folder upload_validation_key upload_encrypted_data_bag_secret setup_json setup_server_config run_chef_client delete_encrypted_data_bag_secret end def cleanup if @config.delete_node delete_from_chef_server("node") end if @config.delete_client delete_from_chef_server("client") end end def create_client_key_folder @machine.ui.info I18n.t("vagrant.provisioners.chef.client_key_folder") path = Pathname.new(guest_client_key_path) if windows? @machine.communicate.sudo("mkdir ""#{path.dirname}"" -f") else @machine.communicate.sudo("mkdir -p #{path.dirname}") end end def upload_validation_key @machine.ui.info I18n.t("vagrant.provisioners.chef.upload_validation_key") @machine.communicate.upload(validation_key_path, guest_validation_key_path) end def setup_server_config setup_config("provisioners/chef_client/client", "client.rb", { chef_server_url: @config.chef_server_url, validation_client_name: @config.validation_client_name, validation_key: guest_validation_key_path, client_key: guest_client_key_path, }) end def run_chef_client if @config.run_list && @config.run_list.empty? @machine.ui.warn(I18n.t("vagrant.chef_run_list_empty")) end command = CommandBuilder.command(:client, @config, windows: windows?, colored: @machine.env.ui.color?, ) still_active = 259 #provisioner has asked chef to reboot @config.attempts.times do |attempt| exit_status = 0 while exit_status == 0 || exit_status == still_active if @machine.guest.capability?(:wait_for_reboot) @machine.guest.capability(:wait_for_reboot) elsif attempt > 0 sleep 10 @machine.communicate.wait_for_ready(@machine.config.vm.boot_timeout) end if attempt == 0 @machine.ui.info I18n.t("vagrant.provisioners.chef.running_client") else @machine.ui.info I18n.t("vagrant.provisioners.chef.running_client_again") end opts = { error_check: false, elevated: true } exit_status = @machine.communicate.sudo(command, opts) do |type, data| # Output the data with the proper color based on the stream. color = type == :stdout ? :green : :red data = data.chomp next if data.empty? @machine.ui.info(data, color: color) end # There is no need to run Chef again if it converges return if exit_status == 0 end end # If we reached this point then Chef never converged! Error. raise ChefError, :no_convergence end def validation_key_path File.expand_path(@config.validation_key_path, @machine.env.root_path) end def guest_client_key_path if !@config.client_key_path.nil? return @config.client_key_path end if windows? "C:/chef/client.pem" else "/etc/chef/client.pem" end end def guest_client_rb_path File.join(guest_provisioning_path, "client.rb") end def guest_validation_key_path File.join(guest_provisioning_path, "validation.pem") end def delete_from_chef_server(deletable) node_name = @config.node_name if !present?(node_name) @machine.ui.warn(I18n.t("vagrant.provisioners.chef.missing_node_name", deletable: deletable, )) return end @machine.ui.info(I18n.t("vagrant.provisioners.chef.deleting_from_server", deletable: deletable, name: node_name)) command = "knife #{deletable} delete #{node_name}" command << " --config '#{guest_client_rb_path}'" command << " --yes" output = [] result = @machine.communicate.sudo(command, error_check: false) do |_, data| output << data end if result != 0 @machine.ui.error("There were errors removing the #{deletable} from the Chef Server:") @machine.ui.error("") @machine.ui.error(output.join("\n")) @machine.ui.error("") @machine.ui.error("Vagrant will continue destroying the virtual machine, but you may need") @machine.ui.error("to manually delete the #{deletable} from the Chef Server!") end end end end end end ================================================ FILE: plugins/provisioners/chef/provisioner/chef_solo.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/md5" require "securerandom" require "set" require "log4r" require "vagrant/util/counter" require "vagrant/action/builtin/mixin_synced_folders" require_relative "base" module VagrantPlugins module Chef module Provisioner # This class implements provisioning via chef-solo. class ChefSolo < Base extend Vagrant::Util::Counter include Vagrant::Util::Counter include Vagrant::Action::Builtin::MixinSyncedFolders attr_reader :environments_folders attr_reader :cookbook_folders attr_reader :node_folders attr_reader :role_folders attr_reader :data_bags_folders def initialize(machine, config) super @logger = Log4r::Logger.new("vagrant::provisioners::chef_solo") @shared_folders = [] end def configure(root_config) @cookbook_folders = expanded_folders(@config.cookbooks_path, "cookbooks") @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 install_chef # Verify that the proper shared folders exist. check = [] @shared_folders.each do |type, local_path, remote_path| # We only care about checking folders that have a local path, meaning # they were shared from the local machine, rather than assumed to # exist on the VM. check << remote_path if local_path end chown_provisioning_folder verify_shared_folders(check) verify_binary(chef_binary_path("chef-solo")) upload_encrypted_data_bag_secret setup_json setup_solo_config run_chef_solo delete_encrypted_data_bag_secret end # Converts paths to a list of properly expanded paths with types. def expanded_folders(paths, appended_folder=nil) # Convert the path to an array if it is a string or just a single # path element which contains the folder location (:host or :vm) paths = [paths] if paths.is_a?(String) || paths.first.is_a?(Symbol) results = [] paths.each do |type, path| # Create the local/remote path based on whether this is a host # or VM path. local_path = nil remote_path = nil if type == :host # Get the expanded path that the host path points to local_path = File.expand_path(path, @machine.env.root_path) if File.exist?(local_path) # Path exists on the host, setup the remote path. We use # the MD5 of the local path so that it is predictable. key = Digest::MD5.hexdigest(local_path) remote_path = "#{guest_provisioning_path}/#{key}" else appended_folder = "cookbooks" if appended_folder.nil? @machine.ui.warn(I18n.t("vagrant.provisioners.chef.#{appended_folder}_folder_not_found_warning", path: local_path.to_s)) next end else # Path already exists on the virtual machine. Expand it # relative to where we're provisioning. # Remove drive letter if running on a windows host. This is a bit # of a hack but is the most portable way I can think of at the moment # to achieve this. Otherwise, Vagrant attempts to share at some crazy # path like /home/vagrant/c:/foo/bar remote_path = File.expand_path(path.sub(/^[a-zA-Z]:\//, "/"), guest_provisioning_path.sub(/^[a-zA-Z]:\//, "/")) remote_path.sub!(/^[a-zA-Z]:\//, "/") end # If we have specified a folder name to append then append it if type == :host remote_path += "/#{appended_folder}" if appended_folder end # Append the result results << [type, local_path, remote_path] end results end # Shares the given folders with the given prefix. The folders should # be of the structure resulting from the `expanded_folders` function. def share_folders(root_config, prefix, folders, existing=nil) existing_set = Set.new (existing || []).each do |_, fs| fs.each do |id, data| existing_set.add(data[:guestpath]) end end folders.each do |type, local_path, remote_path| next if type != :host # If this folder already exists, then we don't share it, it means # it was already put down on disk. # # NOTE: This is currently commented out because it was causing # major bugs (GH-5199). We will investigate why this is in more # detail for 1.8.0, but we wanted to fix this in a patch release # and this was the hammer that did that. =begin if existing_set.include?(remote_path) @logger.debug("Not sharing #{local_path}, exists as #{remote_path}") next end =end key = Digest::MD5.hexdigest(remote_path) key = key[0..8] opts = {} opts[:id] = "v-#{prefix}-#{key}" opts[:type] = @config.synced_folder_type if @config.synced_folder_type root_config.vm.synced_folder(local_path, remote_path, opts) end @shared_folders += folders end def setup_solo_config setup_config("provisioners/chef_solo/solo", "solo.rb", solo_config) end def solo_config { 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), environments_path: guest_paths(@environments_folders).first } end def run_chef_solo if @config.run_list && @config.run_list.empty? @machine.ui.warn(I18n.t("vagrant.chef_run_list_empty")) end command = CommandBuilder.command(:solo, @config, windows: windows?, colored: @machine.env.ui.color?, legacy_mode: @config.legacy_mode, ) still_active = 259 #provisioner has asked chef to reboot @config.attempts.times do |attempt| exit_status = 0 while exit_status == 0 || exit_status == still_active if @machine.guest.capability?(:wait_for_reboot) @machine.guest.capability(:wait_for_reboot) elsif attempt > 0 sleep 10 @machine.communicate.wait_for_ready(@machine.config.vm.boot_timeout) end if attempt == 0 @machine.ui.info I18n.t("vagrant.provisioners.chef.running_solo") else @machine.ui.info I18n.t("vagrant.provisioners.chef.running_solo_again") end opts = { error_check: false, elevated: true } exit_status = @machine.communicate.sudo(command, opts) do |type, data| # Output the data with the proper color based on the stream. color = type == :stdout ? :green : :red data = data.chomp next if data.empty? @machine.ui.info(data, color: color) end # There is no need to run Chef again if it converges return if exit_status == 0 end end # If we reached this point then Chef never converged! Error. raise ChefError, :no_convergence end def verify_shared_folders(folders) folders.each do |folder| @logger.debug("Checking for shared folder: #{folder}") if !@machine.communicate.test("test -d #{folder}", sudo: true) raise ChefError, :missing_shared_folders end end end protected # Extracts only the remote paths from a list of folders def guest_paths(folders) folders.map { |parts| parts[2] } end end end end end ================================================ FILE: plugins/provisioners/chef/provisioner/chef_zero.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/md5" require "securerandom" require "set" require "log4r" require "vagrant/util/counter" require_relative "chef_solo" module VagrantPlugins module Chef module Provisioner # This class implements provisioning via chef-zero. class ChefZero < ChefSolo def initialize(machine, config) super @logger = Log4r::Logger.new("vagrant::provisioners::chef_zero") end def provision install_chef # Verify that the proper shared folders exist. check = [] @shared_folders.each do |type, local_path, remote_path| # We only care about checking folders that have a local path, meaning # they were shared from the local machine, rather than assumed to # exist on the VM. check << remote_path if local_path end chown_provisioning_folder verify_shared_folders(check) verify_binary(chef_binary_path("chef-client")) upload_encrypted_data_bag_secret setup_json setup_zero_config run_chef_zero delete_encrypted_data_bag_secret end def setup_zero_config setup_config("provisioners/chef_zero/zero", "client.rb", { 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), environments_path: guest_paths(@environments_folders).first, }) end def run_chef_zero if @config.run_list && @config.run_list.empty? @machine.ui.warn(I18n.t("vagrant.chef_run_list_empty")) end command = CommandBuilder.command(:client, @config, windows: windows?, colored: @machine.env.ui.color?, local_mode: true, ) still_active = 259 #provisioner has asked chef to reboot @config.attempts.times do |attempt| exit_status = 0 while exit_status == 0 || exit_status == still_active if @machine.guest.capability?(:wait_for_reboot) @machine.guest.capability(:wait_for_reboot) elsif attempt > 0 sleep 10 @machine.communicate.wait_for_ready(@machine.config.vm.boot_timeout) end if attempt == 0 @machine.ui.info I18n.t("vagrant.provisioners.chef.running_zero") else @machine.ui.info I18n.t("vagrant.provisioners.chef.running_zero_again") end opts = { error_check: false, elevated: true } exit_status = @machine.communicate.sudo(command, opts) do |type, data| # Output the data with the proper color based on the stream. color = type == :stdout ? :green : :red data = data.chomp next if data.empty? @machine.ui.info(data, color: color) end # There is no need to run Chef again if it converges return if exit_status == 0 end end # If we reached this point then Chef never converged! Error. raise ChefError, :no_convergence end def verify_shared_folders(folders) folders.each do |folder| @logger.debug("Checking for shared folder: #{folder}") if !@machine.communicate.test("test -d #{folder}", sudo: true) raise ChefError, :missing_shared_folders end end end protected # Extracts only the remote paths from a list of folders def guest_paths(folders) folders.map { |parts| parts[2] } end end end end end ================================================ FILE: plugins/provisioners/container/client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'digest/sha1' module VagrantPlugins module ContainerProvisioner class Client def initialize(machine, container_command) @machine = machine @container_command = container_command end # Build an image given a path to a Dockerfile # # @param [Array>] - Path and options to the Dockerfile to pass to # container build command def build_images(images) @machine.communicate.tap do |comm| images.each do |path, opts| @machine.ui.info(I18n.t("vagrant.container_building_single", path: path)) comm.sudo("#{@container_command} build #{opts[:args]} #{path}") do |type, data| handle_comm(type, data) end end end end # Pull image given a list of images # # @param [String] - Image name def pull_images(*images) @machine.communicate.tap do |comm| images.each do |image| @machine.ui.info(I18n.t("vagrant.container_pulling_single", name: image)) comm.sudo("#{@container_command} pull #{image}") do |type, data| handle_comm(type, data) end end end end def run(containers) containers.each do |name, config| cids_dir = "/var/lib/vagrant/cids" config[:cidfile] ||= "#{cids_dir}/#{Digest::SHA1.hexdigest name}" @machine.ui.info(I18n.t("vagrant.container_running", name: name)) @machine.communicate.sudo("mkdir -p #{cids_dir}") run_container({ name: name, original_name: name, }.merge(config)) end end # Run a OCI container. If the container does not exist it will be # created. If the image is stale it will be recreated and restarted def run_container(config) raise "Container's cidfile was not provided!" if !config[:cidfile] id = "$(cat #{config[:cidfile]})" if container_exists?(id) if container_args_changed?(config) @machine.ui.info(I18n.t("vagrant.container_restarting_container_args", name: config[:name], )) stop_container(id) create_container(config) elsif container_image_changed?(config) @machine.ui.info(I18n.t("vagrant.container_restarting_container_image", name: config[:name], )) stop_container(id) create_container(config) else start_container(id) end else create_container(config) end end def container_exists?(id) lookup_container(id, true) end # Start container # # @param String - Image id def start_container(id) if !container_running?(id) @machine.communicate.sudo("#{@container_command} start #{id}") end end # Stop and remove container # # @param String - Image id def stop_container(id) @machine.communicate.sudo %[ #{@container_command} stop #{id} #{@container_command} rm #{id} ] end def container_running?(id) lookup_container(id) end def container_image_changed?(config) # Returns true if there is a container running with the given :name, # and the container is not using the latest :image. # Here, " inspect " returns the id of the image # that the container is using. We check that the latest image that # has been built with that name (:image) matches the one that the # container is running. cmd = ("#{@container_command} inspect --format='{{.Image}}' #{config[:name]} |" + " grep $(#{@container_command} images -q #{config[:image]})") return !@machine.communicate.test(cmd) end def container_args_changed?(config) path = container_data_path(config) return true if !path.exist? args = container_run_args(config) sha = Digest::SHA1.hexdigest(args) return true if path.read.chomp != sha return false end def create_container(config) args = container_run_args(config) @machine.communicate.sudo %[rm -f "#{config[:cidfile]}"] @machine.communicate.sudo %[#{@container_command} run #{args}] sha = Digest::SHA1.hexdigest(args) container_data_path(config).open("w+") do |f| f.write(sha) end end # Looks up if a container with a given id exists using the # `ps` command. Returns Boolean # # @param String - Image id def lookup_container(id, list_all = false) container_ps = "sudo #{@container_command} ps -q" container_ps << " -a" if list_all @machine.communicate.tap do |comm| return comm.test("#{container_ps} --no-trunc | grep -wFq #{id}") end end def container_name(config) name = config[:name] # If the name is the automatically assigned name, then # replace the "/" with "-" because "/" is not a valid # character for a container name. name = name.gsub("/", "-").gsub(":", "-") if name == config[:original_name] name end # Compiles run arguments to be appended to command string. # Returns String def container_run_args(config) name = container_name(config) args = "--cidfile=#{config[:cidfile]} " args << "-d " if config[:daemonize] args << "--name #{name} " if name && config[:auto_assign_name] args << "--restart=#{config[:restart]}" if config[:restart] args << " #{config[:args]}" if config[:args] "#{args} #{config[:image]} #{config[:cmd]}".strip end def container_data_path(config) name = container_name(config) @machine.data_dir.join("#{@container_command}-#{name}") end protected # This handles outputting the communication data back to the UI def handle_comm(type, data) if [:stderr, :stdout].include?(type) # Clear out the newline since we add one data = data.chomp return if data.empty? options = {} #options[:color] = color if !config.keep_color @machine.ui.info(data.chomp, **options) end end end end end ================================================ FILE: plugins/provisioners/container/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'set' module VagrantPlugins module ContainerProvisioner class Config < Vagrant.plugin("2", :config) attr_reader :images attr_accessor :post_install_provisioner def initialize @images = Set.new @post_install_provisioner = nil @__build_images = [] @__containers = Hash.new { |h, k| h[k] = {} } end # Accessor for internal state. def build_images @__build_images end # Accessor for the internal state. def containers @__containers end # Defines an image to build using ` build` within the machine. # # @param [String] path Path to the Dockerfile to pass to # container build command def build_image(path, **opts) @__build_images << [path, opts] end def images=(images) @images = Set.new(images) end def pull_images(*images) @images += images.map(&:to_s) end def post_install_provision(name, **options, &block) proxy = VagrantPlugins::Kernel_V2::VMConfig.new proxy.provision(name, **options, &block) @post_install_provisioner = proxy.provisioners.first end def run(name, **options) @__containers[name.to_s] = options.dup end def merge(other) super.tap do |result| result.pull_images(*(other.images + self.images)) build_images = @__build_images.dup build_images += other.build_images result.instance_variable_set(:@__build_images, build_images) containers = {} @__containers.each do |name, params| containers[name] = params.dup end other.containers.each do |name, params| containers[name] = @__containers[name].merge(params) end result.instance_variable_set(:@__containers, containers) end end def finalize! @__containers.each do |name, params| params[:image] ||= name params[:auto_assign_name] = true if !params.key?(:auto_assign_name) params[:daemonize] = true if !params.key?(:daemonize) params[:restart] = "always" if !params.key?(:restart) end end end end end ================================================ FILE: plugins/provisioners/container/installer.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module ContainerProvisioner class Installer def initialize(machine) @machine = machine end def ensure_installed # nothing to do end end end end ================================================ FILE: plugins/provisioners/container/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module ContainerProvisioner class Plugin < Vagrant.plugin("2") name "container" description <<-DESC Provides support for provisioning your virtual machines with OCI images and containers. DESC config(:container, :provisioner) do require_relative "config" Config end provisioner(:container) do require_relative "provisioner" Provisioner end end end end ================================================ FILE: plugins/provisioners/container/provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "client" require_relative "installer" module VagrantPlugins module ContainerProvisioner class Provisioner < Vagrant.plugin("2", :provisioner) def initialize(machine, config, installer = nil, client = nil) super(machine, config) @installer = installer || Installer.new(@machine) @client = client || Client.new(@machine, "") @logger = Log4r::Logger.new("vagrant::provisioners::container") end def provision # nothing to do end def run_provisioner(env) klass = Vagrant.plugin("2").manager.provisioners[env[:provisioner].type] result = klass.new(env[:machine], env[:provisioner].config) result.config.finalize! result.provision end end end end ================================================ FILE: plugins/provisioners/docker/cap/centos/docker_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvisioner module Cap module Centos module DockerInstall def self.docker_install(machine) machine.communicate.tap do |comm| comm.sudo("yum -q -y update") comm.sudo("yum -q -y remove docker-io* || true") comm.sudo("yum install -y -q yum-utils") comm.sudo("yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo") comm.sudo("yum makecache") comm.sudo("yum install -y -q docker-ce") end case machine.guest.capability("flavor") when :centos docker_enable_service(machine) else docker_enable_systemctl(machine) end end def self.docker_enable_systemctl(machine) machine.communicate.tap do |comm| comm.sudo("systemctl start docker.service") comm.sudo("systemctl enable docker.service") end end def self.docker_enable_service(machine) machine.communicate.tap do |comm| comm.sudo("service docker start") comm.sudo("chkconfig docker on") end end end end end end end ================================================ FILE: plugins/provisioners/docker/cap/centos/docker_start_service.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvisioner module Cap module Centos module DockerStartService def self.docker_start_service(machine) case machine.guest.capability("flavor") when :centos machine.communicate.tap do |comm| comm.sudo("service docker start") comm.sudo("chkconfig docker on") end else machine.communicate.tap do |comm| comm.sudo("systemctl start docker.service") comm.sudo("systemctl enable docker.service") end end end end end end end end ================================================ FILE: plugins/provisioners/docker/cap/debian/docker_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvisioner module Cap module Debian module DockerInstall def self.docker_install(machine) machine.communicate.tap do |comm| comm.sudo("apt-get update -qq -y") comm.sudo("apt-get install -qq -y --force-yes curl apt-transport-https") comm.sudo("apt-get purge -qq -y lxc-docker* || true") comm.sudo("curl -sSL https://get.docker.com/ | sh") end end end end end end end ================================================ FILE: plugins/provisioners/docker/cap/debian/docker_start_service.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvisioner module Cap module Debian module DockerStartService def self.docker_start_service(machine) machine.communicate.sudo("service docker start") end end end end end end ================================================ FILE: plugins/provisioners/docker/cap/fedora/docker_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvisioner module Cap module Fedora module DockerInstall def self.docker_install(machine) 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 ================================================ FILE: plugins/provisioners/docker/cap/linux/docker_configure_vagrant_user.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvisioner module Cap module Linux module DockerConfigureVagrantUser def self.docker_configure_vagrant_user(machine) ssh_info = machine.ssh_info machine.communicate.tap do |comm| if comm.test("getent group docker") && !comm.test("id -Gn | grep docker") comm.sudo("usermod -a -G docker #{ssh_info[:username]}") comm.reset! end end end end end end end end ================================================ FILE: plugins/provisioners/docker/cap/linux/docker_daemon_running.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvisioner module Cap module Linux module DockerDaemonRunning def self.docker_daemon_running(machine) machine.communicate.test("test -f /var/run/docker.pid") end end end end end end ================================================ FILE: plugins/provisioners/docker/cap/linux/docker_installed.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvisioner module Cap module Linux module DockerInstalled def self.docker_installed(machine) paths = [ "/bin/docker", "/usr/bin/docker", "/usr/local/bin/docker", "/usr/sbin/docker", ] paths.each do |p| if machine.communicate.test("test -f #{p}", sudo: true) return true end end return false end end end end end end ================================================ FILE: plugins/provisioners/docker/cap/windows/docker_daemon_running.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module DockerProvisioner module Cap module Windows module DockerDaemonRunning def self.docker_daemon_running(machine) machine.communicate.test("tasklist | find \"`\"dockerd`\"\"") end end end end end end ================================================ FILE: plugins/provisioners/docker/client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../container/client" module VagrantPlugins module DockerProvisioner class Client < VagrantPlugins::ContainerProvisioner::Client def initialize(machine) super(machine, "docker") @container_command = "docker" end def start_service if !daemon_running? && @machine.guest.capability?(:docker_start_service) @machine.guest.capability(:docker_start_service) end end def daemon_running? @machine.guest.capability(:docker_daemon_running) end end end end ================================================ FILE: plugins/provisioners/docker/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../container/config" module VagrantPlugins module DockerProvisioner class Config < VagrantPlugins::ContainerProvisioner::Config def post_install_provision(name, **options, &block) # Abort raise DockerError, :wrong_provisioner if options[:type] == "docker" proxy = VagrantPlugins::Kernel_V2::VMConfig.new proxy.provision(name, **options, &block) @post_install_provisioner = proxy.provisioners.first end end end end ================================================ FILE: plugins/provisioners/docker/installer.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../container/installer" module VagrantPlugins module DockerProvisioner class Installer < VagrantPlugins::ContainerProvisioner::Installer # This handles verifying the Docker installation, installing it if it was # requested, and so on. This method will raise exceptions if things are # wrong. # @return [Boolean] - false if docker cannot be detected on machine, else # true if docker installs correctly or is installed def ensure_installed if !@machine.guest.capability?(:docker_installed) @machine.ui.warn(I18n.t("vagrant.docker_cant_detect")) return false end if !@machine.guest.capability(:docker_installed) @machine.ui.detail(I18n.t("vagrant.docker_installing")) @machine.guest.capability(:docker_install) end if !@machine.guest.capability(:docker_installed) raise DockerError, :install_failed end if @machine.guest.capability?(:docker_configure_vagrant_user) @machine.guest.capability(:docker_configure_vagrant_user) end true end end end end ================================================ FILE: plugins/provisioners/docker/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module DockerProvisioner class Plugin < Vagrant.plugin("2") name "docker" description <<-DESC Provides support for provisioning your virtual machines with Docker images and containers. DESC config(:docker, :provisioner) do require_relative "config" Config end guest_capability("debian", "docker_install") do require_relative "cap/debian/docker_install" Cap::Debian::DockerInstall end guest_capability("debian", "docker_start_service") do require_relative "cap/debian/docker_start_service" Cap::Debian::DockerStartService end guest_capability("fedora", "docker_install") do require_relative "cap/fedora/docker_install" Cap::Fedora::DockerInstall end guest_capability("centos", "docker_install") do require_relative "cap/centos/docker_install" Cap::Centos::DockerInstall end guest_capability("centos", "docker_start_service") do require_relative "cap/centos/docker_start_service" Cap::Centos::DockerStartService end guest_capability("linux", "docker_installed") do require_relative "cap/linux/docker_installed" Cap::Linux::DockerInstalled end guest_capability("linux", "docker_configure_vagrant_user") do require_relative "cap/linux/docker_configure_vagrant_user" Cap::Linux::DockerConfigureVagrantUser end guest_capability("linux", "docker_daemon_running") do require_relative "cap/linux/docker_daemon_running" Cap::Linux::DockerDaemonRunning end guest_capability("windows", "docker_daemon_running") do require_relative "cap/windows/docker_daemon_running" Cap::Windows::DockerDaemonRunning end provisioner(:docker) do require_relative "provisioner" Provisioner end end end end ================================================ FILE: plugins/provisioners/docker/provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../container/provisioner" require_relative "client" require_relative "installer" module VagrantPlugins module DockerProvisioner class DockerError < Vagrant::Errors::VagrantError error_namespace("vagrant.provisioners.docker") end class Provisioner < VagrantPlugins::ContainerProvisioner::Provisioner def initialize(machine, config, installer = nil, client = nil) super(machine, config) @installer = installer || Installer.new(@machine) @client = client || Client.new(@machine) @logger = Log4r::Logger.new("vagrant::provisioners::docker") end def provision @logger.info("Checking for Docker installation...") if @installer.ensure_installed if !config.post_install_provisioner.nil? @logger.info("Running post setup provision script...") env = { callable: method(:run_provisioner), provisioner: config.post_install_provisioner, machine: machine} machine.env.hook(:run_provisioner, env) end end # Attempt to start service if not running @client.start_service raise DockerError, :not_running if !@client.daemon_running? if config.images.any? @machine.ui.info(I18n.t("vagrant.docker_pulling_images")) @client.pull_images(*config.images) end if config.build_images.any? @machine.ui.info(I18n.t("vagrant.docker_building_images")) @client.build_images(config.build_images) end if config.containers.any? @machine.ui.info(I18n.t("vagrant.docker_starting_containers")) @client.run(config.containers) end end end end end ================================================ FILE: plugins/provisioners/file/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" module VagrantPlugins module FileUpload class Config < Vagrant.plugin("2", :config) attr_accessor :source attr_accessor :destination def validate(machine) errors = _detected_errors if !source errors << I18n.t("vagrant.provisioners.file.no_source_file") end if !destination errors << I18n.t("vagrant.provisioners.file.no_dest_file") end if source s = Pathname.new(source).expand_path(machine.env.root_path) if !s.exist? errors << I18n.t("vagrant.provisioners.file.path_invalid", path: s.to_s) end end { "File provisioner" => errors } end end end end ================================================ FILE: plugins/provisioners/file/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module FileUpload class Plugin < Vagrant.plugin("2") name "file" description <<-DESC Provides support for provisioning your virtual machines with uploaded files. DESC config(:file, :provisioner) do require File.expand_path("../config", __FILE__) Config end provisioner(:file) do require File.expand_path("../provisioner", __FILE__) Provisioner end end end end ================================================ FILE: plugins/provisioners/file/provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module FileUpload class Provisioner < Vagrant.plugin("2", :provisioner) def provision @machine.communicate.tap do |comm| source = File.expand_path(config.source, @machine.env.cwd) destination = expand_guest_path(config.destination) # If the source is a directory determine if any path modifications # need to be applied to the source for upload behavior. If the original # source value ends with a "." or if the original source does not end # with a "." but the original destination ends with a file separator # then append a "." character to the new source. This ensures that # the contents of the directory are uploaded to the destination and # not folder itself. if File.directory?(source) if config.source.end_with?(".") || (!config.destination.end_with?(File::SEPARATOR) && !config.source.end_with?("#{File::SEPARATOR}.")) source = File.join(source, ".") end end @machine.ui.detail(I18n.t("vagrant.actions.vm.provision.file.locations", src: config.source, dst: config.destination)) # now upload the file comm.upload(source, destination) end end private # Expand the guest path if the guest has the capability def expand_guest_path(destination) if machine.guest.capability?(:shell_expand_guest_path) machine.guest.capability(:shell_expand_guest_path, destination) else destination end end end end end ================================================ FILE: plugins/provisioners/podman/cap/centos/podman_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module PodmanProvisioner module Cap module Centos module PodmanInstall def self.podman_install(machine, kubic) if kubic # Official install instructions for podman # https://podman.io/getting-started/installation.html case machine.guest.capability("flavor") when :centos_7 machine.communicate.tap do |comm| comm.sudo("curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable.repo https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/CentOS_7/devel:kubic:libcontainers:stable.repo") comm.sudo("yum -q -y install podman") end when :centos_8 machine.communicate.tap do |comm| comm.sudo("dnf -y module disable container-tools &> /dev/null || echo 'container-tools module does not exist'") comm.sudo("dnf -y install 'dnf-command(copr)'") comm.sudo("dnf -y copr enable rhcontainerbot/container-selinux") comm.sudo("curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable.repo https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/CentOS_8/devel:kubic:libcontainers:stable.repo") comm.sudo("dnf -y install podman") end end else machine.communicate.tap do |comm| comm.sudo("yum -q -y install podman") end end end end end end end end ================================================ FILE: plugins/provisioners/podman/cap/linux/podman_installed.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module PodmanProvisioner module Cap module Linux module PodmanInstalled def self.podman_installed(machine) machine.communicate.test("command -v podman") end end end end end end ================================================ FILE: plugins/provisioners/podman/cap/redhat/podman_install.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module PodmanProvisioner module Cap module Redhat module PodmanInstall def self.podman_install(machine, kubic) # Official install instructions for podman # https://podman.io/getting-started/installation.html case machine.guest.capability("flavor") when :rhel_7 machine.communicate.tap do |comm| comm.sudo("subscription-manager repos --enable=rhel-7-server-extras-rpms") comm.sudo("yum -q -y install podman") end when :rhel_8 machine.communicate.tap do |comm| comm.sudo("yum module enable -y container-tools") comm.sudo("yum module install -y container-tools") end end end end end end end end ================================================ FILE: plugins/provisioners/podman/client.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../container/client" module VagrantPlugins module PodmanProvisioner class Client < VagrantPlugins::ContainerProvisioner::Client def initialize(machine) super(machine, "podman") @container_command = "podman" end end end end ================================================ FILE: plugins/provisioners/podman/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../container/config" module VagrantPlugins module PodmanProvisioner class Config < VagrantPlugins::ContainerProvisioner::Config attr_accessor :kubic def initialize super() @kubic = UNSET_VALUE end def finalize! super() @kubic = false if @kubic == UNSET_VALUE end def post_install_provision(name, **options, &block) # Abort raise PodmanError, :wrong_provisioner if options[:type] == "podman" proxy = VagrantPlugins::Kernel_V2::VMConfig.new proxy.provision(name, **options, &block) @post_install_provisioner = proxy.provisioners.first end end end end ================================================ FILE: plugins/provisioners/podman/installer.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../container/installer" module VagrantPlugins module PodmanProvisioner class Installer < VagrantPlugins::ContainerProvisioner::Installer # This handles verifying the Podman installation, installing it if it was # requested, and so on. This method will raise exceptions if things are # wrong. # @params [Boolean] - if true install should use kubic project (this will) # add a yum repo. # if false install comes from default yum # @return [Boolean] - false if podman cannot be detected on machine, else # true if podman installs correctly or is installed def ensure_installed(kubic) if !@machine.guest.capability?(:podman_installed) @machine.ui.warn("Podman can not be installed") return false end if !@machine.guest.capability(:podman_installed) @machine.ui.detail("Podman installing") @machine.guest.capability(:podman_install, kubic) end if !@machine.guest.capability(:podman_installed) raise PodmanError, :install_failed end true end end end end ================================================ FILE: plugins/provisioners/podman/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module PodmanProvisioner class Plugin < Vagrant.plugin("2") name "podman" description <<-DESC Provides support for provisioning your virtual machines with OCI images and containers using Podman. DESC config(:podman, :provisioner) do require_relative "config" Config end guest_capability("redhat", "podman_install") do require_relative "cap/redhat/podman_install" Cap::Redhat::PodmanInstall end guest_capability("centos", "podman_install") do require_relative "cap/centos/podman_install" Cap::Centos::PodmanInstall end guest_capability("linux", "podman_installed") do require_relative "cap/linux/podman_installed" Cap::Linux::PodmanInstalled end provisioner(:podman) do require_relative "provisioner" Provisioner end end end end ================================================ FILE: plugins/provisioners/podman/provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../container/provisioner" require_relative "installer" require_relative "client" module VagrantPlugins module PodmanProvisioner class PodmanError < Vagrant::Errors::VagrantError error_namespace("vagrant.provisioners.podman") end class Provisioner < VagrantPlugins::ContainerProvisioner::Provisioner def initialize(machine, config, installer = nil, client = nil) super(machine, config, installer, client) @installer = installer || Installer.new(@machine) @client = client || Client.new(@machine) @logger = Log4r::Logger.new("vagrant::provisioners::podman") end def provision @logger.info("Checking for Podman installation...") if @installer.ensure_installed(config.kubic) if !config.post_install_provisioner.nil? @logger.info("Running post setup provision script...") env = { callable: method(:run_provisioner), provisioner: config.post_install_provisioner, machine: machine} machine.env.hook(:run_provisioner, env) end end if config.images.any? @machine.ui.info(I18n.t("vagrant.docker_pulling_images")) @client.pull_images(*config.images) end if config.build_images.any? @machine.ui.info(I18n.t("vagrant.docker_building_images")) @client.build_images(config.build_images) end if config.containers.any? @machine.ui.info(I18n.t("vagrant.docker_starting_containers")) @client.run(config.containers) end end end end end ================================================ FILE: plugins/provisioners/puppet/config/puppet.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Puppet module Config class Puppet < Vagrant.plugin("2", :config) # The path to Puppet's bin/ directory. # @return [String] attr_accessor :binary_path attr_accessor :facter attr_accessor :structured_facts attr_accessor :hiera_config_path attr_accessor :manifest_file attr_accessor :manifests_path attr_accessor :environment attr_accessor :environment_path attr_accessor :environment_variables attr_accessor :module_path attr_accessor :options attr_accessor :synced_folder_type attr_accessor :synced_folder_args attr_accessor :temp_dir attr_accessor :working_directory def initialize super @binary_path = UNSET_VALUE @hiera_config_path = UNSET_VALUE @manifest_file = UNSET_VALUE @manifests_path = UNSET_VALUE @environment = UNSET_VALUE @environment_path = UNSET_VALUE @environment_variables = UNSET_VALUE @module_path = UNSET_VALUE @options = [] @facter = {} @synced_folder_type = UNSET_VALUE @temp_dir = UNSET_VALUE @working_directory = UNSET_VALUE @structured_facts = UNSET_VALUE end def nfs=(value) puts "DEPRECATION: The 'nfs' setting for the Puppet provisioner is" puts "deprecated. Please use the 'synced_folder_type' setting instead." puts "The 'nfs' setting will be removed in the next version of Vagrant." if value @synced_folder_type = "nfs" else @synced_folder_type = nil end end def merge(other) super.tap do |result| result.facter = @facter.merge(other.facter) end end def finalize! super if @environment_path == UNSET_VALUE && @manifests_path == UNSET_VALUE #If both are unset, assume 'manifests' mode for now. TBD: Switch to environments by default? @manifests_path = [:host, "manifests"] end # If the paths are just strings, assume they are 'host' paths (rather than guest) if @environment_path != UNSET_VALUE && !@environment_path.is_a?(Array) @environment_path = [:host, @environment_path] end if @manifests_path != UNSET_VALUE && !@manifests_path.is_a?(Array) @manifests_path = [:host, @manifests_path] end @hiera_config_path = nil if @hiera_config_path == UNSET_VALUE if @environment_path == UNSET_VALUE @manifests_path[0] = @manifests_path[0].to_sym @environment_path = nil @manifest_file = "default.pp" if @manifest_file == UNSET_VALUE @environment = "" if @environment == UNSET_VALUE else @environment_path[0] = @environment_path[0].to_sym @environment = "production" if @environment == UNSET_VALUE if @manifests_path == UNSET_VALUE @manifests_path = nil end if @manifest_file == UNSET_VALUE @manifest_file = nil end end if @environment_variables == UNSET_VALUE @environment_variables = {} end @binary_path = nil if @binary_path == UNSET_VALUE @module_path = nil if @module_path == UNSET_VALUE @synced_folder_type = nil if @synced_folder_type == UNSET_VALUE @synced_folder_args = nil if @synced_folder_args == UNSET_VALUE @temp_dir = "/tmp/vagrant-puppet" if @temp_dir == UNSET_VALUE @working_directory = nil if @working_directory == UNSET_VALUE @structured_facts = nil if @structured_facts == UNSET_VALUE end # Returns the module paths as an array of paths expanded relative to the # root path. def expanded_module_paths(root_path) return [] if !module_path # Get all the paths and expand them relative to the root path, returning # the array of expanded paths paths = module_path paths = [paths] if !paths.is_a?(Array) paths.map do |path| Pathname.new(path).expand_path(root_path) end end def validate(machine) errors = _detected_errors # Calculate the manifests and module paths based on env this_expanded_module_paths = expanded_module_paths(machine.env.root_path) # Manifests path/file validation if manifests_path != nil && manifests_path[0].to_sym == :host expanded_path = Pathname.new(manifests_path[1]). expand_path(machine.env.root_path) if !expanded_path.directory? errors << I18n.t("vagrant.provisioners.puppet.manifests_path_missing", path: expanded_path.to_s) else expanded_manifest_file = expanded_path.join(manifest_file) if !expanded_manifest_file.file? && !expanded_manifest_file.directory? errors << I18n.t("vagrant.provisioners.puppet.manifest_missing", manifest: expanded_manifest_file.to_s) end end elsif environment_path != nil && environment_path[0].to_sym == :host # Environments path/file validation expanded_path = Pathname.new(environment_path[1]). expand_path(machine.env.root_path) if !expanded_path.directory? errors << I18n.t("vagrant.provisioners.puppet.environment_path_missing", path: expanded_path.to_s) else expanded_environment_file = expanded_path.join(environment) if !expanded_environment_file.file? && !expanded_environment_file.directory? errors << I18n.t("vagrant.provisioners.puppet.environment_missing", environment: environment.to_s, environmentpath: expanded_path.to_s) end end end if environment_path == nil && manifests_path == nil errors << "Please specify either a Puppet environment_path + environment (preferred) or manifests_path (deprecated)." end # Module paths validation this_expanded_module_paths.each do |path| if !path.directory? errors << I18n.t("vagrant.provisioners.puppet.module_path_missing", path: path) end end { "puppet provisioner" => errors } end end end end end ================================================ FILE: plugins/provisioners/puppet/config/puppet_server.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 attr_accessor :options attr_accessor :puppet_server attr_accessor :puppet_node def initialize super @binary_path = UNSET_VALUE @client_cert_path = UNSET_VALUE @client_private_key_path = UNSET_VALUE @facter = {} @options = [] @puppet_node = UNSET_VALUE @puppet_server = UNSET_VALUE end def merge(other) super.tap do |result| result.facter = @facter.merge(other.facter) end end 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 @puppet_server = "puppet" if @puppet_server == UNSET_VALUE end def validate(machine) errors = _detected_errors if (client_cert_path && !client_private_key_path) || (client_private_key_path && !client_cert_path) errors << I18n.t( "vagrant.provisioners.puppet_server.client_cert_and_private_key") end if client_cert_path path = Pathname.new(client_cert_path). expand_path(machine.env.root_path) if !path.file? errors << I18n.t( "vagrant.provisioners.puppet_server.client_cert_not_found") end end if client_private_key_path path = Pathname.new(client_private_key_path). expand_path(machine.env.root_path) if !path.file? errors << I18n.t( "vagrant.provisioners.puppet_server.client_private_key_not_found") end end if !puppet_node && (client_cert_path || client_private_key_path) errors << I18n.t( "vagrant.provisioners.puppet_server.cert_requires_node") end { "puppet server provisioner" => errors } end end end end end ================================================ FILE: plugins/provisioners/puppet/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Puppet class Plugin < Vagrant.plugin("2") name "puppet" description <<-DESC Provides support for provisioning your virtual machines with Puppet either using `puppet apply` or a Puppet server. DESC config(:puppet, :provisioner) do require_relative "config/puppet" Config::Puppet end config(:puppet_server, :provisioner) do require_relative "config/puppet_server" Config::PuppetServer end provisioner(:puppet) do require_relative "provisioner/puppet" Provisioner::Puppet end provisioner(:puppet_server) do require_relative "provisioner/puppet_server" Provisioner::PuppetServer end end end end ================================================ FILE: plugins/provisioners/puppet/provisioner/puppet.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/md5" require "log4r" module VagrantPlugins module Puppet module Provisioner class PuppetError < Vagrant::Errors::VagrantError error_namespace("vagrant.provisioners.puppet") end class Puppet < Vagrant.plugin("2", :provisioner) def initialize(machine, config) super @logger = Log4r::Logger.new("vagrant::provisioners::puppet") end def configure(root_config) # Calculate the paths we're going to use based on the environment root_path = @machine.env.root_path @expanded_module_paths = @config.expanded_module_paths(root_path) # Setup the module paths @module_paths = [] @expanded_module_paths.each_with_index do |path, _| key = Digest::MD5.hexdigest(path.to_s) @module_paths << [path, File.join(config.temp_dir, "modules-#{key}")] end folder_opts = {} folder_opts[:type] = @config.synced_folder_type if @config.synced_folder_type folder_opts[:owner] = "root" if !@config.synced_folder_type folder_opts[:args] = @config.synced_folder_args if @config.synced_folder_args folder_opts[:nfs__quiet] = true if @config.environment_path.is_a?(Array) # Share the environments directory with the guest if @config.environment_path[0].to_sym == :host root_config.vm.synced_folder( File.expand_path(@config.environment_path[1], root_path), environments_guest_path, folder_opts) end end if @config.manifest_file @manifest_file = File.join(manifests_guest_path, @config.manifest_file) # Share the manifests directory with the guest if @config.manifests_path[0].to_sym == :host root_config.vm.synced_folder( File.expand_path(@config.manifests_path[1], root_path), manifests_guest_path, folder_opts) end end # Share the module paths @module_paths.each do |from, to| root_config.vm.synced_folder(from, to, folder_opts) end end def parse_environment_metadata # Parse out the environment manifest path since puppet apply doesn't 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) @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! "$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 end end end end def provision # If the machine has a wait for reboot functionality, then # do that (primarily Windows) if @machine.guest.capability?(:wait_for_reboot) @machine.guest.capability(:wait_for_reboot) end # 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" parse_environment_metadata end # Check that the shared folders are properly shared check = [] if @config.manifests_path.is_a?(Array) && @config.manifests_path[0] == :host check << manifests_guest_path end if @config.environment_path.is_a?(Array) && @config.environment_path[0] == :host check << environments_guest_path end @module_paths.each do |host_path, guest_path| check << guest_path end # Make sure the temporary directory is properly set up if windows? tmp_command = "mkdir -p #{config.temp_dir}" comm_opts = { shell: :powershell} else tmp_command = "mkdir -p #{config.temp_dir}; chmod 0777 #{config.temp_dir}" comm_opts = {} end @machine.communicate.tap do |comm| comm.sudo(tmp_command, comm_opts) end verify_shared_folders(check) # Verify Puppet is installed and run it puppet_bin = "puppet" verify_binary(puppet_bin) # Upload Hiera configuration if we have it @hiera_config_path = nil if config.hiera_config_path local_hiera_path = File.expand_path(config.hiera_config_path, @machine.env.root_path) @hiera_config_path = File.join(config.temp_dir, "hiera.yaml") @machine.communicate.upload(local_hiera_path, @hiera_config_path) end # Build up the structured custom facts if we have any # With structured facts on, we assume the config.facter is yaml. if config.structured_facts && !config.facter.empty? @facter_config_path = "/etc/puppetlabs/facter/facts.d/vagrant_facts.yaml" if windows? @facter_config_path = "/ProgramData/PuppetLabs/facter/facts.d/vagrant_facts.yaml" end t = Tempfile.new("vagrant_facts.yaml") t.write(config.facter.to_yaml) t.close() @machine.communicate.tap do |comm| comm.upload(t.path, File.join(@config.temp_dir, "vagrant_facts.yaml")) comm.sudo("cp #{config.temp_dir}/vagrant_facts.yaml #{@facter_config_path}") end end run_puppet_apply end def manifests_guest_path if config.manifests_path[0] == :host # The path is on the host, so point to where it is shared key = Digest::MD5.hexdigest(config.manifests_path[1]) File.join(config.temp_dir, "manifests-#{key}") else # The path is on the VM, so just point directly to it config.manifests_path[1] end end def environments_guest_path if config.environment_path[0] == :host # The path is on the host, so point to where it is shared File.join(config.temp_dir, "environments") else # The path is on the VM, so just point directly to it config.environment_path[1] end end def verify_binary(binary) # Determine the command to use to test whether Puppet is available. # This is very platform dependent. test_cmd = "sh -c 'command -v #{binary}'" if windows? test_cmd = "where.exe #{binary}" if @config.binary_path test_cmd = "where.exe \"#{@config.binary_path}:#{binary}\"" end end if !machine.communicate.test(test_cmd) @config.binary_path = "/opt/puppetlabs/bin/" @machine.communicate.sudo( "test -x /opt/puppetlabs/bin/#{binary}", error_class: PuppetError, error_key: :not_detected, binary: binary) end end def run_puppet_apply default_module_path = "/etc/puppet/modules" if windows? default_module_path = "/ProgramData/PuppetLabs/puppet/etc/modules" end options = [config.options].flatten module_paths = @module_paths.map { |_, to| to } if !@module_paths.empty? # Append the default module path module_paths << default_module_path # Add the command line switch to add the module path module_path_sep = windows? ? ";" : ":" options << "--modulepath '#{module_paths.join(module_path_sep)}'" end if @hiera_config_path options << "--hiera_config=#{@hiera_config_path}" end if !@machine.env.ui.color? options << "--color=false" end options << "--detailed-exitcodes" if config.environment_path options << "--environmentpath #{environments_guest_path}/" options << "--environment #{@config.environment}" end options << @manifest_file options = options.join(" ") # Build up the custom facts if we have any facter = nil # Build up the (non-structured) custom facts if we have any if !config.structured_facts && !config.facter.empty? facts = [] config.facter.each do |key, value| facts << "FACTER_#{key}='#{value}'" end # If we're on Windows, we need to use the PowerShell style if windows? facts.map! { |v| "$env:#{v};" } end facter = facts.join(" ") end puppet_bin = "puppet" if @config.binary_path puppet_bin = File.join(@config.binary_path, puppet_bin) end env_vars = nil if !config.environment_variables.nil? && !config.environment_variables.empty? env_vars = config.environment_variables.map do |env_key, env_value| "#{env_key}=\"#{env_value}\"" end if windows? env_vars.map! do |env_var_string| "$env:#{env_var_string}" end env_vars = env_vars.join("; ") env_vars << ";" else env_vars = env_vars.join(" ") end end command = [ env_vars, facter, puppet_bin, "apply", options ].compact.map(&:to_s).join(" ") if config.working_directory if windows? command = "cd #{config.working_directory}; if ($?) \{ #{command} \}" else command = "cd #{config.working_directory} && #{command}" end end if config.environment_path @machine.ui.info(I18n.t( "vagrant.provisioners.puppet.running_puppet_env", environment: config.environment)) else @machine.ui.info(I18n.t( "vagrant.provisioners.puppet.running_puppet", manifest: config.manifest_file)) end opts = { elevated: true, error_class: Vagrant::Errors::VagrantError, error_key: :ssh_bad_exit_status_muted, good_exit: [0,2], } if windows? opts[:shell] = :powershell end @machine.communicate.sudo(command, opts) do |type, data| if !data.chomp.empty? @machine.ui.info(data.chomp) end end end def verify_shared_folders(folders) folders.each do |folder| @logger.debug("Checking for shared folder: #{folder}") if windows? testcommand = "Test-Path #{folder}" comm_opts = { shell: :powershell} else testcommand = "test -d #{folder}" comm_opts = { sudo: true} end if !@machine.communicate.test(testcommand, comm_opts) raise PuppetError, :missing_shared_folders end end end def windows? @machine.config.vm.communicator == :winrm || @machine.config.vm.communicator == :winssh end end end end end ================================================ FILE: plugins/provisioners/puppet/provisioner/puppet_server.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module Puppet module Provisioner class PuppetServerError < Vagrant::Errors::VagrantError error_namespace("vagrant.provisioners.puppet_server") end class PuppetServer < Vagrant.plugin("2", :provisioner) def provision if @machine.config.vm.communicator == :winrm raise Vagrant::Errors::ProvisionerWinRMUnsupported, name: "puppet_server" end verify_binary("puppet") run_puppet_agent 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( test_cmd, error_class: PuppetServerError, error_key: :not_detected, binary: binary) end def run_puppet_agent options = config.options options = [options] if !options.is_a?(Array) # Intelligently set the puppet node cert name based on certain # external parameters. cn = nil if config.puppet_node # If a node name is given, we use that directly for the certname cn = config.puppet_node elsif @machine.config.vm.hostname # If a host name is given, we explicitly set the certname to # nil so that the hostname becomes the cert name. cn = nil else # Otherwise, we default to the name of the box. cn = @machine.config.vm.box end # Add the certname option if there is one options += ["--certname", cn] if cn # A shortcut to make things easier comm = @machine.communicate # If we have client certs specified, then upload them if config.client_cert_path && config.client_private_key_path @machine.ui.info( I18n.t("vagrant.provisioners.puppet_server.uploading_client_cert")) dirname = "/tmp/puppet-#{Time.now.to_i}-#{rand(1000)}" comm.sudo("mkdir -p #{dirname}") comm.sudo("mkdir -p #{dirname}/certs") comm.sudo("mkdir -p #{dirname}/private_keys") comm.sudo("chmod -R 0777 #{dirname}") comm.upload(config.client_cert_path, "#{dirname}/certs/#{cn}.pem") comm.upload(config.client_private_key_path, "#{dirname}/private_keys/#{cn}.pem") # Setup the options so that they point to our directories options << "--certdir=#{dirname}/certs" options << "--privatekeydir=#{dirname}/private_keys" end # Disable colors if we must if !@machine.env.ui.color? options << "--color=false" end # Build up the custom facts if we have any facter = "" if !config.facter.empty? facts = [] config.facter.each do |key, value| facts << "FACTER_#{key}='#{value}'" end 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_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") @machine.communicate.sudo(command) do |type, data| if !data.chomp.empty? @machine.ui.info(data.chomp) end end end end end end end ================================================ FILE: plugins/provisioners/salt/bootstrap_downloader.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'open-uri' require 'digest' require_relative "./errors" module VagrantPlugins module Salt class BootstrapDownloader WINDOWS_URL = "https://github.com/saltstack/salt-bootstrap/releases/latest/download/bootstrap-salt.ps1" URL = "https://github.com/saltstack/salt-bootstrap/releases/latest/download/bootstrap-salt.sh" SHA256_SUFFIX = ".sha256" def initialize(guest) @guest = guest @logger = Log4r::Logger.new("vagrant::salt::bootstrap_downloader") end def source_url @guest == :windows ? WINDOWS_URL : URL end def get_bootstrap_script @logger.debug "Downloading bootstrap script from #{source_url}" script_file = download(source_url) verify_sha256(script_file) @logger.info "Downloaded and verified salt-bootstrap script" script_file end def verify_sha256(script) @logger.debug "Downloading sha256 file from #{source_url}#{SHA256_SUFFIX}" sha256_file = download("#{source_url}#{SHA256_SUFFIX}") sha256 = extract_sha256(sha256_file.read) sha256_file.close @logger.debug "Computing sha256 value from script file" computed_sha256 = Digest::SHA256.hexdigest(script.read) script.rewind @logger.debug "Comparing sha256 values" if computed_sha256 != sha256 @logger.debug "Mismatched sha256, expected #{sha256} but computed #{computed_sha256}" raise Salt::Errors::InvalidShasumError, source: source_url, expected_sha: sha256, computed_sha: computed_sha256 end @logger.debug "Sha256 values match" end def extract_sha256(text) text.scan(/\b([a-f0-9]{64})\b/).last.first end def download(url) URI(url).open end end end end ================================================ FILE: plugins/provisioners/salt/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" require "vagrant/util/deep_merge" module VagrantPlugins module Salt class Config < Vagrant.plugin("2", :config) ## salty-vagrant options attr_accessor :minion_config attr_accessor :minion_json_config attr_accessor :minion_key attr_accessor :minion_pub attr_accessor :master_config attr_accessor :master_json_config attr_accessor :master_key attr_accessor :master_pub attr_accessor :grains_config attr_accessor :run_highstate attr_accessor :run_overstate attr_accessor :orchestrations attr_accessor :always_install attr_accessor :bootstrap_script attr_accessor :verbose attr_accessor :seed_master attr_reader :pillar_data attr_accessor :colorize attr_accessor :log_level attr_accessor :masterless attr_accessor :minion_id attr_accessor :salt_call_args attr_accessor :salt_args ## bootstrap options attr_accessor :temp_config_dir attr_accessor :install_type attr_accessor :install_args attr_accessor :install_master attr_accessor :install_syndic attr_accessor :no_minion attr_accessor :bootstrap_options attr_accessor :version attr_accessor :python_version attr_accessor :run_service attr_accessor :master_id def initialize @minion_config = UNSET_VALUE @minion_json_config = UNSET_VALUE @minion_key = UNSET_VALUE @minion_pub = UNSET_VALUE @master_config = UNSET_VALUE @master_json_config = UNSET_VALUE @master_key = UNSET_VALUE @master_pub = UNSET_VALUE @grains_config = UNSET_VALUE @run_highstate = UNSET_VALUE @run_overstate = UNSET_VALUE @always_install = UNSET_VALUE @bootstrap_script = UNSET_VALUE @verbose = UNSET_VALUE @seed_master = UNSET_VALUE @pillar_data = UNSET_VALUE @colorize = UNSET_VALUE @log_level = UNSET_VALUE @temp_config_dir = UNSET_VALUE @install_type = UNSET_VALUE @install_args = UNSET_VALUE @install_master = UNSET_VALUE @install_syndic = UNSET_VALUE @no_minion = UNSET_VALUE @bootstrap_options = UNSET_VALUE @masterless = UNSET_VALUE @minion_id = UNSET_VALUE @version = UNSET_VALUE @python_version = UNSET_VALUE @run_service = UNSET_VALUE @master_id = UNSET_VALUE @salt_call_args = UNSET_VALUE @salt_args = UNSET_VALUE end def finalize! @grains_config = nil if @grains_config == UNSET_VALUE @run_highstate = nil if @run_highstate == UNSET_VALUE @run_overstate = nil if @run_overstate == UNSET_VALUE @always_install = nil if @always_install == UNSET_VALUE @bootstrap_script = nil if @bootstrap_script == UNSET_VALUE @verbose = nil if @verbose == UNSET_VALUE @seed_master = nil if @seed_master == UNSET_VALUE @pillar_data = {} if @pillar_data == UNSET_VALUE @colorize = nil if @colorize == UNSET_VALUE @log_level = nil if @log_level == UNSET_VALUE @temp_config_dir = nil if @temp_config_dir == UNSET_VALUE @install_type = nil if @install_type == UNSET_VALUE @install_args = nil if @install_args == UNSET_VALUE @install_master = nil if @install_master == UNSET_VALUE @install_syndic = nil if @install_syndic == UNSET_VALUE @no_minion = nil if @no_minion == UNSET_VALUE @bootstrap_options = nil if @bootstrap_options == UNSET_VALUE @masterless = false if @masterless == UNSET_VALUE @minion_id = nil if @minion_id == UNSET_VALUE @version = nil if @version == UNSET_VALUE @python_version = nil if @python_version == UNSET_VALUE @run_service = nil if @run_service == UNSET_VALUE @master_id = nil if @master_id == UNSET_VALUE @salt_call_args = nil if @salt_call_args == UNSET_VALUE @salt_args = nil if @salt_args == UNSET_VALUE @minion_json_config = nil if @minion_json_config == UNSET_VALUE @master_json_config = nil if @master_json_config == UNSET_VALUE # NOTE: Optimistic defaults are set in the provisioner. UNSET_VALUEs # are converted there to allow proper detection of unset values. # @minion_config = nil if @minion_config == UNSET_VALUE # @minion_key = nil if @minion_key == UNSET_VALUE # @minion_pub = nil if @minion_pub == UNSET_VALUE # @master_config = nil if @master_config == UNSET_VALUE # @master_key = nil if @master_key == UNSET_VALUE # @master_pub = nil if @master_pub == UNSET_VALUE end def pillar(data) @pillar_data = {} if @pillar_data == UNSET_VALUE @pillar_data = Vagrant::Util::DeepMerge.deep_merge(@pillar_data, data) end def validate(machine) errors = _detected_errors if @minion_config && @minion_config != UNSET_VALUE expanded = Pathname.new(@minion_config).expand_path(machine.env.root_path) if !expanded.file? errors << I18n.t("vagrant.provisioners.salt.minion_config_nonexist", missing_config_file: expanded) end end if @master_config && @master_config != UNSET_VALUE expanded = Pathname.new(@master_config).expand_path(machine.env.root_path) if !expanded.file? errors << I18n.t("vagrant.provisioners.salt.master_config_nonexist", missing_config_file: expanded) end end if @minion_key || @minion_pub if !@minion_key || !@minion_pub errors << I18n.t("vagrant.provisioners.salt.missing_key") end end if @master_key || @master_pub if !@master_key || !@master_pub errors << I18n.t("vagrant.provisioners.salt.missing_key") end end if @grains_config expanded = Pathname.new(@grains_config).expand_path(machine.env.root_path) if !expanded.file? errors << I18n.t("vagrant.provisioners.salt.grains_config_nonexist") end end if @install_master && !@no_minion && !@seed_master && @run_highstate errors << I18n.t("vagrant.provisioners.salt.must_accept_keys") end if @salt_call_args && !@salt_call_args.is_a?(Array) errors << I18n.t("vagrant.provisioners.salt.args_array") end if @salt_args && !@salt_args.is_a?(Array) errors << I18n.t("vagrant.provisioners.salt.args_array") end if @python_version && @python_version.is_a?(String) && !@python_version.scan(/\D/).empty? errors << I18n.t("vagrant.provisioners.salt.python_version") end if @python_version && !(@python_version.is_a?(Integer) || @python_version.is_a?(String)) errors << I18n.t("vagrant.provisioners.salt.python_version") end # install_type is not supported in a Windows environment if machine.config.vm.communicator != :winrm if @version && !@install_type errors << I18n.t("vagrant.provisioners.salt.version_type_missing") end end return {"salt provisioner" => errors} end end end end ================================================ FILE: plugins/provisioners/salt/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Salt module Errors class SaltError < Vagrant::Errors::VagrantError error_namespace("vagrant.provisioners.salt") end class InvalidShasumError < SaltError error_key(:salt_invalid_shasum_error) end end end end ================================================ FILE: plugins/provisioners/salt/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Salt class Plugin < Vagrant.plugin("2") name "salt" description <<-DESC Provisions virtual machines using SaltStack DESC config(:salt, :provisioner) do require File.expand_path("../config", __FILE__) Config end provisioner(:salt) do require File.expand_path("../provisioner", __FILE__) Provisioner end end end end ================================================ FILE: plugins/provisioners/salt/provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'json' require_relative "bootstrap_downloader" module VagrantPlugins module Salt class Provisioner < Vagrant.plugin("2", :provisioner) # Default path values to set within configuration only # if configuration value is unset and local path exists OPTIMISTIC_PATH_DEFAULTS = Hash[*[ "minion_config", "salt/minion", "minion_key", "salt/key/minion.key", "minion_pub", "salt/key/minion.pub", "master_config", "salt/master", "master_key", "salt/key/master.key", "master_pub", "salt/key/master.pub" ].map(&:freeze)].freeze def provision set_default_configs upload_configs upload_keys run_bootstrap_script call_overstate call_highstate call_orchestrate end # Return a list of accepted keys def keys(group='minions') out = @machine.communicate.sudo("salt-key --out json") do |type, output| begin if type == :stdout out = JSON::load(output) break out[group] end end end return out end ## Utilities def expanded_path(rel_path) Pathname.new(rel_path).expand_path(@machine.env.root_path) end def binaries_found if @machine.config.vm.communicator == :winrm which_cmd = "Get-Command" else which_cmd = "which" end ## Determine States, ie: install vs configure desired_binaries = [] if !@config.no_minion desired_binaries.push('salt-minion') desired_binaries.push('salt-call') end if @config.install_master if @machine.config.vm.communicator == :winrm raise Vagrant::Errors::ProvisionerWinRMUnsupported, name: "salt.install_master" else desired_binaries.push('salt-master') end end if @config.install_syndic if @machine.config.vm.communicator == :winrm raise Vagrant::Errors::ProvisionerWinRMUnsupported, name: "salt.install_syndic" else desired_binaries.push('salt-syndic') end end found = true for binary in desired_binaries @machine.env.ui.info "Checking if %s is installed" % binary if !@machine.communicate.test("%s %s" % [which_cmd, binary]) @machine.env.ui.info "%s was not found." % binary found = false else @machine.env.ui.info "%s found" % binary end end return found end def need_configure @config.minion_config or @config.minion_key or @config.master_config or @config.master_key or @config.grains_config or @config.version or @config.minion_json_config or @config.master_json_config end def need_install if @config.always_install return true else return !binaries_found() end end def temp_config_dir if @machine.config.vm.communicator == :winrm return @config.temp_config_dir || "C:\\tmp" else return @config.temp_config_dir || "/tmp" end end # Generates option string for bootstrap script def bootstrap_options(install, configure, config_dir) # Any extra options passed to bootstrap if @config.bootstrap_options options = @config.bootstrap_options else options = "" end if @config.master_json_config && @machine.config.vm.communicator != :winrm config = @config.master_json_config options = "%s -J '#{config}'" % [options] end if @config.minion_json_config && @machine.config.vm.communicator != :winrm config = @config.minion_json_config options = "%s -j '#{config}'" % [options] end if configure && @machine.config.vm.communicator != :winrm options = "%s -F -c %s" % [options, config_dir] end if @config.seed_master && @config.install_master && @machine.config.vm.communicator != :winrm seed_dir = "/tmp/minion-seed-keys" @machine.communicate.sudo("mkdir -p -m777 #{seed_dir}") @config.seed_master.each do |name, keyfile| sourcepath = expanded_path(keyfile).to_s dest = "#{seed_dir}/#{name}" @machine.communicate.upload(sourcepath, dest) end options = "#{options} -k #{seed_dir}" end if configure && !install if @machine.config.vm.communicator == :winrm options = "%s -ConfigureOnly" % options else options = "%s -C" % options end end if @config.install_master && @machine.config.vm.communicator != :winrm options = "%s -M" % options end if @config.install_syndic && @machine.config.vm.communicator != :winrm options = "%s -S" % options end if @config.no_minion && @machine.config.vm.communicator != :winrm options = "%s -N" % options end if @config.python_version && @machine.config.vm.communicator != :winrm options = "%s -x python%s" % [options, @config.python_version] end if @config.install_type && @machine.config.vm.communicator != :winrm options = "%s %s" % [options, @config.install_type] end if @config.install_args && @machine.config.vm.communicator != :winrm options = "%s %s" % [options, @config.install_args] end if @config.verbose @machine.env.ui.info "Using Bootstrap Options: %s" % options end return options end ## Actions # Get pillar string to pass with the salt command def get_pillar if !@config.pillar_data.empty? if @machine.config.vm.communicator == :winrm # ' doesn't have any special behavior on the command prompt, # so '{"x":"y"}' becomes '{x:y}' with literal single quotes. # However, """ will become " , and \\""" will become \" . # Use \\"" instead of \\""" for literal inner-value quotes # to avoid issue with odd number of quotes. # --% disables special PowerShell parsing on the rest of the line. " --% pillar=#{@config.pillar_data.to_json.gsub(/(? errors } end # Args are optional, but if they're provided we only support them as a # string or as an array. def args_valid? return true if !args return true if args.is_a?(String) return true if args.is_a?(Integer) if args.is_a?(Array) args.each do |a| return false if !a.kind_of?(String) && !a.kind_of?(Integer) end return true end end def remote? path =~ URI.regexp(["ftp", "http", "https"]) end end end end ================================================ FILE: plugins/provisioners/shell/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module Shell class Plugin < Vagrant.plugin("2") name "shell" description <<-DESC Provides support for provisioning your virtual machines with shell scripts. DESC config(:shell, :provisioner) do require File.expand_path("../config", __FILE__) Config end provisioner(:shell) do require File.expand_path("../provisioner", __FILE__) Provisioner end end end end ================================================ FILE: plugins/provisioners/shell/provisioner.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tempfile" require "vagrant/util/downloader" require "vagrant/util/line_buffer" require "vagrant/util/retryable" module VagrantPlugins module Shell class Provisioner < Vagrant.plugin("2", :provisioner) include Vagrant::Util::Retryable DEFAULT_WINDOWS_SHELL_EXT = ".ps1".freeze CMD_WINDOWS_SHELL_EXT = ".bat".freeze def provision args = "" if config.args.is_a?(String) args = " #{config.args.to_s}" elsif config.args.is_a?(Array) args = config.args.map { |a| quote_and_escape(a) } args = " #{args.join(" ")}" end # In cases where the connection is just being reset # bail out before attempting to do any actual provisioning return if !config.path && !config.inline case @machine.config.vm.communicator when :winrm provision_winrm(args) when :winssh provision_winssh(args) else provision_ssh(args) end ensure if config.reboot @machine.guest.capability(:reboot) else @machine.communicate.reset! if config.reset end end def upload_path if !defined?(@_upload_path) case @machine.config.vm.guest when :windows @_upload_path = Vagrant::Util::Platform.unix_windows_path(config.upload_path.to_s) else @_upload_path = config.upload_path.to_s end if @_upload_path.empty? case @machine.config.vm.guest when :windows @_upload_path = "C:/tmp/vagrant-shell" else @_upload_path = "/tmp/vagrant-shell" end end end @_upload_path end protected def build_outputs outputs = { stdout: Vagrant::Util::LineBuffer.new { |line| handle_comm(:stdout, line) }, stderr: Vagrant::Util::LineBuffer.new { |line| handle_comm(:stderr, line) }, } block = proc { |type, data| outputs[type] << data if outputs[type] } return outputs, block end # This handles outputting the communication line back to the UI def handle_comm(type, data) if [:stderr, :stdout].include?(type) # Output the line with the proper color based on the stream. color = type == :stdout ? :green : :red options = {} options[:color] = color if !config.keep_color @machine.ui.detail(data.chomp, **options) end end # This is the provision method called if SSH is what is running # on the remote end, which assumes a POSIX-style host. def provision_ssh(args) env = config.env.map { |k,v| "#{k}=#{quote_and_escape(v.to_s)}" } env = env.join(" ") command = "chmod +x '#{upload_path}'" command << " &&" command << " #{env}" if !env.empty? command << " #{upload_path}#{args}" with_script_file do |path| # Upload the script to the machine @machine.communicate.tap do |comm| # Reset upload path permissions for the current ssh user info = nil retryable(on: Vagrant::Errors::SSHNotReady, tries: 3, sleep: 2) do info = @machine.ssh_info raise Vagrant::Errors::SSHNotReady if info.nil? end comm.upload(path.to_s, upload_path) user = info[:username] comm.sudo("chown -R #{user} #{upload_path}", error_check: false) if config.name @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", script: "script: #{config.name}")) elsif config.path @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", script: path.to_s)) else @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", script: "inline script")) end # Execute it with sudo outputs, handler = build_outputs begin comm.execute( command, sudo: config.privileged, error_key: :ssh_bad_exit_status_muted, &handler ) ensure outputs.values.map(&:close) end end end end # This is the provision method called if Windows OpenSSH is what is running # on the remote end, which assumes a non-POSIX-style host. def provision_winssh(args) with_script_file do |path| # Upload the script to the machine @machine.communicate.tap do |comm| env = config.env.map{|k,v| comm.generate_environment_export(k, v)}.join(';') remote_ext = get_windows_ext(path) remote_path = add_extension(upload_path, remote_ext) if remote_ext == ".bat" command = "#{env}\n cmd.exe /c \"#{remote_path}\" #{args}" else # Copy powershell_args from configuration shell_args = config.powershell_args # For PowerShell scripts bypass the execution policy unless already specified shell_args += " -ExecutionPolicy Bypass" if config.powershell_args !~ /[-\/]ExecutionPolicy/i # CLIXML output is kinda useless, especially on non-windows hosts shell_args += " -OutputFormat Text" if config.powershell_args !~ /[-\/]OutputFormat/i command = "#{env}\npowershell #{shell_args} -file \"#{remote_path}\"#{args}" end # Reset upload path permissions for the current ssh user info = nil retryable(on: Vagrant::Errors::SSHNotReady, tries: 3, sleep: 2) do info = @machine.ssh_info raise Vagrant::Errors::SSHNotReady if info.nil? end comm.upload(path.to_s, remote_path) if config.name @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", script: "script: #{config.name}")) elsif config.path @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", script: path.to_s)) else @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", script: "inline script")) end # Execute it with sudo begin outputs, handler = build_outputs comm.execute( command, shell: :powershell, error_key: :ssh_bad_exit_status_muted, &handler ) ensure outputs.values.map(&:close) end end end end # This provisions using WinRM, which assumes a PowerShell # console on the other side. def provision_winrm(args) if @machine.guest.capability?(:wait_for_reboot) @machine.guest.capability(:wait_for_reboot) end with_script_file do |path| @machine.communicate.tap do |comm| # Make sure that the upload path has an extension, since # having an extension is critical for Windows execution winrm_upload_path = add_extension(upload_path, get_windows_ext(path)) # Upload it comm.upload(path.to_s, winrm_upload_path) # Build the environment env = config.env.map { |k,v| "$env:#{k} = #{quote_and_escape(v.to_s)}" } env = env.join("; ") # Calculate the path that we'll be executing exec_path = winrm_upload_path exec_path.gsub!('/', '\\') exec_path = "c:#{exec_path}" if exec_path.start_with?("\\") # Copy powershell_args from configuration shell_args = config.powershell_args # For PowerShell scripts bypass the execution policy unless already specified shell_args += " -ExecutionPolicy Bypass" if config.powershell_args !~ /[-\/]ExecutionPolicy/i # CLIXML output is kinda useless, especially on non-windows hosts shell_args += " -OutputFormat Text" if config.powershell_args !~ /[-\/]OutputFormat/i command = "\"#{exec_path}\"#{args}" if File.extname(exec_path).downcase == ".ps1" command = "powershell #{shell_args.to_s} -file #{command}" else command = "cmd /q /c #{command}" end # Append the environment if !env.empty? command = "#{env}; #{command}" end if config.name @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", script: "script: #{config.name}")) elsif config.path @machine.ui.detail(I18n.t("vagrant.provisioners.shell.runningas", local: config.path.to_s, remote: exec_path)) else @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", script: "inline PowerShell script")) end # Execute it with sudo begin outputs, handler = build_outputs comm.sudo(command, elevated: config.privileged, interactive: config.powershell_elevated_interactive, &handler ) ensure outputs.values.map(&:close) end end end end # Quote and escape strings for shell execution, thanks to Capistrano. def quote_and_escape(text, quote = '"') "#{quote}#{text.gsub(/#{quote}/) { |m| "#{m}\\#{m}#{m}" }}#{quote}" end def add_extension(path, ext) return path if !File.extname(path.to_s).empty? path + ext end def get_windows_ext(path) remote_ext = File.extname(upload_path.to_s) if remote_ext.empty? remote_ext = File.extname(path.to_s) if remote_ext.empty? remote_ext = @machine.config.winssh.shell == "cmd" ? CMD_WINDOWS_SHELL_EXT : DEFAULT_WINDOWS_SHELL_EXT end end remote_ext end # This method yields the path to a script to upload and execute # on the remote server. This method will properly clean up the # script file if needed. def with_script_file ext = nil script = nil if config.remote? download_path = @machine.env.tmp_path.join( "#{@machine.id}-remote-script") download_path.delete if download_path.file? begin Vagrant::Util::Downloader.new( config.path, download_path, md5: config.md5, sha1: config.sha1, sha256: config.sha256, sha384: config.sha384, sha512: config.sha512 ).download! ext = File.extname(config.path) script = download_path.read ensure download_path.delete if download_path.file? end elsif config.path # Just yield the path to that file... root_path = @machine.env.root_path ext = File.extname(config.path) script = Pathname.new(config.path).expand_path(root_path).read else script = config.inline end # Replace Windows line endings with Unix ones unless binary file # or we're running on Windows. if !config.binary && @machine.config.vm.guest != :windows begin script = script.gsub(/\r\n?$/, "\n") rescue ArgumentError script = script.force_encoding("ASCII-8BIT").gsub(/\r\n?$/, "\n") end end # Otherwise we have an inline script, we need to Tempfile it, # and handle it specially... file = Tempfile.new(['vagrant-shell', ext]) # Unless you set binmode, on a Windows host the shell script will # have CRLF line endings instead of LF line endings, causing havoc # when the guest executes it. This fixes [GH-1181]. file.binmode begin file.write(script) file.fsync file.close yield file.path ensure file.close file.unlink end end end end end ================================================ FILE: plugins/pushes/atlas/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module AtlasPush class Config < Vagrant.plugin("2", :config) # The address of the Atlas server to upload to. By default this will # be the public Atlas server. # # @return [String] attr_accessor :address # The Atlas token to use. If the user has run `vagrant login`, this will # use that token. If the environment variable `ATLAS_TOKEN` is set, the # uploader will use this value. By default, this is nil. # # @return [String, nil] attr_accessor :token # The name of the application to push to. This will be created (with # user confirmation) if it doesn't already exist. # # @return [String] attr_accessor :app # The base directory with file contents to upload. By default this # is the same directory as the Vagrantfile, but you can specify this # if you have a `src` folder or `bin` folder or some other folder # you want to upload. # # @return [String] attr_accessor :dir # Lists of files to include/exclude in what is uploaded. Exclude is # always the last run filter, so if a file is matched in both include # and exclude, it will be excluded. # # The value of the array elements should be a simple file glob relative # to the directory being packaged. # # @return [Array] attr_accessor :includes attr_accessor :excludes # If set to true, Vagrant will automatically use VCS data to determine # the files to upload. As a caveat: uncommitted changes will not be # deployed. # # @return [Boolean] attr_accessor :vcs # The path to the uploader binary to shell out to. This usually # is only set for debugging/development. If not set, the uploader # will be looked for within the Vagrant installer dir followed by # the PATH. # # @return [String] attr_accessor :uploader_path def initialize @address = UNSET_VALUE @token = UNSET_VALUE @app = UNSET_VALUE @dir = UNSET_VALUE @vcs = UNSET_VALUE @includes = [] @excludes = [] @uploader_path = UNSET_VALUE end def merge(other) super.tap do |result| result.includes = self.includes.dup.concat(other.includes).uniq result.excludes = self.excludes.dup.concat(other.excludes).uniq end end def finalize! @address = nil if @address == UNSET_VALUE @token = nil if @token == UNSET_VALUE @token = ENV["ATLAS_TOKEN"] if !@token && ENV["ATLAS_TOKEN"] != "" @app = nil if @app == UNSET_VALUE @dir = "." if @dir == UNSET_VALUE @uploader_path = nil if @uploader_path == UNSET_VALUE @vcs = true if @vcs == UNSET_VALUE end def validate(machine) errors = _detected_errors if missing?(@token) token = token_from_vagrant_login(machine.env) if missing?(token) errors << I18n.t("atlas_push.errors.missing_token") else @token = token end end if missing?(@app) errors << I18n.t("atlas_push.errors.missing_attribute", attribute: "app", ) end if missing?(@dir) errors << I18n.t("atlas_push.errors.missing_attribute", attribute: "dir", ) end { "Atlas push" => errors } end # Add the filepath to the list of includes # @param [String] filepath def include(filepath) @includes << filepath end alias_method :include=, :include # Add the filepath to the list of excludes # @param [String] filepath def exclude(filepath) @excludes << filepath end alias_method :exclude=, :exclude private # Determine if the given string is "missing" (blank) # @return [true, false] def missing?(obj) obj.to_s.strip.empty? end # Attempt to load the token from disk using the vagrant-login plugin. If # the constant is not defined, that means the user is operating in some # bespoke and unsupported Ruby environment. # # @param [Vagrant::Environment] env # # @return [String, nil] # the token, or nil if it does not exist def token_from_vagrant_login(env) client = VagrantPlugins::LoginCommand::Client.new(env) client.token end end end end ================================================ FILE: plugins/pushes/atlas/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module AtlasPush module Errors class Error < Vagrant::Errors::VagrantError error_namespace("atlas_push.errors") end class UploaderNotFound < Error error_key(:uploader_not_found) end end end end ================================================ FILE: plugins/pushes/atlas/locales/en.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: atlas_push: errors: missing_attribute: |- Missing required attribute '%{attribute}'. The Vagrant Atlas Push plugin requires you set this attribute. Please set this attribute in your Vagrantfile, for example: config.push.define "atlas" do |push| push.%{attribute} = "..." end missing_token: |- Missing required configuration parameter 'token'. This is required for Vagrant to securely communicate with your Atlas account. To generate an access token, run 'vagrant login'. uploader_not_found: |- Vagrant was unable to find the Atlas uploader CLI. If your Vagrantfile specifies the path explicitly with "uploader_path", then make sure that path is valid. Otherwise, make sure that you have a valid install of Vagrant. If you installed Vagrant outside of the official installers, the "atlas-upload" binary must exist on your PATH. ================================================ FILE: plugins/pushes/atlas/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module AtlasPush autoload :Errors, File.expand_path("../errors", __FILE__) class Plugin < Vagrant.plugin("2") name "atlas" description <<-DESC Deploy using HashiCorp's Atlas service. DESC config(:atlas, :push) do require_relative "config" init! Config end push(:atlas) do require_relative "push" init! Push end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path("../locales/en.yml", __FILE__) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/pushes/atlas/push.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/safe_exec" require "vagrant/util/subprocess" require "vagrant/util/which" module VagrantPlugins module AtlasPush class Push < Vagrant.plugin("2", :push) UPLOADER_BIN = "atlas-upload".freeze def push uploader = self.uploader_path # If we didn't find the uploader binary it is a critical error raise Errors::UploaderNotFound if !uploader # We found it. Build up the command and the args. execute(uploader) return 0 end # Executes the uploader with the proper flags based on the configuration. # This function shouldn't return since it will exec, but might return # if we're on a system that doesn't support exec, so handle that properly. def execute(uploader) cmd = [] cmd << "-debug" if !Vagrant.log_level.nil? cmd << "-vcs" if config.vcs cmd += config.includes.map { |v| ["-include", v] } cmd += config.excludes.map { |v| ["-exclude", v] } cmd += metadata.map { |k,v| ["-metadata", "#{k}=#{v}"] } cmd += ["-address", config.address] if config.address cmd += ["-token", config.token] if config.token cmd << config.app cmd << File.expand_path(config.dir, env.root_path) Vagrant::Util::SafeExec.exec(uploader, *cmd.flatten) end # This returns the path to the uploader binary, or nil if it can't # be found. # # @return [String] def uploader_path # Determine the uploader path if uploader = config.uploader_path return uploader end if Vagrant.in_installer? path = File.join( Vagrant.installer_embedded_dir, "bin", UPLOADER_BIN) return path if File.file?(path) end return Vagrant::Util::Which.which(UPLOADER_BIN) end # The metadata command for this push. # # @return [Array] def metadata box = env.vagrantfile.config.vm.box box_url = env.vagrantfile.config.vm.box_url result = {} if !box.nil? && !box.empty? result["box"] = box end if !box_url.nil? && !box_url.empty? result["box_url"] = Array(box_url).first end return result end include Vagrant::Util::CommandDeprecation::Complete def deprecation_command_name "push (atlas strategy)" end end end end ================================================ FILE: plugins/pushes/ftp/adapter.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" module VagrantPlugins module FTPPush class Adapter attr_reader :host attr_reader :port attr_reader :username attr_reader :password attr_reader :options attr_reader :server def initialize(host, username, password, options = {}) @host, @port = parse_host(host) @username = username @password = password @options = options @server = nil end # Parse the host into it's url and port parts. # @return [Array] def parse_host(host) if host.include?(":") split = host.split(":", 2) [split[0], split[1].to_i] else [host, default_port] end end def default_port raise NotImplementedError end def connect(&block) raise NotImplementedError end def upload(local, remote) raise NotImplementedError end end # # The FTP Adapter # class FTPAdapter < Adapter def initialize(*) require "net/ftp" super end def default_port 21 end def connect(&block) @server = Net::FTP.new @server.passive = options.fetch(:passive, true) @server.connect(host, port) @server.login(username, password) begin yield self ensure @server.close end end def upload(local, remote) parent = File.dirname(remote) fullpath = Pathname.new(File.expand_path(parent, pwd)) # Create the parent directories if they does not exist (naive mkdir -p) fullpath.descend do |path| if !directory_exists?(path.to_s) @server.mkdir(path.to_s) end end # Upload the file @server.putbinaryfile(local, remote) end def directory_exists?(path) begin @server.chdir(path) return true rescue Net::FTPPermError return false end end private def pwd @pwd ||= @server.pwd end end # # The SFTP Adapter # class SFTPAdapter < Adapter def initialize(*) require "net/sftp" super @dirs = {} end def default_port 22 end def connect(&block) Net::SFTP.start(@host, @username, password: @password, port: @port) do |server| @server = server yield self end end def upload(local, remote) dir = File.dirname(remote) fullpath = Pathname.new(dir) fullpath.descend do |path| if @dirs[path.to_s].nil? begin @server.mkdir!(path.to_s) # Cache visited directories in a list to avoid duplicate requests @dirs[path.to_s] = true rescue Net::SFTP::StatusException => e # Directory exists, skip... end end end @server.upload!(local, remote) end end end end ================================================ FILE: plugins/pushes/ftp/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module FTPPush class Config < Vagrant.plugin("2", :config) # The (S)FTP host to use. # @return [String] attr_accessor :host # The username to use for authentication with the (S)FTP server. # @return [String] attr_accessor :username # The password to use for authentication with the (S)FTP server. # @return [String] attr_accessor :password # Use passive FTP (default is true). # @return [true, false] attr_accessor :passive # Use secure (SFTP) (default is false). # @return [true, false] attr_accessor :secure # The root destination on the target system to sync the files (default is # /). # @return [String] attr_accessor :destination # Lists of files to include/exclude in what is uploaded. Exclude is # always the last run filter, so if a file is matched in both include # and exclude, it will be excluded. # # The value of the array elements should be a simple file glob relative # to the directory being packaged. # @return [Array] attr_accessor :includes attr_accessor :excludes # The base directory with file contents to upload. By default this # is the same directory as the Vagrantfile, but you can specify this # if you have a `src` folder or `bin` folder or some other folder # you want to upload. # @return [String] attr_accessor :dir def initialize @host = UNSET_VALUE @username = UNSET_VALUE @password = UNSET_VALUE @passive = UNSET_VALUE @secure = UNSET_VALUE @destination = UNSET_VALUE @includes = [] @excludes = [] @dir = UNSET_VALUE end def merge(other) super.tap do |result| result.includes = self.includes.dup.concat(other.includes).uniq result.excludes = self.excludes.dup.concat(other.excludes).uniq end end def finalize! @host = nil if @host == UNSET_VALUE @username = nil if @username == UNSET_VALUE @password = nil if @password == UNSET_VALUE @passive = true if @passive == UNSET_VALUE @secure = false if @secure == UNSET_VALUE @destination = "/" if @destination == UNSET_VALUE @dir = "." if @dir == UNSET_VALUE end def validate(machine) errors = _detected_errors if missing?(@host) errors << I18n.t("ftp_push.errors.missing_attribute", attribute: "host", ) end if missing?(@username) errors << I18n.t("ftp_push.errors.missing_attribute", attribute: "username", ) end if missing?(@destination) errors << I18n.t("ftp_push.errors.missing_attribute", attribute: "destination", ) end if missing?(@dir) errors << I18n.t("ftp_push.errors.missing_attribute", attribute: "dir", ) end { "FTP push" => errors } end # Add the filepath to the list of includes # @param [String] filepath def include(filepath) @includes << filepath end alias_method :include=, :include # Add the filepath to the list of excludes # @param [String] filepath def exclude(filepath) @excludes << filepath end alias_method :exclude=, :exclude private # Determine if the given string is "missing" (blank) # @return [true, false] def missing?(obj) obj.to_s.strip.empty? end end end end ================================================ FILE: plugins/pushes/ftp/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module FTPPush module Errors class Error < Vagrant::Errors::VagrantError error_namespace("ftp_push.errors") end class TooManyFiles < Error error_key(:too_many_files) end end end end ================================================ FILE: plugins/pushes/ftp/locales/en.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: ftp_push: errors: missing_attribute: |- Missing required attribute '%{attribute}'. The Vagrant FTP Push plugin requires you set this attribute. Please set this attribute in your Vagrantfile, for example: config.push.define "ftp" do |push| push.%{attribute} = "..." end too_many_files: |- The configured directory for Vagrant FTP push contains too many files to successfully complete the command. This can be resolved by either removing extraneous files from the configured directory, or updating the `dir` configuration option to a subdirectory. ================================================ FILE: plugins/pushes/ftp/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module FTPPush class Plugin < Vagrant.plugin("2") name "ftp" description <<-DESC Deploy to a remote FTP or SFTP server. DESC config(:ftp, :push) do require File.expand_path("../config", __FILE__) init! Config end push(:ftp) do require File.expand_path("../push", __FILE__) init! Push end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path("../locales/en.yml", __FILE__) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/pushes/ftp/push.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "net/ftp" require "pathname" require_relative "adapter" require_relative "errors" module VagrantPlugins module FTPPush class Push < Vagrant.plugin("2", :push) IGNORED_FILES = %w(. ..).freeze DEFAULT_EXCLUDES = %w(.git .hg .svn .vagrant).freeze def initialize(*) super @logger = Log4r::Logger.new("vagrant::pushes::ftp") end def push # Grab files early so if there's an exception or issue, we don't have to # wait and close the (S)FTP connection as well files = nil begin files = Hash[*all_files.flat_map do |file| relative_path = relative_path_for(file, base_dir) destination = File.join(config.destination, relative_path) file = File.expand_path(file, env.root_path) [file, destination] end] rescue SystemStackError raise Errors::TooManyFiles end ftp = "#{config.username}@#{config.host}:#{config.destination}" env.ui.info "Uploading #{files.length} files to #{env.root_path} to #{ftp}" connect do |ftp| files.each do |local, remote| env.ui.info "Uploading #{local} => #{remote}" ftp.upload(local, remote) end end end # Helper method for creating the FTP or SFTP connection. # @yield [Adapter] def connect(&block) klass = config.secure ? SFTPAdapter : FTPAdapter ftp = klass.new(config.host, config.username, config.password, passive: config.passive) ftp.connect(&block) end # The list of all files that should be pushed by this push. This method # only returns **files**, not folders or symlinks! # @return [Array] def all_files files = glob("#{base_dir}/**/*") + includes_files filter_excludes!(files, config.excludes) files.reject! { |f| !File.file?(f) } files end # The list of files to include in addition to those specified in `dir`. # @return [Array] def includes_files includes = config.includes.flat_map do |i| path = absolute_path_for(i, base_dir) [path, "#{path}/**/*"] end glob("{#{includes.join(",")}}") end # Filter the excludes out of the given list. This method modifies the # given list in memory! # # @param [Array] list # the filepaths # @param [Array] excludes # the exclude patterns or files def filter_excludes!(list, excludes) excludes = Array(excludes) excludes = excludes + DEFAULT_EXCLUDES excludes = excludes.flat_map { |e| [e, "#{e}/*"] } list.reject! do |file| basename = relative_path_for(file, config.dir) # Handle the special case where the file is outside of the working # directory... if basename.start_with?("../") basename = file end excludes.any? { |e| File.fnmatch?(e, basename, File::FNM_DOTMATCH) } end end # Get the list of files that match the given pattern. # @return [Array] def glob(pattern) Dir.glob(pattern, File::FNM_DOTMATCH).sort.reject do |file| IGNORED_FILES.include?(File.basename(file)) end end # The absolute path to the given `path` and `parent`, unless the given # path is absolute. # @return [String] def absolute_path_for(path, parent) path = Pathname.new(path) return path if path.absolute? File.expand_path(path, parent) end # The relative path from the given `parent`. If files exist on another # device, this will probably blow up. # @return [String] def relative_path_for(path, parent) Pathname.new(path).relative_path_from(Pathname.new(parent)).to_s rescue ArgumentError return path end def base_dir if Pathname.new(config.dir).absolute? config.dir else File.join(File.expand_path(env.root_path), config.dir) end end end end end ================================================ FILE: plugins/pushes/heroku/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HerokuPush class Config < Vagrant.plugin("2", :config) # The name of the Heroku application to push to. # @return [String] attr_accessor :app # The base directory with file contents to upload. By default this # is the same directory as the Vagrantfile, but you can specify this # if you have a `src` folder or `bin` folder or some other folder # you want to upload. This directory must be a git repository. # @return [String] attr_accessor :dir # The path to the git binary to shell out to. This usually is only set for # debugging/development. If not set, the git bin will be searched for # in the PATH. # @return [String] attr_accessor :git_bin # The Git remote to push to (default: "heroku"). # @return [String] attr_accessor :remote def initialize @app = UNSET_VALUE @dir = UNSET_VALUE @git_bin = UNSET_VALUE @remote = UNSET_VALUE end def finalize! @app = nil if @app == UNSET_VALUE @dir = "." if @dir == UNSET_VALUE @git_bin = "git" if @git_bin == UNSET_VALUE @remote = "heroku" if @remote == UNSET_VALUE end def validate(machine) errors = _detected_errors if missing?(@dir) errors << I18n.t("heroku_push.errors.missing_attribute", attribute: "dir", ) end if missing?(@git_bin) errors << I18n.t("heroku_push.errors.missing_attribute", attribute: "git_bin", ) end if missing?(@remote) errors << I18n.t("heroku_push.errors.missing_attribute", attribute: "remote", ) end { "Heroku push" => errors } end private # Determine if the given string is "missing" (blank) # @return [true, false] def missing?(obj) obj.to_s.strip.empty? end end end end ================================================ FILE: plugins/pushes/heroku/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module HerokuPush module Errors class Error < Vagrant::Errors::VagrantError error_namespace("heroku_push.errors") end class CommandFailed < Error error_key(:command_failed) end class GitNotFound < Error error_key(:git_not_found) end class NotAGitRepo < Error error_key(:not_a_git_repo) end end end end ================================================ FILE: plugins/pushes/heroku/locales/en.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: heroku_push: errors: command_failed: |- The following command exited with a non-zero exit status: %{cmd} stdout: %{stdout} stderr: %{stderr} git_not_found: |- The Git binary '%{bin}' could not be found. Please ensure you have downloaded and installed the latest version of Git: https://git-scm.com/downloads missing_attribute: |- Missing required attribute '%{attribute}'. The Vagrant Heroku Push plugin requires you set this attribute. Please set this attribute in your Vagrantfile, for example: config.push.define "heroku" do |push| push.%{attribute} = "..." end not_a_git_repo: |- The following path is not a valid Git repository: %{path} Please ensure you are working in the correct directory. In order to use the Vagrant Heroku Push plugin, you must have a git repository. ================================================ FILE: plugins/pushes/heroku/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module HerokuPush class Plugin < Vagrant.plugin("2") name "heroku" description <<-DESC Deploy to a Heroku DESC config(:heroku, :push) do require File.expand_path("../config", __FILE__) init! Config end push(:heroku) do require File.expand_path("../push", __FILE__) init! Push end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path("../locales/en.yml", __FILE__) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/pushes/heroku/push.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant/util/subprocess" require "vagrant/util/which" require_relative "errors" module VagrantPlugins module HerokuPush class Push < Vagrant.plugin("2", :push) def push # Expand any paths relative to the root dir = File.expand_path(config.dir, env.root_path) # Verify git is installed verify_git_bin!(config.git_bin) # Verify we are operating in a git repo verify_git_repo!(dir) # Get the current branch branch = git_branch(dir) # Get the name of the app app = config.app || interpret_app(dir) # Check if we need to add the git remote if !has_git_remote?(config.remote, dir) add_heroku_git_remote(config.remote, app, dir) end # Push to Heroku git_push_heroku(config.remote, branch, dir) end # Verify that git is installed. # @raise [Errors::GitNotFound] def verify_git_bin!(path) if Vagrant::Util::Which.which(path).nil? raise Errors::GitNotFound, bin: path end end # Verify that the given path is a git directory. # @raise [Errors::NotAGitRepo] # @param [String] def verify_git_repo!(path) if !File.directory?(git_dir(path)) raise Errors::NotAGitRepo, path: path end end # Interpret the name of the Heroku application from the given path. # @param [String] path # @return [String] def interpret_app(path) File.basename(path) end # The git directory for the given path. # @param [String] path # @return [String] def git_dir(path) "#{path}/.git" end # The name of the current git branch. # @param [String] path # @return [String] def git_branch(path) result = execute!("git", "--git-dir", git_dir(path), "--work-tree", path, "symbolic-ref", "HEAD", ) # Returns something like "* master" result.stdout.sub("*", "").strip end # Push to the Heroku remote. # @param [String] remote # @param [String] branch def git_push_heroku(remote, branch, path) execute!("git", "--git-dir", git_dir(path), "--work-tree", path, "push", remote, "#{branch}:master", ) end # Check if the git remote has the given remote. # @param [String] remote # @return [true, false] def has_git_remote?(remote, path) result = execute!("git", "--git-dir", git_dir(path), "--work-tree", path, "remote", ) remotes = result.stdout.split(/\r?\n/).map(&:strip) remotes.include?(remote.to_s) end # Add the Heroku to the current repository. # @param [String] remote # @param [String] app def add_heroku_git_remote(remote, app, path) execute!("git", "--git-dir", git_dir(path), "--work-tree", path, "remote", "add", remote, heroku_git_url(app), ) end # The URL for this project on Heroku. # @return [String] def heroku_git_url(app) "git@heroku.com:#{app}.git" end # Execute the command, raising an exception if it fails. # @return [Vagrant::Util::Subprocess::Result] def execute!(*cmd) subproccmd = cmd.dup << { notify: [:stdout, :stderr] } result = Vagrant::Util::Subprocess.execute(*subproccmd) do |type, data| if type == :stdout @env.ui.info(data, new_line: false) elsif type == :stderr @env.ui.warn(data, new_line: false) end end if result.exit_code != 0 raise Errors::CommandFailed, cmd: cmd.join(" "), stdout: result.stdout, stderr: result.stderr end result end end end end ================================================ FILE: plugins/pushes/local-exec/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module LocalExecPush class Config < Vagrant.plugin("2", :config) # The path (relative to the machine root) to a local script that will be # executed. # @return [String] attr_accessor :script # The command (as a string) to execute. # @return [String] attr_accessor :inline # The arguments to provide when executing the script. # @return [Array] attr_accessor :args def initialize @script = UNSET_VALUE @inline = UNSET_VALUE @args = UNSET_VALUE end def finalize! @script = nil if @script == UNSET_VALUE @inline = nil if @inline == UNSET_VALUE @args = nil if @args == UNSET_VALUE if @args && args_valid? @args = @args.is_a?(Array) ? @args.map { |a| a.to_s } : @args.to_s end end def validate(machine) errors = _detected_errors if missing?(@script) && missing?(@inline) errors << I18n.t("local_exec_push.errors.missing_attribute", attribute: "script", ) end if !missing?(@script) && !missing?(@inline) errors << I18n.t("local_exec_push.errors.cannot_specify_script_and_inline") end if !args_valid? errors << I18n.t("local_exec_push.errors.args_bad_type") end { "Local Exec push" => errors } end private # Determine if the given string is "missing" (blank) # @return [true, false] def missing?(obj) obj.to_s.strip.empty? end # Args are optional, but if they're provided we only support them as a # string or as an array. def args_valid? return true if !args return true if args.is_a?(String) return true if args.is_a?(Integer) if args.is_a?(Array) args.each do |a| return false if !a.kind_of?(String) && !a.kind_of?(Integer) end return true end end end end end ================================================ FILE: plugins/pushes/local-exec/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module LocalExecPush module Errors class Error < Vagrant::Errors::VagrantError error_namespace("local_exec_push.errors") end class CommandFailed < Error error_key(:command_failed) end end end end ================================================ FILE: plugins/pushes/local-exec/locales/en.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: local_exec_push: errors: cannot_specify_script_and_inline: |- You have specified both the 'script' and 'inline' attributes for the Vagrant Local Exec Push plugin. You may only specify one of these attributes. command_failed: |- The following command exited with a non-zero exit status: %{cmd} stdout: %{stdout} stderr: %{stderr} missing_attribute: |- Missing required attribute '%{attribute}'. The Vagrant Local Exec Push plugin requires you set this attribute. Please set this attribute in your Vagrantfile, for example: config.push.define "local-exec" do |push| push.%{attribute} = "..." end args_bad_type: "Local-exec push `args` must be a string or array." ================================================ FILE: plugins/pushes/local-exec/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module LocalExecPush class Plugin < Vagrant.plugin("2") name "local-exec" description <<-DESC Run a local command or script to push DESC config(:"local-exec", :push) do require File.expand_path("../config", __FILE__) init! Config end push(:"local-exec") do require File.expand_path("../push", __FILE__) init! Push end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path("../locales/en.yml", __FILE__) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/pushes/local-exec/push.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "tempfile" require "vagrant/util/safe_exec" require_relative "errors" module VagrantPlugins module LocalExecPush class Push < Vagrant.plugin("2", :push) @@logger = Log4r::Logger.new("vagrant::push::local_exec") def push if config.inline execute_inline!(config.inline, config.args) else execute_script!(config.script, config.args) end end # Execute the inline script by writing it to a tempfile and executing. def execute_inline!(inline, args) script = Tempfile.new(["vagrant-local-exec-script", ".sh"]) script.write(inline) script.rewind script.close execute_script!(script.path, args) ensure if script script.close script.unlink end end # Execute the script, expanding the path relative to the current env root. def execute_script!(path, args) path = File.expand_path(path, env.root_path) FileUtils.chmod("+x", path) if args.is_a?(String) args = " #{args.to_s}" elsif args.is_a?(Array) args = args.map { |a| quote_and_escape(a) } args = " #{args.join(" ")}" end execute!("#{path}#{args}") end # Execute the script, raising an exception if it fails. def execute!(*cmd) if Vagrant::Util::Platform.windows? execute_subprocess!(*cmd) else execute_exec!(*cmd) end end private # Quote and escape strings for shell execution, thanks to Capistrano. def quote_and_escape(text, quote = '"') "#{quote}#{text.gsub(/#{quote}/) { |m| "#{m}\\#{m}#{m}" }}#{quote}" end # Run the command as exec (unix). def execute_exec!(*cmd) @@logger.debug("executing command via exec: #{cmd.inspect}") Vagrant::Util::SafeExec.exec(cmd[0], *cmd[1..-1]) end # Run the command as a subprocess (windows). def execute_subprocess!(*cmd) @@logger.debug("executing command via subprocess: #{cmd.inspect}") cmd = cmd.dup << { notify: [:stdout, :stderr] } result = Vagrant::Util::Subprocess.execute(*cmd) do |type, data| if type == :stdout @env.ui.info(data, new_line: false) elsif type == :stderr @env.ui.warn(data, new_line: false) end end Kernel.exit(result.exit_code) end end end end ================================================ FILE: plugins/pushes/noop/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module NoopDeploy class Config < Vagrant.plugin("2", :config) def initialize end def finalize! end def validate(machine) errors = _detected_errors { "Noop push" => errors } end end end end ================================================ FILE: plugins/pushes/noop/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module NoopDeploy class Plugin < Vagrant.plugin("2") name "noop" description <<-DESC Literally do nothing DESC config(:noop, :push) do require File.expand_path("../config", __FILE__) Config end push(:noop) do require File.expand_path("../push", __FILE__) Push end end end end ================================================ FILE: plugins/pushes/noop/push.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module NoopDeploy class Push < Vagrant.plugin("2", :push) def push puts "pushed" end end end end ================================================ FILE: plugins/synced_folders/nfs/action_cleanup.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" module VagrantPlugins module SyncedFolderNFS class ActionCleanup def initialize(app, env) @app = app @logger = Log4r::Logger.new("vagrant::synced_folders::nfs") end def call(env) if !env[:nfs_valid_ids] @logger.warn("nfs_valid_ids not set, cleanup cannot occur") return @app.call(env) end if !env[:machine].env.host.capability?(:nfs_prune) @logger.info("Host doesn't support pruning NFS. Skipping.") return @app.call(env) end @logger.info("NFS pruning. Valid IDs: #{env[:nfs_valid_ids].inspect}") env[:machine].env.host.capability( :nfs_prune, env[:machine].ui, env[:nfs_valid_ids]) @app.call(env) end end end end ================================================ FILE: plugins/synced_folders/nfs/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module SyncedFolderNFS class Config < Vagrant.plugin("2", :config) attr_accessor :functional attr_accessor :map_uid attr_accessor :map_gid attr_accessor :verify_installed def initialize super @functional = UNSET_VALUE @map_uid = UNSET_VALUE @map_gid = UNSET_VALUE @verify_installed = UNSET_VALUE end def finalize! @functional = true if @functional == UNSET_VALUE @map_uid = :auto if @map_uid == UNSET_VALUE @map_gid = :auto if @map_gid == UNSET_VALUE @verify_installed = true if @verify_installed == UNSET_VALUE end def to_s "NFS" end end end end ================================================ FILE: plugins/synced_folders/nfs/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module SyncedFolderNFS # This plugin implements NFS synced folders. In order to take advantage # of NFS synced folders, some provider-specific assistance is required. # Within the middleware sequences, some data must be put into the # environment state bag: # # * `nfs_host_ip` (string) - The IP of the host machine that the NFS # client in the machine should talk to. # * `nfs_machine_ip` (string) - The IP of the guest machine that the NFS # server should serve the folders to. # * `nfs_valid_ids` (array of strings) - A list of IDs that are "valid" # and should not be pruned. The synced folder implementation will # regularly prune NFS exports of invalid IDs. # # If any of these variables are not set, an internal exception will be # raised. # class Plugin < Vagrant.plugin("2") name "NFS synced folders" description <<-EOF The NFS synced folders plugin enables you to use NFS as a synced folder implementation. EOF config("nfs") do require_relative "config" Config end synced_folder("nfs", 5) do require_relative "synced_folder" SyncedFolder end action_hook("nfs_cleanup") do |hook| require_relative "action_cleanup" hook.before( Vagrant::Action::Builtin::SyncedFolderCleanup, ActionCleanup) end end end end ================================================ FILE: plugins/synced_folders/nfs/synced_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'fileutils' require 'thread' require 'zlib' require "log4r" require "vagrant/util/platform" module VagrantPlugins module SyncedFolderNFS # This synced folder requires that two keys be set on the environment # within the middleware sequence: # # - `:nfs_host_ip` - The IP of where to mount the NFS folder from. # - `:nfs_machine_ip` - The IP of the machine where the NFS folder # will be mounted. # class SyncedFolder < Vagrant.plugin("2", :synced_folder) @@lock = Mutex.new def initialize(*args) super @logger = Log4r::Logger.new("vagrant::synced_folders::nfs") end def usable?(machine, raise_error=false) # If the machine explicitly said NFS is not supported, then # it isn't supported. if !machine.config.nfs.functional return false end if machine.env.host.capability?(:nfs_installed) return true if machine.env.host.capability(:nfs_installed) end return false if !raise_error raise Vagrant::Errors::NFSNotSupported end def prepare(machine, folders, opts) # Nothing is necessary to do before VM boot. end def enable(machine, folders, nfsopts) raise Vagrant::Errors::NFSNoHostIP if !nfsopts[:nfs_host_ip] raise Vagrant::Errors::NFSNoGuestIP if !nfsopts[:nfs_machine_ip] if machine.config.nfs.verify_installed if machine.guest.capability?(:nfs_client_installed) installed = machine.guest.capability(:nfs_client_installed) if !installed can_install = machine.guest.capability?(:nfs_client_install) raise Vagrant::Errors::NFSClientNotInstalledInGuest if !can_install machine.ui.info I18n.t("vagrant.actions.vm.nfs.installing") machine.guest.capability(:nfs_client_install) end end end machine_ip = nfsopts[:nfs_machine_ip] machine_ip = [machine_ip] if !machine_ip.is_a?(Array) # Prepare the folder, this means setting up various options # and such on the folder itself. folders.each { |id, opts| prepare_folder(machine, opts) } # Determine what folders we'll export export_folders = folders.dup export_folders.keys.each do |id| opts = export_folders[id] if opts.key?(:nfs_export) && !opts[:nfs_export] export_folders.delete(id) end end # Update the exports when there are actually exports [GH-4148] if !export_folders.empty? # Export the folders. We do this with a class-wide lock because # NFS exporting often requires sudo privilege and we don't want # overlapping input requests. [GH-2680] @@lock.synchronize do begin machine.env.lock("nfs-export") do machine.ui.info I18n.t("vagrant.actions.vm.nfs.exporting") machine.env.host.capability( :nfs_export, machine.ui, machine.id, machine_ip, export_folders) end rescue Vagrant::Errors::EnvironmentLockedError sleep 1 retry end end end # Mount machine.ui.info I18n.t("vagrant.actions.vm.nfs.mounting") # Only mount folders that have a guest path specified. mount_folders = {} folders.each do |id, opts| mount_folders[id] = opts.dup if opts[:guestpath] machine.ui.detail(I18n.t("vagrant.actions.vm.nfs.mounting_entry", guestpath: opts[:guestpath], hostpath: opts[:hostpath] )) end # Mount them! if machine.guest.capability?(:nfs_pre) machine.guest.capability(:nfs_pre) end machine.guest.capability(:mount_nfs_folder, nfsopts[:nfs_host_ip], mount_folders) if machine.guest.capability?(:nfs_post) machine.guest.capability(:nfs_post) end end def cleanup(machine, opts) ids = opts[:nfs_valid_ids] raise Vagrant::Errors::NFSNoValidIds if !ids # Prune any of the unused machines @logger.info("NFS pruning. Valid IDs: #{ids.inspect}") machine.env.host.capability(:nfs_prune, machine.ui, ids) end protected def prepare_folder(machine, opts) opts[:map_uid] = prepare_permission(machine, :uid, opts) opts[:map_gid] = prepare_permission(machine, :gid, opts) opts[:nfs_version] ||= 3 if !opts.key?(:nfs_udp) opts[:nfs_udp] = !opts[:nfs_version].to_s.start_with?('4') end if opts[:nfs_version].to_s.start_with?('4') && opts[:nfs_udp] machine.ui.info I18n.t("vagrant.actions.vm.nfs.v4_with_udp_warning") end # We use a CRC32 to generate a 32-bit checksum so that the # fsid is compatible with both old and new kernels. opts[:uuid] = Zlib.crc32(opts[:hostpath]).to_s end # Prepares the UID/GID settings for a single folder. def prepare_permission(machine, perm, opts) key = "map_#{perm}".to_sym return nil if opts.key?(key) && opts[key].nil? # The options on the hash get priority, then the default # values value = opts.key?(key) ? opts[key] : machine.config.nfs.send(key) return value if value != :auto # Get UID/GID from folder if we've made it this far # (value == :auto) stat = File.stat(opts[:hostpath]) return stat.send(perm) end end end end ================================================ FILE: plugins/synced_folders/rsync/command/rsync.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'optparse' require "vagrant/action/builtin/mixin_synced_folders" require_relative "../helper" module VagrantPlugins module SyncedFolderRSync module Command class Rsync < Vagrant.plugin("2", :command) include Vagrant::Action::Builtin::MixinSyncedFolders def self.synopsis "syncs rsync synced folders to remote machine" end def execute options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant rsync [vm-name]" o.separator "" o.separator "This command forces any synced folders with type 'rsync' to sync." o.separator "RSync is not an automatic sync so a manual command is used." o.separator "" o.separator "Options:" o.separator "" o.on("--[no-]rsync-chown", "Use rsync to modify ownership") do |chown| options[:rsync_chown] = chown end end # Parse the options and return if we don't have any target. argv = parse_options(opts) return if !argv # Go through each machine and perform the rsync error = false with_target_vms(argv) do |machine| if machine.provider.capability?(:proxy_machine) proxy = machine.provider.capability(:proxy_machine) if proxy machine.ui.warn(I18n.t( "vagrant.rsync_proxy_machine", name: machine.name.to_s, provider: machine.provider_name.to_s)) machine = proxy end end if !machine.communicate.ready? machine.ui.error(I18n.t("vagrant.rsync_communicator_not_ready")) error = true next end # Determine the rsync synced folders for this machine folders = synced_folders(machine, cached: true)[:rsync] next if !folders || folders.empty? # Get the SSH info for this machine so we can access it ssh_info = machine.ssh_info # Sync them! folders.each do |id, folder_opts| if options.has_key?(:rsync_chown) folder_opts = folder_opts.merge(rsync_ownership: options[:rsync_chown]) end RsyncHelper.rsync_single(machine, ssh_info, folder_opts) end end return error ? 1 : 0 end end end end end ================================================ FILE: plugins/synced_folders/rsync/command/rsync_auto.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require 'optparse' require "thread" require "vagrant/action/builtin/mixin_synced_folders" require "vagrant/util/busy" require "vagrant/util/platform" require_relative "../helper" require "listen" module VagrantPlugins module SyncedFolderRSync module Command class RsyncAuto < Vagrant.plugin("2", :command) include Vagrant::Action::Builtin::MixinSyncedFolders def self.synopsis "syncs rsync synced folders automatically when files change" end def execute @logger = Log4r::Logger.new("vagrant::commands::rsync-auto") options = {} opts = OptionParser.new do |o| o.banner = "Usage: vagrant rsync-auto [vm-name]" o.separator "" o.separator "Options:" o.separator "" o.on("--[no-]poll", "Force polling filesystem (slow)") do |poll| options[:poll] = poll end o.on("--[no-]rsync-chown", "Use rsync to modify ownership") do |chown| options[:rsync_chown] = chown end end # Parse the options and return if we don't have any target. argv = parse_options(opts) return if !argv # Build up the paths that we need to listen to. paths = {} ignores = [] with_target_vms(argv) do |machine| next if machine.state.id == :not_created cwd = machine.env.cwd.to_s if machine.provider.capability?(:proxy_machine) proxy = machine.provider.capability(:proxy_machine) if proxy machine.ui.warn(I18n.t( "vagrant.rsync_proxy_machine", name: machine.name.to_s, provider: machine.provider_name.to_s)) machine = proxy end end cached = synced_folders(machine, cached: true) fresh = synced_folders(machine) diff = synced_folders_diff(cached, fresh) if !diff[:added].empty? machine.ui.warn(I18n.t("vagrant.rsync_auto_new_folders")) end folders = cached[:rsync] next if !folders || folders.empty? # NOTE: This check is required with boot2docker since all containers # share the same virtual machine. This prevents rsync-auto from # syncing all known containers with rsync to the boot2docker vm # and only syncs the current working dirs folders. sync_folders = {} # Still sync existing synced folders from vagrantfile config_synced_folders = machine.config.vm.synced_folders.values.map { |x| x[:hostpath] } config_synced_folders.map! { |x| File.expand_path(x, machine.env.root_path) } folders.each do |id, folder_opts| if cwd != folder_opts[:hostpath] && !config_synced_folders.include?(folder_opts[:hostpath]) machine.ui.info(I18n.t("vagrant.rsync_auto_remove_folder", folder: folder_opts[:hostpath])) else if options.has_key?(:rsync_chown) folder_opts = folder_opts.merge(rsync_ownership: options[:rsync_chown]) end sync_folders[id] = folder_opts end end folders = sync_folders # Get the SSH info for this machine so we can do an initial # sync to the VM. ssh_info = machine.ssh_info if ssh_info machine.ui.info(I18n.t("vagrant.rsync_auto_initial")) folders.each do |id, folder_opts| RsyncHelper.rsync_single(machine, ssh_info, folder_opts) end end folders.each do |id, folder_opts| # If we marked this folder to not auto sync, then # don't do it. next if folder_opts.key?(:auto) && !folder_opts[:auto] hostpath = folder_opts[:hostpath] hostpath = File.expand_path(hostpath, machine.env.root_path) paths[hostpath] ||= [] paths[hostpath] << { id: id, machine: machine, opts: folder_opts, } if folder_opts[:exclude] Array(folder_opts[:exclude]).each do |pattern| ignores << RsyncHelper.exclude_to_regexp(pattern.to_s) end end # Always ignore Vagrant ignores << /.vagrant\// ignores.uniq! end end # Exit immediately if there is nothing to watch if paths.empty? @env.ui.info(I18n.t("vagrant.rsync_auto_no_paths")) return 1 end # Output to the user what paths we'll be watching paths.keys.sort.each do |path| paths[path].each do |path_opts| path_opts[:machine].ui.info(I18n.t( "vagrant.rsync_auto_path", path: path.to_s, )) end end @logger.info("Listening to paths: #{paths.keys.sort.inspect}") @logger.info("Ignoring #{ignores.length} paths:") ignores.each do |ignore| @logger.info(" -- #{ignore.to_s}") end @logger.info("Listening via: #{Listen::Adapter.select.inspect}") callback = method(:callback).to_proc.curry[paths] listopts = { ignore: ignores, force_polling: !!options[:poll] } listener = Listen.to(*paths.keys, listopts, &callback) # Create the callback that lets us know when we've been interrupted queue = Queue.new callback = lambda do # This needs to execute in another thread because Thread # synchronization can't happen in a trap context. Thread.new { queue << true } end # Run the listener in a busy block so that we can cleanly # exit once we receive an interrupt. Vagrant::Util::Busy.busy(callback) do listener.start queue.pop listener.stop if listener.state != :stopped end 0 end # This is the callback that is called when any changes happen def callback(paths, modified, added, removed) @logger.info("File change callback called!") @logger.info(" - Modified: #{modified.inspect}") @logger.info(" - Added: #{added.inspect}") @logger.info(" - Removed: #{removed.inspect}") tosync = [] paths.each do |hostpath, folders| # Find out if this path should be synced found = catch(:done) do [modified, added, removed].each do |changed| changed.each do |listenpath| throw :done, true if listenpath.start_with?(hostpath) end end # Make sure to return false if all else fails so that we # don't sync to this machine. false end # If it should be synced, store it for later tosync << folders if found end # Sync all the folders that need to be synced tosync.each do |folders| folders.each do |opts| # Reload so we get the latest ID opts[:machine].reload if !opts[:machine].id || opts[:machine].id == "" # Skip since we can't get SSH info without an ID next end ssh_info = opts[:machine].ssh_info begin start = Time.now RsyncHelper.rsync_single(opts[:machine], ssh_info, opts[:opts]) finish = Time.now @logger.info("Time spent in rsync: #{finish-start} (in seconds)") rescue Vagrant::Errors::MachineGuestNotReady # Error communicating to the machine, probably a reload or # halt is happening. Just notify the user but don't fail out. opts[:machine].ui.error(I18n.t( "vagrant.rsync_communicator_not_ready_callback")) rescue Vagrant::Errors::RSyncPostCommandError => e # Error executing rsync chown command opts[:machine].ui.error(I18n.t( "vagrant.rsync_auto_post_command_error", message: e.to_s)) rescue Vagrant::Errors::RSyncError => e # Error executing rsync, so show an error opts[:machine].ui.error(I18n.t( "vagrant.rsync_auto_rsync_error", message: e.to_s)) end end end end end end end end ================================================ FILE: plugins/synced_folders/rsync/default_unix_cap.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "shellwords" module VagrantPlugins module SyncedFolderRSync # This module provides default rsync capabilities for # unix type operating systems. module DefaultUnixCap def rsync_installed(machine) machine.communicate.test("which rsync") end def rsync_command(machine) "sudo rsync" end def rsync_pre(machine, opts) guest_path = Shellwords.escape(opts[:guestpath]) machine.communicate.sudo("mkdir -p #{guest_path}") end def rsync_post(machine, opts) if opts.key?(:chown) && !opts[:chown] return end machine.communicate.sudo(build_rsync_chown(opts)) end def build_rsync_chown(opts) guest_path = Shellwords.escape(opts[:guestpath]) if(opts[:exclude] && !Array(opts[:exclude]).empty?) exclude_base = Pathname.new(opts[:guestpath]) exclusions = Array(opts[:exclude]).map do |ex_path| ex_path = ex_path.slice(1, ex_path.size) if ex_path.start_with?(File::SEPARATOR) "-path #{Shellwords.escape(exclude_base.join(ex_path))} -prune" end.join(" -o ") + " -o " end "find #{guest_path} #{exclusions}" \ "'!' -type l -a " \ "'(' ! -user #{opts[:owner]} -or ! -group #{opts[:group]} ')' -exec " \ "chown #{opts[:owner]}:#{opts[:group]} '{}' +" end end end end ================================================ FILE: plugins/synced_folders/rsync/helper.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "ipaddr" require "shellwords" require "tmpdir" require "vagrant/util/platform" require "vagrant/util/subprocess" module VagrantPlugins module SyncedFolderRSync # This is a helper that abstracts out the functionality of rsyncing # folders so that it can be called from anywhere. class RsyncHelper # rsync version requirement to support chown argument RSYNC_CHOWN_REQUIREMENT = Gem::Requirement.new(">= 3.1.0").freeze # This converts an rsync exclude pattern to a regular expression # we can send to Listen. # # Note: Listen expects a path relative to the parameter passed into the # Listener, not a fully qualified path # # @param [String] - exclude path # @return [Regexp] - A regex of the path, modified, to exclude def self.exclude_to_regexp(exclude) start_anchor = false if exclude.start_with?("/") start_anchor = true end exclude = "#{exclude}/" if !exclude.end_with?("/") if start_anchor exclude = "^#{exclude}" if start_anchor exclude += ".*" if (!start_anchor && !exclude.end_with?("*")) # This is not an ideal solution, but it's a start. We can improve and # keep unit tests passing in the future. exclude = exclude.gsub("**", "|||GLOBAL|||") exclude = exclude.gsub("|||GLOBAL|||", ".*") Regexp.new(exclude) end def self.rsync_single(machine, ssh_info, opts) # Folder info guestpath = opts[:guestpath] hostpath = opts[:hostpath] 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 # Shellescape guestpath = Shellwords.escape(guestpath) if Vagrant::Util::Platform.windows? # rsync for Windows expects cygwin style paths, always. hostpath = Vagrant::Util::Platform.cygwin_path(hostpath) end # Make sure the host path ends with a "/" to avoid creating # a nested directory... if !hostpath.end_with?("/") hostpath += "/" end # Folder options opts[:owner] ||= ssh_info[:username] opts[:group] ||= ssh_info[:username] # set log level log_level = ssh_info[:log_level] || "FATAL" # Connection information # make it better match lib/vagrant/util/ssh.rb command_options style and logic username = ssh_info[:username] host = ssh_info[:host] proxy_command = "" if ssh_info[:proxy_command] proxy_command = "-o ProxyCommand='#{ssh_info[:proxy_command]}' " end ssh_config_file = "" if ssh_info[:config] ssh_config_file = "-F #{ssh_info[:config]}" end # Create the path for the control sockets. We used to do this # in the machine data dir but this can result in paths that are # too long for unix domain sockets. control_options = "" unless Vagrant::Util::Platform.windows? controlpath = Dir.mktmpdir("vagrant-rsync-") control_options = "-o ControlMaster=auto -o ControlPath=#{controlpath} -o ControlPersist=10m " end # rsh cmd option rsh = [ "ssh", "-p", "#{ssh_info[:port]}", "-o", "LogLevel=#{log_level}", proxy_command, ssh_config_file, control_options, ] rsh += ssh_info[:extra_args] if ssh_info[:extra_args] # Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the # IdentitiesOnly option. Also, we don't enable it if keys_only is false # so that SSH properly searches our identities and tries to do it itself. if !Vagrant::Util::Platform.solaris? && ssh_info[:keys_only] rsh += ["-o", "IdentitiesOnly=yes"] end # no strict hostkey checking unless paranoid if ssh_info[:verify_host_key] == :never || !ssh_info[:verify_host_key] rsh += [ "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"] end # If specified, attach the private key paths. if ssh_info[:private_key_path] rsh += ssh_info[:private_key_path].map { |p| "-i '#{p}'" } end # Exclude some files by default, and any that might be configured # by the user. excludes = ['.vagrant/'] excludes += Array(opts[:exclude]).map(&:to_s) if opts[:exclude] excludes.uniq! # Get the command-line arguments args = nil args = Array(opts[:args]).dup if opts[:args] args ||= ["--verbose", "--archive", "--delete", "-z", "--copy-links"] # On Windows, we have to set a default chmod flag to avoid permission issues if Vagrant::Util::Platform.windows? && !args.any? { |arg| arg.start_with?("--chmod=") } # Ensures that all non-masked bits get enabled args << "--chmod=ugo=rwX" # Remove the -p option if --archive is enabled (--archive equals -rlptgoD) # otherwise new files will not have the destination-default permissions args << "--no-perms" if args.include?("--archive") || args.include?("-a") end if opts[:rsync_ownership] && rsync_chown_support?(machine) # Allow rsync to map ownership args << "--chown=#{opts[:owner]}:#{opts[:group]}" # Notify rsync post capability not to chown opts[:chown] = false else # Disable rsync's owner/group preservation (implied by --archive) unless # specifically requested, since we adjust owner/group to match shared # folder setting ourselves. args << "--no-owner" unless args.include?("--owner") || args.include?("-o") args << "--no-group" unless args.include?("--group") || args.include?("-g") end # Tell local rsync how to invoke remote rsync with sudo rsync_path = opts[:rsync_path] if !rsync_path && machine.guest.capability?(:rsync_command) rsync_path = machine.guest.capability(:rsync_command) end if rsync_path args << "--rsync-path"<< rsync_path end # If the remote host is an IPv6 address reformat begin if IPAddr.new(host).ipv6? host = "[#{host}]" end rescue IPAddr::Error # Ignore end # Build up the actual command to execute command = [ "rsync", args, "-e", rsh.flatten.join(" "), excludes.map { |e| ["--exclude", e] }, hostpath, "#{username}@#{host}:#{guestpath}", ].flatten # The working directory should be the root path command_opts = {} command_opts[:workdir] = machine.env.root_path.to_s machine.ui.info(I18n.t( "vagrant.rsync_folder", guestpath: guestpath, hostpath: hostpath)) if excludes.length > 1 machine.ui.info(I18n.t( "vagrant.rsync_folder_excludes", excludes: excludes.inspect)) end if opts.include?(:verbose) machine.ui.info(I18n.t("vagrant.rsync_showing_output")); end # If we have tasks to do before rsyncing, do those. if machine.guest.capability?(:rsync_pre) machine.guest.capability(:rsync_pre, opts) end if opts.include?(:verbose) command_opts[:notify] = [:stdout, :stderr] r = Vagrant::Util::Subprocess.execute(*(command + [command_opts])) { |io_name,data| data.each_line { |line| machine.ui.info("rsync[#{io_name}] -> #{line}") } } else r = Vagrant::Util::Subprocess.execute(*(command + [command_opts])) end if r.exit_code != 0 raise Vagrant::Errors::RSyncError, command: command.map(&:inspect).join(" "), guestpath: guestpath, hostpath: hostpath, stderr: r.stderr end # If we have tasks to do after rsyncing, do those. if machine.guest.capability?(:rsync_post) begin machine.guest.capability(:rsync_post, opts) rescue Vagrant::Errors::VagrantError => err raise Vagrant::Errors::RSyncPostCommandError, guestpath: guestpath, hostpath: hostpath, message: err.to_s end end ensure FileUtils.remove_entry_secure(controlpath, true) if controlpath end # Check if rsync versions support using chown option # # @param [Vagrant::Machine] machine The remote machine # @return [Boolean] def self.rsync_chown_support?(machine) if !RSYNC_CHOWN_REQUIREMENT.satisfied_by?(Gem::Version.new(local_rsync_version)) return false end mrv = machine_rsync_version(machine) if mrv && !RSYNC_CHOWN_REQUIREMENT.satisfied_by?(Gem::Version.new(mrv)) return false end true end # @return [String, nil] version of remote rsync def self.machine_rsync_version(machine) if machine.guest.capability?(:rsync_command) rsync_path = machine.guest.capability(:rsync_command) else rsync_path = "rsync" end output = "" machine.communicate.execute(rsync_path + " --version") { |_, data| output << data } vmatch = output.match(/version\s+(?[\d.]+)\s/) if vmatch vmatch[:version] end end # @return [String, nil] version of local rsync def self.local_rsync_version if !@_rsync_version r = Vagrant::Util::Subprocess.execute("rsync", "--version") vmatch = r.stdout.to_s.match(/version\s+(?[\d.]+)\s/) if vmatch @_rsync_version = vmatch[:version] end end @_rsync_version end # @private # Reset the cached values for helper. This is not considered a public # API and should only be used for testing. def self.reset! instance_variables.each(&method(:remove_instance_variable)) end end end end ================================================ FILE: plugins/synced_folders/rsync/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module SyncedFolderRSync # This plugin implements synced folders via rsync. class Plugin < Vagrant.plugin("2") name "RSync synced folders" description <<-EOF The Rsync synced folder plugin will sync folders via rsync. EOF command("rsync", primary: false) do require_relative "command/rsync" Command::Rsync end command("rsync-auto", primary: false) do require_relative "command/rsync_auto" Command::RsyncAuto end synced_folder("rsync", 5) do require_relative "synced_folder" SyncedFolder end end end end ================================================ FILE: plugins/synced_folders/rsync/synced_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "log4r" require "vagrant/util/subprocess" require "vagrant/util/which" require_relative "helper" module VagrantPlugins module SyncedFolderRSync class SyncedFolder < Vagrant.plugin("2", :synced_folder) include Vagrant::Util def initialize(*args) super @logger = Log4r::Logger.new("vagrant::synced_folders::rsync") end def usable?(machine, raise_error=false) rsync_path = Which.which("rsync") return true if rsync_path return false if !raise_error raise Vagrant::Errors::RSyncNotFound end def prepare(machine, folders, opts) # Nothing is necessary to do before VM boot. end def enable(machine, folders, opts) if machine.guest.capability?(:rsync_installed) installed = machine.guest.capability(:rsync_installed) if !installed can_install = machine.guest.capability?(:rsync_install) raise Vagrant::Errors::RSyncNotInstalledInGuest if !can_install machine.ui.info I18n.t("vagrant.rsync_installing") machine.guest.capability(:rsync_install) end end ssh_info = machine.ssh_info if ssh_info[:private_key_path].empty? && ssh_info[:password] machine.ui.warn(I18n.t("vagrant.rsync_ssh_password")) end folders.each do |id, folder_opts| RsyncHelper.rsync_single(machine, ssh_info, folder_opts) end end # Enable rsync synced folders within WSL when in use # on non-DrvFs file systems def self.wsl_allow_non_drvfs? true end end end end ================================================ FILE: plugins/synced_folders/smb/cap/default_fstab_modification.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module SyncedFolderSMB module Cap module DefaultFstabModification def self.default_fstab_modification(machine) return false end end end end end ================================================ FILE: plugins/synced_folders/smb/cap/mount_options.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../unix_mount_helpers" module VagrantPlugins module SyncedFolderSMB module Cap module MountOptions extend VagrantPlugins::SyncedFolder::UnixMountHelpers MOUNT_TYPE = "cifs".freeze # Returns mount options for a smb synced folder # # @param [Machine] machine # @param [String] name of mount # @param [String] path of mount on guest # @param [Hash] hash of mount options def self.mount_options(machine, name, guest_path, options) mount_options = options.fetch(:mount_options, []) options[:smb_id] ||= name detected_ids = detect_owner_group_ids(machine, guest_path, mount_options, options) mount_uid = detected_ids[:uid] mount_gid = detected_ids[:gid] mnt_opts = [] if machine.env.host.capability?(:smb_mount_options) mnt_opts += machine.env.host.capability(:smb_mount_options) else mnt_opts << "sec=ntlmssp" end mnt_opts << "credentials=/etc/smb_creds_#{options[:smb_id]}" mnt_opts << "uid=#{mount_uid}" mnt_opts << "gid=#{mount_gid}" if !ENV['VAGRANT_DISABLE_SMBMFSYMLINKS'] mnt_opts << "mfsymlinks" end mnt_opts << "_netdev" mnt_opts = merge_mount_options(mnt_opts, options[:mount_options] || []) mount_options = mnt_opts.join(",") return mount_options, mount_uid, mount_gid end def self.mount_type(machine) return MOUNT_TYPE end def self.mount_name(machine, name, data) candidate_ips = machine.env.host.capability(:configured_ip_addresses) data[:smb_host] ||= machine.guest.capability( :choose_addressable_ip_addr, candidate_ips) "//#{data[:smb_host]}/#{data[:smb_id]}" end end end end end ================================================ FILE: plugins/synced_folders/smb/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module SyncedFolderSMB class Config < Vagrant.plugin("2", :config) attr_accessor :functional def initialize super @functional = UNSET_VALUE end def finalize! @functional = true if @functional == UNSET_VALUE end def to_s "SMB" end end end end ================================================ FILE: plugins/synced_folders/smb/errors.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module SyncedFolderSMB module Errors # A convenient superclass for all our errors. class SMBError < Vagrant::Errors::VagrantError error_namespace("vagrant_sf_smb.errors") end class SMBNotSupported < SMBError error_key(:not_supported) end class SMBStartFailed < SMBError error_key(:start_failed) end class SMBCredentialsMissing < SMBError error_key(:credentials_missing) end class SMBListFailed < SMBError error_key(:list_failed) end class SMBNameError < SMBError error_key(:name_error) end class CredentialsRequestError < SMBError error_key(:credentials_request_error) end class DefineShareFailed < SMBError error_key(:define_share_failed) end class PruneShareFailed < SMBError error_key(:prune_share_failed) end class NoHostIPAddr < SMBError error_key(:no_routable_host_addr) end class PowershellError < SMBError error_key(:powershell_error) end class PowershellVersion < SMBError error_key(:powershell_version) end class WindowsHostRequired < SMBError error_key(:windows_host_required) end class WindowsAdminRequired < SMBError error_key(:windows_admin_required) end end end end ================================================ FILE: plugins/synced_folders/smb/plugin.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" module VagrantPlugins module SyncedFolderSMB autoload :Errors, File.expand_path("../errors", __FILE__) # This plugin implements SMB synced folders. class Plugin < Vagrant.plugin("2") name "SMB synced folders" description <<-EOF The SMB synced folders plugin enables you to use SMB folders on Windows or macOS and share them to guest machines. EOF config("smb") do require_relative "config" Config end synced_folder("smb", 7) do require_relative "synced_folder" init! SyncedFolder end synced_folder_capability("smb", "default_fstab_modification") do require_relative "cap/default_fstab_modification" Cap::DefaultFstabModification end synced_folder_capability("smb", "mount_options") do require_relative "cap/mount_options" Cap::MountOptions end synced_folder_capability("smb", "mount_name") do require_relative "cap/mount_options" Cap::MountOptions end synced_folder_capability("smb", "mount_type") do require_relative "cap/mount_options" Cap::MountOptions end protected def self.init! return if defined?(@_init) I18n.load_path << File.expand_path( "templates/locales/synced_folder_smb.yml", Vagrant.source_root) I18n.reload! @_init = true end end end end ================================================ FILE: plugins/synced_folders/smb/synced_folder.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/md5" require "json" require "log4r" require "vagrant/util/platform" require "vagrant/util/powershell" require_relative "errors" module VagrantPlugins module SyncedFolderSMB class SyncedFolder < Vagrant.plugin("2", :synced_folder) # Maximum number of times to retry requesting username/password CREDENTIAL_RETRY_MAX = 5 def initialize(*args) super @logger = Log4r::Logger.new("vagrant::synced_folders::smb") end def usable?(machine, raise_error=false) # If the machine explicitly states SMB is not supported, then # believe it return false if !machine.config.smb.functional return true if machine.env.host.capability?(:smb_installed) && machine.env.host.capability(:smb_installed) return false if !raise_error raise Errors::SMBNotSupported end def prepare(machine, folders, opts) machine.ui.output(I18n.t("vagrant_sf_smb.preparing")) smb_username = smb_password = nil # If we need auth information, then ask the user. have_auth = false folders.each do |id, data| smb_username = data[:smb_username] if data[:smb_username] smb_password = data[:smb_password] if data[:smb_password] if smb_username && smb_password have_auth = true break end end modify_username = false if !have_auth machine.ui.detail(I18n.t("vagrant_sf_smb.warning_password") + "\n ") retries = 0 while retries < CREDENTIAL_RETRY_MAX do if smb_username username = machine.ui.ask("Username (#{smb_username}): ") smb_username = username if username != "" modify_username = true else smb_username = machine.ui.ask("Username (user[@domain]): ") end smb_password = machine.ui.ask("Password (will be hidden): ", echo: false) auth_success = true if machine.env.host.capability?(:smb_validate_password) Vagrant::Util::CredentialScrubber.sensitive(smb_password) auth_success = machine.env.host.capability(:smb_validate_password, machine, smb_username, smb_password) end break if auth_success machine.ui.output(I18n.t("vagrant_sf_smb.incorrect_credentials") + "\n ") retries += 1 end if retries >= CREDENTIAL_RETRY_MAX raise Errors::CredentialsRequestError end end # Check if this host can start and SMB service if machine.env.host.capability?(:smb_start) machine.env.host.capability(:smb_start) end folders.each do |id, data| if modify_username # Only override original username if user requests to data[:smb_username] = smb_username else data[:smb_username] ||= smb_username end data[:smb_password] ||= smb_password # Register password as sensitive Vagrant::Util::CredentialScrubber.sensitive(data[:smb_password]) end machine.env.host.capability(:smb_prepare, machine, folders, opts) end def enable(machine, folders, opts) machine.ui.output(I18n.t("vagrant_sf_smb.mounting")) # Make sure that this machine knows this dance if !machine.guest.capability?(:mount_smb_shared_folder) raise Vagrant::Errors::GuestCapabilityNotFound, cap: "mount_smb_shared_folder", guest: machine.guest.name.to_s end # Setup if we have it if machine.guest.capability?(:smb_install) machine.guest.capability(:smb_install) end # Detect the host IP for this guest if one wasn't specified # for every folder. host_ip = nil need_host_ip = false folders.each do |id, data| if !data[:smb_host] need_host_ip = true break end end if need_host_ip candidate_ips = machine.env.host.capability(:configured_ip_addresses) @logger.debug("Potential host IPs: #{candidate_ips.inspect}") host_ip = machine.guest.capability( :choose_addressable_ip_addr, candidate_ips) if !host_ip raise Errors::NoHostIPAddr end end # This is used for defaulting the owner/group ssh_info = machine.ssh_info folders.each do |id, data| data[:smb_host] ||= host_ip # Default the owner/group of the folder to the SSH user data[:owner] ||= ssh_info[:username] data[:group] ||= ssh_info[:username] machine.ui.detail(I18n.t( "vagrant_sf_smb.mounting_single", host: data[:hostpath].to_s, guest: data[:guestpath].to_s)) machine.guest.capability( :mount_smb_shared_folder, data[:smb_id], data[:guestpath], data) clean_folder_configuration(data) end end # Nothing to do here but ensure folder options are scrubbed def disable(machine, folders, opts) folders.each do |_, data| clean_folder_configuration(data) end end def cleanup(machine, opts) if machine.env.host.capability?(:smb_cleanup) machine.env.host.capability(:smb_cleanup, machine, opts) end end protected # Remove data that should not be persisted within folder # specific configuration # # @param [Hash] data Folder configuration def clean_folder_configuration(data) return if !data.is_a?(Hash) data.delete(:smb_password) nil end end end end ================================================ FILE: plugins/synced_folders/unix_mount_helpers.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "shellwords" require "vagrant/util/retryable" module VagrantPlugins module SyncedFolder module UnixMountHelpers def self.extended(klass) if !klass.class_variable_defined?(:@@logger) klass.class_variable_set(:@@logger, Log4r::Logger.new(klass.name.downcase)) end klass.extend Vagrant::Util::Retryable end def detect_owner_group_ids(machine, guest_path, mount_options, options) mount_uid = find_mount_options_id("uid", mount_options) mount_gid = find_mount_options_id("gid", mount_options) if mount_uid.nil? if options[:owner].to_i.to_s == options[:owner].to_s mount_uid = options[:owner] self.class_variable_get(:@@logger).debug("Owner user ID (provided): #{mount_uid}") else output = {stdout: '', stderr: ''} uid_command = "id -u #{options[:owner]}" machine.communicate.execute(uid_command, error_class: Vagrant::Errors::VirtualBoxMountFailed, error_key: :virtualbox_mount_failed, command: uid_command, output: output[:stderr] ) { |type, data| output[type] << data if output[type] } mount_uid = output[:stdout].chomp self.class_variable_get(:@@logger).debug("Owner user ID (lookup): #{options[:owner]} -> #{mount_uid}") end else machine.ui.warn "Detected mount owner ID within mount options. (uid: #{mount_uid} guestpath: #{guest_path})" end if mount_gid.nil? if options[:group].to_i.to_s == options[:group].to_s mount_gid = options[:group] self.class_variable_get(:@@logger).debug("Owner group ID (provided): #{mount_gid}") else begin output = {stdout: '', stderr: ''} gid_command = "getent group #{options[:group]}" machine.communicate.execute(gid_command, error_class: Vagrant::Errors::VirtualBoxMountFailed, error_key: :virtualbox_mount_failed, command: gid_command, output: output[:stderr] ) { |type, data| output[type] << data if output[type] } mount_gid = output[:stdout].split(':').at(2).to_s.chomp self.class_variable_get(:@@logger).debug("Owner group ID (lookup): #{options[:group]} -> #{mount_gid}") rescue Vagrant::Errors::VirtualBoxMountFailed if options[:owner] == options[:group] self.class_variable_get(:@@logger).debug("Failed to locate group `#{options[:group]}`. Group name matches owner. Fetching effective group ID.") output = {stdout: ''} result = machine.communicate.execute("id -g #{options[:owner]}", error_check: false ) { |type, data| output[type] << data if output[type] } mount_gid = output[:stdout].chomp if result == 0 self.class_variable_get(:@@logger).debug("Owner group ID (effective): #{mount_gid}") end raise unless mount_gid end end else machine.ui.warn "Detected mount group ID within mount options. (gid: #{mount_gid} guestpath: #{guest_path})" end {:gid => mount_gid, :uid => mount_uid} end def find_mount_options_id(id_name, mount_options) id_line = mount_options.detect{|line| line.include?("#{id_name}=")} if id_line match = id_line.match(/,?#{Regexp.escape(id_name)}=(?\d+),?/) found_id = match["option_id"] updated_id_line = [ match.pre_match, match.post_match ].find_all{|string| !string.empty?}.join(',') if updated_id_line.empty? mount_options.delete(id_line) else idx = mount_options.index(id_line) mount_options.delete(idx) mount_options.insert(idx, updated_id_line) end end found_id end def emit_upstart_notification(machine, guest_path) # Emit an upstart event if we can machine.communicate.sudo <<-EOH.gsub(/^ {12}/, "") if test -x /sbin/initctl && command -v /sbin/init && /sbin/init 2>/dev/null --version | grep upstart; then /sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT=#{guest_path} fi EOH end def merge_mount_options(base, overrides) base = base.join(",").split(",") overrides = overrides.join(",").split(",") b_kv = Hash[base.map{|item| item.split("=", 2) }] o_kv = Hash[overrides.map{|item| item.split("=", 2) }] merged = {}.tap do |opts| (b_kv.keys + o_kv.keys).uniq.each do |key| opts[key] = o_kv.fetch(key, b_kv[key]) end end merged.map do |key, value| [key, value].compact.join("=") end end end end end ================================================ FILE: scripts/install_rvm ================================================ #!/usr/bin/env bash export DEBIAN_FRONTEND=noninteractive if ! rvm --version then # https://github.com/rvm/ubuntu_rvm#install apt-add-repository -y ppa:rael-gc/rvm && apt-get update -y && apt-get install -y rvm || { echo 'Failed to install rvm' >&2 exit 1 } fi usermod -a -G rvm vagrant || { echo 'Failed to add vagrant to the rvm group' >&2 exit 1 } ================================================ FILE: scripts/setup_tests ================================================ #!/usr/bin/env bash rvm --version || { echo 'rvm not installed' >&2 exit 1 } # bsdtar is required for a small subset of the tests if ! bsdtar --version then apt-get install -y bsdtar && bsdtar --version || { echo 'Failed to install bsdtar' >&2 exit 1 } fi # Install next-to-last Ruby that complies with Vagrant's version # constraint RUBY_VER_REQ=$( awk ' $1 == "s.required_ruby_version" { print $4 } ' /vagrant/vagrant.gemspec | tr -d '"') RUBY_VER=$(sudo -u vagrant -i rvm list known | tr '[]-' ' ' | awk "/^ ruby ${RUBY_VER_REQ:0:1}\./ { print \$2 }" | sort -r | sed -n '2p') sudo -u vagrant -i rvm install "${RUBY_VER}" sudo -u vagrant -i rvm --default use "${RUBY_VER}" # Output the Ruby version (for sanity) sudo -u vagrant -i ruby --version # Upgrade Rubygems sudo -u vagrant -i rvm "${RUBY_VER}" do gem update --system # Prepare to run unit tests sudo -u vagrant -i bash -c 'cd /vagrant; bundle install' # Automatically move into the shared folder, but only add the command if # it's not already there. if ! grep -q 'cd /vagrant' /home/vagrant/.bash_profile then cat <> /home/vagrant/.bash_profile cd /vagrant EOF fi ================================================ FILE: scripts/sign.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 set -e # Get the parent directory of where this script is. SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" # Change into that dir because we expect that cd $DIR # Get the version from the command line VERSION=$1 if [ -z $VERSION ]; then echo "Please specify a version." exit 1 fi # 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 ================================================ FILE: scripts/website_push_www.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # Set the tmpdir if [ -z "$TMPDIR" ]; then TMPDIR="/tmp" fi # Create a temporary build dir and make sure we clean it up. For # debugging, comment out the trap line. DEPLOY=`mktemp -d /tmp/vagrant-www-XXXXXX` trap "rm -rf $DEPLOY" INT TERM EXIT # Get the parent directory of where this script is. SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" # Copy into tmpdir shopt -s dotglob cp -R $DIR/website/www/* $DEPLOY/ # Change into that directory cd $DEPLOY # Ignore some stuff touch .gitignore echo ".sass-cache" >> .gitignore echo "build" >> .gitignore echo "vendor" >> .gitignore # Add everything git init . git add . git commit -q -m "Deploy by $USER" git remote add heroku git@heroku.com:vagrantup-www-2.git git push -f heroku main # Cleanup the deploy rm -rf $DEPLOY ================================================ FILE: tasks/acceptance.rake ================================================ namespace :acceptance do desc "shows components that can be tested" task :components do exec("vagrant-spec components --config=vagrant-spec.config.rb") end desc "runs acceptance tests" task :run do args = [ "--config=vagrant-spec.config.rb", ] if ENV["COMPONENTS"] args << "--components=\"#{ENV["COMPONENTS"]}\"" end command = "vagrant-spec test #{args.join(" ")}" puts command puts exec(command) end end ================================================ FILE: tasks/bundler.rake ================================================ # This installs the tasks that help with gem creation and # publishing. Bundler::GemHelper.install_tasks ================================================ FILE: tasks/test.rake ================================================ require 'rake/testtask' require 'rspec/core/rake_task' namespace :test do RSpec::Core::RakeTask.new(:unit) do |t| t.pattern = "test/unit/**/*_test.rb" end end ================================================ FILE: templates/commands/init/Vagrantfile.erb ================================================ # -*- mode: ruby -*- # vi: set ft=ruby : # All Vagrant configuration is done below. The "2" in Vagrant.configure # configures the configuration version (we support older styles for # backwards compatibility). Please don't change it unless you know what # you're doing. Vagrant.configure("2") do |config| # The most common configuration options are documented and commented below. # For a complete reference, please see the online documentation at # https://docs.vagrantup.com. # Every Vagrant development environment requires a box. You can search for # boxes at https://vagrantcloud.com/search. config.vm.box = "<%= box_name %>" <% if box_version -%> config.vm.box_version = "<%= box_version %>" <% end -%> <% if box_url -%> # The url from where the 'config.vm.box' box will be fetched if it # doesn't already exist on the user's system. config.vm.box_url = "<%= box_url %>" <% else -%> # Disable automatic box update checking. If you disable this, then # boxes will only be checked for updates when the user runs # `vagrant box outdated`. This is not recommended. # config.vm.box_check_update = false <% end -%> # Create a forwarded port mapping which allows access to a specific port # within the machine from a port on the host machine. In the example below, # accessing "localhost:8080" will access port 80 on the guest machine. # NOTE: This will enable public access to the opened port # config.vm.network "forwarded_port", guest: 80, host: 8080 # Create a forwarded port mapping which allows access to a specific port # within the machine from a port on the host machine and only allow access # via 127.0.0.1 to disable public access # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1" # Create a private network, which allows host-only access to the machine # using a specific IP. # config.vm.network "private_network", ip: "192.168.33.10" # Create a public network, which generally matched to bridged network. # Bridged networks make the machine appear as another physical device on # your network. # config.vm.network "public_network" # Share an additional folder to the guest VM. The first argument is # the path on the host to the actual folder. The second argument is # the path on the guest to mount the folder. And the optional third # argument is a set of non-required options. # config.vm.synced_folder "../data", "/vagrant_data" # Disable the default share of the current code directory. Doing this # provides improved isolation between the vagrant box and your host # by making sure your Vagrantfile isn't accessible to the vagrant box. # If you use this you may want to enable additional shared subfolders as # shown above. # config.vm.synced_folder ".", "/vagrant", disabled: true # Provider-specific configuration so you can fine-tune various # backing providers for Vagrant. These expose provider-specific options. # Example for VirtualBox: # # config.vm.provider "virtualbox" do |vb| # # Display the VirtualBox GUI when booting the machine # vb.gui = true # # # Customize the amount of memory on the VM: # vb.memory = "1024" # end # # View the documentation for the provider you are using for more # information on available options. # Enable provisioning with a shell script. Additional provisioners such as # Ansible, Chef, Docker, Puppet and Salt are also available. Please see the # documentation for more information about their specific syntax and use. # config.vm.provision "shell", inline: <<-SHELL # apt-get update # apt-get install -y apache2 # SHELL end ================================================ FILE: templates/commands/init/Vagrantfile.min.erb ================================================ # -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure("2") do |config| config.vm.box = "<%= box_name %>" <% if box_version -%> config.vm.box_version = "<%= box_version %>" <% end -%> <% if box_url -%> config.vm.box_url = "<%= box_url %>" <% end -%> end ================================================ FILE: templates/commands/ssh_config/config.erb ================================================ Host <%= host_key %> <% if config -%> Include <%= config %> <% end -%> HostName <%= ssh_host %> User <%= ssh_user %> Port <%= ssh_port %> <% if ! verify_host_key || verify_host_key == :never -%> UserKnownHostsFile /dev/null StrictHostKeyChecking no <% end -%> PasswordAuthentication no <% if private_key_path -%> <% private_key_path.each do |path| %> <% if path.include?(" ") -%> IdentityFile "<%= path %>" <% else -%> IdentityFile <%= path %> <% end -%> <% end -%> <% end -%> <% if keys_only -%> IdentitiesOnly yes <% end -%> <% if log_level -%> LogLevel <%= log_level %> <% else -%> LogLevel FATAL <% end -%> <% if forward_agent -%> ForwardAgent yes <% end -%> <% if forward_x11 -%> ForwardX11 yes <% end -%> <% if proxy_command -%> ProxyCommand <%= proxy_command %> <% end -%> <% if !disable_deprecated_algorithms -%> PubkeyAcceptedKeyTypes +ssh-rsa HostKeyAlgorithms +ssh-rsa <% end -%> ================================================ FILE: templates/commands/winrm_config/config.erb ================================================ Host <%= host_key %> HostName <%= winrm_host %> User <%= winrm_user %> Password <%= winrm_password %> Port <%= winrm_port %> <% if rdp_port -%> RDPHostName <%= rdp_host %> RDPPort <%= rdp_port %> RDPUser <%= rdp_user %> RDPPassword <%= rdp_pass %> <% end -%> ================================================ FILE: templates/config/messages.erb ================================================ <% if !warnings.empty? -%> Warnings: <% warnings.each do |warning| -%> * <%= warning %> <% end -%> <% end -%> <% if !errors.empty? -%> Errors: <% errors.each do |error| -%> * <%= error %> <% end -%> <% end -%> ================================================ FILE: templates/config/validation_failed.erb ================================================ <% errors.each do |section, list| -%> <%= section %>: <% list.each do |error| -%> * <%= error %> <% end -%> <% end -%> ================================================ FILE: templates/guests/alpine/network_dhcp.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. auto eth<%= options[:interface] %> iface eth<%= options[:interface] %> inet dhcp <% if !options[:use_dhcp_assigned_default_route] %> post-up route del default dev $IFACE || true <% else %> # We need to disable eth0, see GH-2648 post-up route del default dev eth0 pre-down route add default dev eth0 <% end %> #VAGRANT-END ================================================ FILE: templates/guests/alpine/network_static.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. auto eth<%= options[:interface] %> iface eth<%= options[:interface] %> inet static address <%= options[:ip] %> netmask <%= options[:netmask] %> #VAGRANT-END ================================================ FILE: templates/guests/alt/network_dhcp.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. TYPE=eth NM_CONTROLLED=<%= options.fetch(:nm_controlled, "no") %> BOOTPROTO=dhcp ONBOOT=yes #VAGRANT-END ================================================ FILE: templates/guests/alt/network_ipv4address.erb ================================================ #VAGRANT-BEGIN <%= options[:ip] %>/<%= options[:netmask] %> #VAGRANT-END ================================================ FILE: templates/guests/alt/network_ipv4route.erb ================================================ #VAGRANT-BEGIN <% if options[:gateway] %> default via <%= options[:gateway] %> <% end %> #VAGRANT-END ================================================ FILE: templates/guests/alt/network_static.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. TYPE=eth NM_CONTROLLED=<%= options.fetch(:nm_controlled, "no") %> BOOTPROTO=static ONBOOT=yes #VAGRANT-END ================================================ FILE: templates/guests/arch/default_network/network_dhcp.erb ================================================ Description='A basic dhcp ethernet connection' Interface=<%= options[:device] %> Connection=ethernet IP=dhcp ================================================ FILE: templates/guests/arch/default_network/network_static.erb ================================================ Connection=ethernet Description='A basic static ethernet connection' Interface=<%= options[:device] %> IP=static Address=('<%= options[:ip]%>/<%= options[:netmask] %>') <% if options[:gateway] -%> Gateway='<%= options[:gateway] %>' <% end -%> ================================================ FILE: templates/guests/arch/default_network/network_static6.erb ================================================ Connection=ethernet Description='A basic IPv6 ethernet connection' Interface=<%= options[:device] %> IP6=static Address6=('<%= options[:ip]%>/<%= options[:netmask] %>') <% if options[:gateway] -%> Gateway6='<%= options[:gateway] %>' <% end -%> ================================================ FILE: templates/guests/arch/systemd_networkd/network_dhcp.erb ================================================ [Match] Name=<%= options[:device] %> [Network] Description=A basic DHCP ethernet connection DHCP=ipv4 ================================================ FILE: templates/guests/arch/systemd_networkd/network_static.erb ================================================ [Match] Name=<%= options[:device] %> [Network] Description=A basic static ethernet connection Address=<%= options[:ip]%>/<%= options[:netmask] %> <% if options[:gateway] -%> Gateway=<%= options[:gateway] %> <% end -%> ================================================ FILE: templates/guests/arch/systemd_networkd/network_static6.erb ================================================ [Match] Name=<%= options[:device] %> [Network] Description=A basic static ethernet connection Address=<%= options[:ip]%>/<%= options[:netmask] %> <% if options[:gateway] -%> Gateway=<%= options[:gateway] %> <% end -%> ================================================ FILE: templates/guests/coreos/etcd.service.erb ================================================ [Unit] Description=Clustered etcd #After=docker.service [Service] Restart=always ExecStart=/usr/bin/etcd -c 4001 -s 7001 -h <%= options[:my_ip] %> <% if options[:connection_string] %>-C <%= options[:connection_string] %><% end %> -d /home/core/etcd [Install] WantedBy=local.target ================================================ FILE: templates/guests/debian/network_dhcp.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. auto <%= options[:device] %> iface <%= options[:device] %> inet dhcp <% if !options[:use_dhcp_assigned_default_route] %> post-up ip route del default dev $IFACE || true <% else %> # We need to disable eth0, see GH-2648 post-up ip route del default dev <%= options[:root_device] %> || true post-up dhclient $IFACE pre-down ip route add default dev <%= options[:root_device] %> <% end %> #VAGRANT-END ================================================ FILE: templates/guests/debian/network_static.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. auto <%= options[:device] %> iface <%= options[:device] %> inet static address <%= options[:ip] %> netmask <%= options[:netmask] %> <% if options[:gateway] %> gateway <%= options[:gateway] %> <% end %> #VAGRANT-END ================================================ FILE: templates/guests/debian/network_static6.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. auto <%= options[:device] %> iface <%= options[:device] %> inet6 static address <%= options[:ip] %> netmask <%= options[:netmask] %> <% if options[:gateway] %> gateway <%= options[:gateway] %> <% end %> #VAGRANT-END ================================================ FILE: templates/guests/freebsd/network_dhcp.erb ================================================ #VAGRANT-BEGIN ifconfig_<%= options[:device] %>="DHCP" synchronous_dhclient="YES" #VAGRANT-END ================================================ FILE: templates/guests/freebsd/network_static.erb ================================================ #VAGRANT-BEGIN ifconfig_<%= options[:device] %>="inet <%= options[:ip] %> netmask <%= options[:netmask] %>" <% if options[:gateway] %> defaultrouter="<%= options[:gateway] %>" <% end %> #VAGRANT-END ================================================ FILE: templates/guests/freebsd/network_static6.erb ================================================ #VAGRANT-BEGIN ifconfig_<%= options[:device] %>_ipv6="inet6 <%= options[:ip] %> prefixlen <%= options[:netmask] %>" <% if options[:gateway] %> ipv6_defaultrouter="<%= options[:gateway] %>" <% end %> #VAGRANT-END ================================================ FILE: templates/guests/funtoo/network_dhcp.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='dhcp' #VAGRANT-END ================================================ FILE: templates/guests/funtoo/network_static.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='interface' ipaddr='<%= options[:ip] %>/<%= options[:netmask] %>' <% [:gateway, :nameservers, :domain, :route, :gateway6, :route6, :mtu].each do |key| %> <% if options[key] %> <%= key %>='<%= options[key] %>' <% end %> <% end %> #VAGRANT-END ================================================ FILE: templates/guests/funtoo/network_static6.erb ================================================ #VAGRANT-BEGIN template='interface' ipaddr='<%= options[:ip] %>/<%= options[:netmask] %>' <% [:gateway, :nameservers, :domain, :route, :gateway6, :route6, :mtu].each do |key| %> <% if options[key] %> <%= key %>='<%= options[key] %>' <% end %> <% end %> #VAGRANT-END ================================================ FILE: templates/guests/gentoo/network_dhcp.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. config_<%= options[:device] %>="dhcp" #VAGRANT-END ================================================ FILE: templates/guests/gentoo/network_static.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. config_<%= options[:device] %>=("<%= options[:ip] %> netmask <%= options[:netmask] %>") modules_<%= options[:device] %>=("!plug") <% if options[:gateway] -%> gateways_<%= options[:device] %>="<%= options[:gateway] %>" <% end -%> #VAGRANT-END ================================================ FILE: templates/guests/gentoo/network_static6.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. config_<%= options[:device] %>="<%= options[:ip] %>/<%= options[:netmask] %>" modules_<%= options[:device] %>="!plug" <% if options[:gateway] -%> gateways_<%= options[:device] %>="<%= options[:gateway] %>" <% end -%> #VAGRANT-END ================================================ FILE: templates/guests/gentoo/network_systemd.erb ================================================ [Match] Name=<%= networks[0][:device] %> [Network] <%- gateway = nil %> <%- networks.each do |net| %> <%- if net[:type] == 'dhcp' %> DHCP=yes <%- elsif net[:ip] %> Address=<%= net[:ip] %>/<%= net[:netmask] %> <%- end %> <%- gateway ||= net[:gateway] %> <%- end %> <%- if gateway %> Gateway=<%= gateway %> <%- end %> ================================================ FILE: templates/guests/linux/etc_fstab.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. <% folders.each do |opts| %> <%= opts[:name] %> <%= opts[:mount_point] %> <%= opts[:mount_type] %> <%= opts[:mount_options] || 'default' %> <%= opts[:dump] || 0 %> <%= opts[:fsck] || 0 %> <%end%> #VAGRANT-END ================================================ FILE: templates/guests/netbsd/network_dhcp.erb ================================================ #VAGRANT-BEGIN ifconfig_wm<%= options[:interface] %>=dhcp #VAGRANT-END ================================================ FILE: templates/guests/netbsd/network_static.erb ================================================ #VAGRANT-BEGIN ifconfig_wm<%= options[:interface] %>="media autoselect up;inet <%= options[:ip] %> netmask <%= options[:netmask] %>" #VAGRANT-END ================================================ FILE: templates/guests/nixos/hostname.erb ================================================ { config, pkgs, ... }: { <% if name %> networking.hostName = "<%= name %>"; <% end %> } ================================================ FILE: templates/guests/nixos/network.erb ================================================ { config, pkgs, ... }: { networking.interfaces = { <% networks.select {|n| n[:device]}.each do |network| %> <%= network[:device] %>.ipv4.addresses = [{ <% if network[:type] == :static %> address = "<%= network[:ip] %>"; <% end %> <% if network[:prefix_length] %> prefixLength = <%= network[:prefix_length] %>; <% end %> }]; <% end %> }; } ================================================ FILE: templates/guests/openbsd/network_dhcp.erb ================================================ dhcp ================================================ FILE: templates/guests/openbsd/network_static.erb ================================================ inet <%= options[:ip] %> <%= options[:netmask] %> NONE ================================================ FILE: templates/guests/openbsd/network_static6.erb ================================================ inet6 <%= options[:ip] %> <%= options[:netmask] %> NONE ================================================ FILE: templates/guests/redhat/network_dhcp.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. BOOTPROTO=dhcp ONBOOT=yes DEVICE=<%= options[:device] %> NM_CONTROLLED=<%= options.fetch(:nm_controlled, "no") %> #VAGRANT-END ================================================ FILE: templates/guests/redhat/network_static.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. NM_CONTROLLED=<%= options.fetch(:nm_controlled, "no") %> BOOTPROTO=none ONBOOT=yes IPADDR=<%= options[:ip] %> NETMASK=<%= options[:netmask] %> DEVICE=<%= options[:device] %> <% if options[:gateway] %> GATEWAY=<%= options[:gateway] %> <% end %> <% if options[:mac_address] %> HWADDR=<%= options[:mac_address] %> <% end %> PEERDNS=no #VAGRANT-END ================================================ FILE: templates/guests/redhat/network_static6.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. NM_CONTROLLED=<%= options.fetch(:nm_controlled, "no") %> BOOTPROTO=static ONBOOT=yes DEVICE=<%= options[:device] %> IPV6INIT=yes IPV6ADDR=<%= options[:ip] %>/<%= options[:netmask] %> <% if options[:gateway] -%> IPV6_DEFAULTGW=<%= options[:gateway] %> <% end %> #VAGRANT-END ================================================ FILE: templates/guests/slackware/network_dhcp.erb ================================================ #VAGRANT-BEGIN # Config for eth<%= i %> USE_DHCP[<%= i %>]="yes" DHCP_HOSTNAME[<%= i %>]="" <% if options[:gateway] -%> GATEWAY="<%= options[:gateway] %>" <% end -%> DEBUG_ETH_UP="no" #VAGRANT-END ================================================ FILE: templates/guests/slackware/network_static.erb ================================================ #VAGRANT-BEGIN # Config for eth<%= i %> IPADDR[<%= i %>]="<%= options[:ip] %>" NETMASK[<%= i %>]="<%= options[:ip] %>" USE_DHCP[<%= i %>]="" DHCP_HOSTNAME[<%= i %>]="" <% if options[:gateway] -%> GATEWAY="<%= options[:gateway] %>" <% end -%> DEBUG_ETH_UP="no" #VAGRANT-END ================================================ FILE: templates/guests/suse/network_dhcp.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. BOOTPROTO='dhcp' STARTMODE='auto' DEVICE='<%= options[:device] %>' #VAGRANT-END ================================================ FILE: templates/guests/suse/network_static.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. BOOTPROTO='static' IPADDR='<%= options[:ip] %>' NETMASK='<%= options[:netmask] %>' DEVICE='<%= options[:device] %>' <% if options[:gateway] -%> GATEWAY='<%= options[:gateway] %>' <% end -%> PEERDNS='no' STARTMODE='auto' USERCONTROL='no' #VAGRANT-END ================================================ FILE: templates/guests/suse/network_static6.erb ================================================ #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. STARTMODE='auto' BOOTPROTO='static' IPADDR=<%= options[:ip] %> <% if options[:netmask] -%> NETMASK=<%= options[:netmask] %> <% end -%> DEVICE=<%= options[:device] %> <% if options[:gateway] -%> GATEWAY=<%= options[:gateway] %> <% end -%> <% if options[:prefix_length] -%> PREFIXLEN=<%= options[:prefix_length] %> <% end -%> #VAGRANT-END ================================================ FILE: templates/license/license.html.tmpl ================================================

License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of MariaDB Corporation Ab.

Parameters

  • Licensor: HashiCorp, Inc.
  • Licensed Work: Vagrant 2.4.3 or later. The Licensed Work is (c) 2025 HashiCorp, Inc.
  • Additional Use Grant: You may make production use of the Licensed Work, provided Your use does not include offering the Licensed Work to third parties on a hosted or embedded basis in order to compete with HashiCorp's paid version(s) of the Licensed Work. For purposes of this license:

    A "competitive offering" is a Product that is offered to third parties on a paid basis, including through paid support arrangements, that significantly overlaps with the capabilities of HashiCorp's paid version(s) of the Licensed Work. If Your Product is not a competitive offering when You first make it generally available, it will not become a competitive offering later due to HashiCorp releasing a new version of the Licensed Work with additional capabilities. In addition, Products that are not provided on a paid basis are not competitive.

    "Product" means software that is offered to end users to manage in their own environments or offered as a service on a hosted basis.

    "Embedded" means including the source code or executable code from the Licensed Work in a competitive offering. "Embedded" also means packaging the competitive offering in such a way that the Licensed Work must be accessed or downloaded for the competitive offering to operate.

    Hosting or using the Licensed Work(s) for internal purposes within an organization is not considered a competitive offering. HashiCorp considers your organization to include all of your affiliates under common control.

    For binding interpretive guidance on using HashiCorp products under the Business Source License, please visit our FAQ. (https://www.hashicorp.com/license-faq)

  • Change Date: Four years from the date the Licensed Work is published.
  • Change License: MPL 2.0

For information about alternative licensing arrangements for the Licensed Work, please contact licensing@hashicorp.com.

Notice

Business Source License 1.1

Terms

The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use.

Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate.

If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work.

All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor.

You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work.

Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work.

This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License).

TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.

================================================ FILE: templates/license/license.tmpl ================================================ License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of MariaDB Corporation Ab. Parameters Licensor: HashiCorp, Inc. Licensed Work: Vagrant 2.4.3 or later. The Licensed Work is (c) 2024 HashiCorp, Inc. Additional Use Grant: You may make production use of the Licensed Work, provided Your use does not include offering the Licensed Work to third parties on a hosted or embedded basis in order to compete with HashiCorp’s paid version(s) of the Licensed Work. For purposes of this license: A "competitive offering" is a Product that is offered to third parties on a paid basis, including through paid support arrangements, that significantly overlaps with the capabilities of HashiCorp's paid version(s) of the Licensed Work. If Your Product is not a competitive offering when You first make it generally available, it will not become a competitive offering later due to HashiCorp releasing a new version of the Licensed Work with additional capabilities. In addition, Products that are not provided on a paid basis are not competitive. "Product" means software that is offered to end users to manage in their own environments or offered as a service on a hosted basis. "Embedded" means including the source code or executable code from the Licensed Work in a competitive offering. "Embedded" also means packaging the competitive offering in such a way that the Licensed Work must be accessed or downloaded for the competitive offering to operate. Hosting or using the Licensed Work(s) for internal purposes within an organization is not considered a competitive offering. HashiCorp considers your organization to include all of your affiliates under common control. For binding interpretive guidance on using HashiCorp products under the Business Source License, please visit our FAQ. (https://www.hashicorp.com/license-faq) Change Date: Four years from the date the Licensed Work is published. Change License: MPL 2.0 For information about alternative licensing arrangements for the Licensed Work, please contact licensing@hashicorp.com. Notice Business Source License 1.1 Terms The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate. If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work. All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor. You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work. Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work. This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License). TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. ================================================ FILE: templates/locales/comm_winrm.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: vagrant_winrm: errors: authentication_failed: |- An authorization error occurred while connecting to WinRM. User: %{user} Endpoint: %{endpoint} Message: %{message} winrm_bad_exit_status: |- The following WinRM command responded with a non-zero exit status. Vagrant assumes that this means the command failed! %{command} Stdout from the command: %{stdout} Stderr from the command: %{stderr} execution_error: |- An error occurred executing a remote WinRM command. Shell: %{shell} Command: %{command} Message: %{message} invalid_shell: |- %{shell} is not a supported type of Windows shell. invalid_transport: |- %{transport} is not a supported WinRM transport. ssl_error: |- An SSL error occurred while connecting to WinRM. This usually occurs when you are using a self-signed certificate and have not set the WinRM `ssl_peer_verification` config setting to false. Message: %{message} winrm_not_ready: |- The box is not able to report an address for WinRM to connect to yet. WinRM cannot access this Vagrant environment. Please wait for the Vagrant environment to be running and try again. winrm_file_transfer_error: |- Failed to transfer a file between the host and guest From: %{from} To: %{to} Message: %{message} connection_refused: |- WinRM connection was refused! This usually happens if the VM failed to boot properly. Some steps to try to fix this: First, try reloading your VM with `vagrant reload`, since a simple restart sometimes fixes things. If that doesn't work, destroy your VM and recreate it with a `vagrant destroy` followed by a `vagrant up`. If that doesn't work, contact a Vagrant maintainer (support channels listed on the website) for more assistance. connection_reset: |- WinRM connection was reset! This usually happens when the machine is taking too long to reboot. First, try reloading your machine with `vagrant reload`, since a simple restart sometimes fixes things. If that doesn't work, destroy your machine and recreate it with a `vagrant destroy` followed by a `vagrant up`. If that doesn't work, contact support. connection_timeout: |- Vagrant timed out while attempting to connect via WinRM. This usually means that the VM booted, but there are issues with the WinRM configuration or network connectivity issues. Please try to `vagrant reload` or `vagrant up` again. disconnected: |- The WinRM connection was unexpectedly closed by the remote end. This usually indicates that WinRM within the guest machine was unable to properly start up. Please boot the VM in GUI mode to check whether it is booting properly. no_route: |- While attempting to connect with WinRM, a "no route to host" (EHOSTUNREACH) error was received. Please verify your network settings are correct and try again. host_down: |- While attempting to connect with WinRM, a "host is down" (EHOSTDOWN) error was received. Please verify your WinRM settings are correct and try again. ================================================ FILE: templates/locales/command_ps.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: vagrant_ps: detecting: |- Detecting if a remote PowerShell connection can be made with the guest... resetting: |- Resetting WinRM TrustedHosts to their original value. errors: elevated_no_command: |- A command must be provided when the --elevated flag is provided for the powershell command. Please provide a command when using the --elevated flag and try again. 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. 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} ================================================ FILE: templates/locales/command_rdp.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: vagrant_rdp: detecting: |- Detecting RDP info... connecting: |- Vagrant will now launch your RDP client with the connection parameters above. If the connection fails, verify that the information above is correct. Additionally, make sure the RDP server is configured and running in the guest machine (it is disabled by default on Windows). Also, verify that the firewall is open to allow RDP connections. errors: host_unsupported: |- Vagrant doesn't support running an RDP client on your host OS. If you wish for the OS you're running to support launching an RDP client, please contribute this functionality back into Vagrant. At the very least, open an issue on how it could be done and we can handle the integration. rdp_undetected: |- RDP connection information for this machine could not be detected. This is typically caused when we can't find the IP or port to connect to for RDP. Please verify you're forwarding an RDP port and that your machine is accessible. ================================================ FILE: templates/locales/en.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: vagrant: alert: |- [%{date}]: %{message} - %{url} boot_completed: |- Machine booted and ready! boot_waiting: |- Waiting for machine to boot. This may take a few minutes... box_auto_adding: |- Box '%{name}' could not be found. Attempting to find and install... box_add_choose_provider: |- This box can work with multiple providers! The providers that it can work with are listed below. Please review the list and choose the provider you will be working with. %{options} Enter your choice: box_add_choose_provider_again: |- Invalid choice. Try again: box_add_with_version: |- Adding box '%{name}' (v%{version}) for provider: %{providers} box_added: |- Successfully added box '%{name}' (v%{version}) for '%{provider}'! box_adding_direct: |- Box file was not detected as metadata. Adding it directly... box_add_url_warn: |- It looks like you attempted to add a box with a URL for the name... Instead, use box_url instead of box for box URLs. box_downloading: |- Downloading: %{url} box_download_error: |- Error downloading: %{message} box_unpacking: |- Unpacking necessary files from: %{url} box_expanding_url: |- URL: %{url} box_loading_metadata: |- Loading metadata for box '%{name}' box_malformed_continue_on_update: |- Could not determine box updates because box metadata was malformed. Vagrant will continue on... box_outdated: |- * '%{name}' for '%{provider}' is outdated! Current: %{current}. Latest: %{latest} box_outdated_checking_with_refresh: |- Checking if box '%{name}' version '%{version}' is up to date... box_outdated_local: |- A newer version of the box '%{name}' is available and already installed, but your Vagrant machine is running against version '%{old}'. To update to version '%{new}', destroy and recreate your machine. box_outdated_metadata_download_error: |- There was a problem while downloading the metadata for your box to check for updates. This is not an error, since it is usually due to temporary network problems. This is just a warning. The problem encountered was: %{message} If you want to check for box updates, verify your network connection is valid and try again. box_outdated_metadata_error_single: |- Error loading box metadata while attempting to check for updates: %{message} box_outdated_single: |- A newer version of the box '%{name}' for provider '%{provider}' is available! You currently have version '%{current}'. The latest is version '%{latest}'. Run `vagrant box update` to update. box_outdated_metadata_error: |- * '%{name}' for '%{provider}': Error loading metadata: %{message} box_outdated_no_metadata: |- * '%{name}' for '%{provider}' wasn't added from a catalog, no version information box_updating: |- Updating '%{name}' with provider '%{provider}' from version '%{old}' to '%{new}'... box_update_checking: |- Checking for updates to '%{name}' box_up_to_date: |- * '%{name}' for '%{provider}' (v%{version}) is up to date box_up_to_date_single: |- Box '%{name}' (v%{version}) is running the latest version. box_version_malformed: Invalid version '%{version}' for '%{box_name}', ignoring... cfengine_bootstrapping: |- Bootstrapping CFEngine with policy server: %{policy_server}... cfengine_bootstrapping_policy_hub: |- Performing additional bootstrap for policy hubs... cfengine_cant_detect: |- Vagrant doesn't support detecting whether CFEngine is installed for the guest OS running in the machine. Vagrant will assume it is installed and attempt to continue. cfengine_detected_ip: |- Detected policy server IP address: %{address}... cfengine_installing: |- Installing CFEngine onto machine... cfengine_installing_files_path: |- Copying the 'files_path' files... cfengine_no_bootstrap: |- CFEngine doesn't require bootstrap. Not bootstrapping. cfengine_single_run: |- CFEngine running in "single run" mode. Will execute one file. cfengine_single_run_execute: |- Executing run file for CFEngine... chef_cant_detect: |- Vagrant does not support detecting whether Chef is installed for the guest OS running in the machine. Vagrant will assume it is installed and attempt to continue. chef_already_installed: |- Detected Chef (%{version}) is already installed chef_installing: |- Installing Chef (%{version})... chef_client_cleanup_failed: |- Cleaning up the '%{deletable}' for Chef failed. The stdout and stderr are shown below. Vagrant will continue destroying the machine, so please clean up these resources manually. stdout: %{stdout} stderr: %{stderr} chef_config_knife_not_found: |- The `knife` application was not found! This is required by Vagrant to automatically delete Chef nodes and clients. chef_run_list_empty: |- Warning: Chef run list is empty. This may not be what you want. cli_interrupt: |- Exiting due to interrupt. cloud_init_waiting: Waiting for cloud init to finish running container_pulling_single: |- -- Image: %{name} container_building_single: |- -- Path: %{path} container_running: |- -- Container: %{name} container_restarting_container_args: |- -- Detected changes to container '%{name}' args, restarting... container_restarting_container_image: |- -- Detected newer image for container '%{name}', restarting... docker_auto_start_not_available: |- Unable to configure automatic restart of Docker containers on the guest machine docker_cant_detect: |- Vagrant doesn't support detecting whether Docker is installed for the guest OS running in the machine. Vagrant will assume it is installed and attempt to continue. docker_configure_autostart: |- Configuring Docker to autostart containers... docker_installing: |- Installing Docker onto machine... docker_pulling_images: Pulling Docker images... docker_pulling_single: |- -- Image: %{name} docker_building_single: |- -- Path: %{path} docker_building_images: Building Docker images... docker_running: |- -- Container: %{name} docker_restarting_container_args: |- -- Detected changes to container '%{name}' args, restarting... docker_restarting_container_image: |- -- Detected newer image for container '%{name}', restarting... docker_starting_containers: |- Starting Docker containers... hyperv_enable_enhanced_session: |- Setting VM Enhanced session transport type to HvSocket hyperv_disable_enhanced_session: |- Setting VM Enhanced session transport type to disabled/default (VMBus) inserted_key: |- Key inserted! Disconnecting and reconnecting using new SSH key... inserting_insecure_detected: |- Vagrant insecure key detected. Vagrant will automatically replace this with a newly generated keypair for better security. inserting_random_key: |- 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. %{list} moved_cwd: |- This machine used to live in %{old_wd} but it's now at %{current_wd}. Depending on your current provider you may need to change the name of the machine to run it as a different machine. guest_deb_installing_smb: |- Installing SMB "mount.cifs"... global_status_footer: |- The above shows information about all known Vagrant environments on this machine. This data is cached and may not be completely up-to-date (use "vagrant global-status --prune" to prune invalid entries). To interact with any of the machines, you can go to that directory and run Vagrant, or you can use the ID directly with Vagrant commands from any directory. For example: "vagrant destroy 1a2b3c4d" global_status_none: |- There are no active Vagrant environments on this computer! Or, you haven't destroyed and recreated Vagrant environments that were started with an older version of Vagrant. plugin_needs_reinstall: |- The following plugins were installed with a version of Vagrant that had different versions of underlying components. Because these component versions were changed (which rarely happens), the plugins must be uninstalled and reinstalled. To ensure that all the dependencies are properly updated as well it is _highly recommended_ to do a `vagrant plugin uninstall` prior to reinstalling. This message will not go away until all the plugins below are either uninstalled or uninstalled then reinstalled. The plugins below will not be loaded until they're uninstalled and reinstalled: %{names} post_up_message: |- Machine '%{name}' has a post `vagrant up` message. This is a message from the creator of the Vagrantfile, and not from Vagrant itself: %{message} provisioner_cleanup: |- Running cleanup tasks for '%{name}' provisioner... rsync_auto_initial: |- Doing an initial rsync... rsync_auto_new_folders: |- New synced folders were added to the Vagrantfile since running `vagrant reload`. If these new synced folders are backed by rsync, they won't be automatically synced until a `vagrant reload` is run. rsync_auto_no_paths: |- There are no paths to watch! This is either because you have no synced folders using rsync, or any rsync synced folders you have have specified `rsync_auto` to be false. rsync_auto_path: |- Watching: %{path} rsync_auto_remove_folder: |- Not syncing %{folder} as it is not part of the current working directory. rsync_auto_rsync_error: |- There was an error while executing rsync. The error is shown below. This may not be critical since rsync sometimes fails, but if this message repeats, then please fix the issue: %{message} rsync_auto_post_command_error: |- There was an error while executing the rsync post command. This error is shown below. This may not be critical but if this message repeats please fix the issue: %{message} rsync_communicator_not_ready: |- The machine is reporting that it is not ready for rsync to communicate with it. Verify that this machine is properly running. rsync_communicator_not_ready_callback: |- Failed to connect to remote machine. This is usually caused by the machine rebooting or being halted. Please make sure the machine is running, and modify a file to try again. rsync_folder: |- Rsyncing folder: %{hostpath} => %{guestpath} rsync_folder_excludes: " - Exclude: %{excludes}" rsync_installing: "Installing rsync to the VM..." rsync_proxy_machine: |- The provider ('%{provider}') for the machine '%{name}' is using a proxy machine. RSync will sync to this proxy instead of directly to the environment itself. rsync_showing_output: "Showing rsync output..." rsync_ssh_password: |- The machine you're rsyncing folders to is configured to use password-based authentication. Vagrant can't script rsync to automatically enter this password, so you'll likely be prompted for a password shortly. If you don't want to have to do this, please enable automatic key insertion using `config.ssh.insert_key`. ssh_exec_password: |- The machine you're attempting to SSH into is configured to use password-based authentication. Vagrant can't script entering the password for you. If you're prompted for a password, please enter the same password you have configured in the Vagrantfile. stdin_cant_hide_input: |- Error! Your console doesn't support hiding input. We'll ask for input again below, but we WILL NOT be able to hide input. If this is a problem for you, ctrl-C to exit and fix your stdin. up_no_machines: |- No machines to bring up. This is usually because all machines are set to `autostart: false`, which means you have to explicitly specify the name of the machine to bring up. upgrading_home_path_v1_5: |- Vagrant is upgrading some internal state for the latest version. Please do not quit Vagrant at this time. While upgrading, Vagrant will need to copy all your boxes, so it will use a considerable amount of disk space. After it is done upgrading, the temporary disk space will be freed. Press ctrl-c now to exit if you want to remove some boxes or free up some disk space. Press the Enter or Return key to continue. trigger: on_error_continue: |- Trigger configured to continue on error... abort: |- Vagrant has been configured to abort. Terminating now... abort_threaded: |- Vagrant has been configured to abort. Vagrant will terminate after remaining running actions have completed... start: |- Running %{type} triggers %{stage} %{name} ... fire_with_name: |- Running trigger: %{name}... fire: |- Running trigger... run: inline: |- Running local: Inline script %{command} script: |- Running local script: %{path} version_current: |- Installed Version: %{version} version_latest: |- Latest Version: %{version} version_latest_installed: |- You're running an up-to-date version of Vagrant! version_no_checkpoint: |- Vagrant was unable to check for the latest version of Vagrant. Please check manually at https://www.vagrantup.com version_upgrade_available: |- A new version of Vagrant is available: %{latest_version} (installed version: %{installed_version})! To upgrade visit: https://www.vagrantup.com/downloads.html version_upgrade_howto: |- To upgrade to the latest version, visit the downloads page and download and install the latest version of Vagrant from the URL below: https://www.vagrantup.com/downloads.html If you're curious what changed in the latest release, view the CHANGELOG below: https://github.com/hashicorp/vagrant/blob/v%{version}/CHANGELOG.md cfengine_config: classes_array: |- The 'classes' configuration must be an array. files_path_not_directory: |- The 'files_path' must point to a valid directory. invalid_mode: |- The mode must be 'bootstrap' or 'single_run' policy_server_address: |- The policy server address must be set for bootstrapping. run_file_not_found: |- The 'run_file' specified could not be found. virtualbox: checking_guest_additions: |- Checking for guest additions in VM... network_adapter: |- Adapter %{adapter}: %{type}%{extra} config: id_in_pre_import: |- The ':id' parameter is not available in "pre-import" customizations. intnet_on_bad_type: |- VirtualBox internal networks can only be enabled on "private_network" invalid_event: |- %{event} is not a valid event for customization. Valid events are: %{valid_events} warning: shared_folder_symlink_create: |- Vagrant is currently configured to create VirtualBox synced folders with the `SharedFoldersEnableSymlinksCreate` option enabled. If the Vagrant guest is not trusted, you may want to disable this option. For more information on this option, please refer to the VirtualBox manual: https://www.virtualbox.org/manual/ch04.html#sharedfolders This option can be disabled globally with an environment variable: VAGRANT_DISABLE_VBOXSYMLINKCREATE=1 or on a per folder basis within the Vagrantfile: config.vm.synced_folder '/host/path', '/guest/path', SharedFoldersEnableSymlinksCreate: false general: batch_notify_error: |- An error occurred. The error will be shown after all tasks complete. batch_unexpected_error: |- An unexpected error occurred when executing the action on the '%{machine}' machine. Please report this as a bug: %{message} batch_vagrant_error: |- An error occurred while executing the action on the '%{machine}' machine. Please handle this error then try again: %{message} config_upgrade_messages: |- There were warnings and/or errors while loading your Vagrantfile for the machine '%{name}'. Your Vagrantfile was written for an earlier version of Vagrant, and while Vagrant does the best it can to remain backwards compatible, there are some cases where things have changed significantly enough to warrant a message. These messages are shown below. %{output} experimental: all: |- You have enabled the experimental flag with all features enabled. Please use with caution, as some of the features may not be fully functional yet. features: |- You have requested to enable the experimental flag with the following features: Features: %{features} Please use with caution, as some of the features may not be fully functional yet. not_in_installer: |- You appear to be running Vagrant outside of the official installers. Note that the installers are what ensure that Vagrant has all required dependencies. Vagrant has detected that the following executables are currently unavailable: %{tools} upgraded_v1_dotfile: |- A Vagrant 1.0.x state file was found for this environment. Vagrant has gone ahead and auto-upgraded this to the latest format. Everything should continue working as normal. Beware, however, that older versions of Vagrant may no longer be used with this environment. However, in case anything went wrong, the old dotfile was backed up to the location below. If everything is okay, it is safe to remove this backup. Backup: %{backup_path} plugins: local: uninstalled_plugins: |- Vagrant has detected project local plugins configured for this project which are not installed. %{plugins} request_plugin_install: |- Install local plugins (Y/N) install_rerun_command: |- Vagrant has completed installing local plugins for the current Vagrant project directory. Please run the requested command again. install_all: |- Vagrant will now install the following plugins to the local project which have been defined in current Vagrantfile: %{plugins} Press ctrl-c to cancel... #------------------------------------------------------------------------------- # Translations for exception classes #------------------------------------------------------------------------------- errors: active_machine_with_different_provider: |- An active machine was found with a different provider. Vagrant currently allows each machine to be brought up with only a single provider at a time. A future version will remove this limitation. Until then, please destroy the existing machine to up with a new provider. Machine name: %{name} Active provider: %{active_provider} Requested provider: %{requested_provider} alias_invalid_error: |- The defined alias is not valid. Please review the information below to help resolve the issue: Alias: %{alias} Message: %{message} batch_multi_error: |- An error occurred while executing multiple actions in parallel. Any errors that occurred are shown below. %{message} box_add_no_matching_provider: |- The box you're attempting to add doesn't support the provider you requested. Please find an alternate box or use an alternate provider. Double-check your requested provider to verify you didn't simply misspell it. If you're adding a box from HashiCorp's Vagrant Public Registry, make sure the box is released. Name: %{name} Address: %{url} Requested provider: %{requested} box_add_no_architecture_support: |- The box you're attempting to add doesn't support the requested architecture. Please find an alternate box that support the requested architecture. Box: %{name} Address: %{url} Architecture: %{architecture} box_add_no_matching_architecture: |- The box you're attempting to add doesn't support the requested architecture with the current provider. The following providers support the requested architecture for this box: %{supported_providers} If the above providers cannot be used, please find and alternate box that supports the requested architecture. Box: %{name} Address: %{url} Architecture: %{architecture} Provider: %{provider} box_add_no_matching_provider_version: |- The box you're attempting to add has no available version that matches the constraints you requested with support for the required provider and architecture. Versions of the box that support the required provider and architecture are listed below. Box: %{name} Address: %{url} Constraints: %{constraints} Architecture: %{architecture} Provider: %{provider} Supported versions: %{versions} box_add_no_matching_version: |- The box you're attempting to add has no available version that matches the constraints you requested. Please double-check your settings. Also verify that if you specified version constraints, that the provider you wish to use is available for these constraints. Box: %{name} Address: %{url} Constraints: %{constraints} Available versions: %{versions} box_add_short_not_found: |- The box '%{name}' could not be found or could not be accessed in the remote catalog. If this is a private box on the HashiCorp Vagrant Public Registry, please verify you're logged in via `vagrant cloud auth login`. Also, please double-check the name. The expanded URL and error message are shown below: URL: %{url} Error: %{error} boot_bad_state: |- The guest machine entered an invalid state while waiting for it to boot. Valid states are '%{valid}'. The machine is in the '%{invalid}' state. Please verify everything is configured properly and try again. If the provider you're using has a GUI that comes with it, it is often helpful to open that and watch the machine, since the GUI often has more helpful error messages than Vagrant can retrieve. For example, if you're using VirtualBox, run `vagrant up` while the VirtualBox GUI is open. The primary issue for this error is that the provider you're using is not properly configured. This is very rarely a Vagrant issue. boot_timeout: |- Timed out while waiting for the machine to boot. This means that Vagrant was unable to communicate with the guest machine within the configured ("config.vm.boot_timeout" value) time period. If you look above, you should be able to see the error(s) that Vagrant had when attempting to connect to the machine. These errors are usually good hints as to what may be wrong. If you're using a custom box, make sure that networking is properly working and you're able to connect to the machine. It is a common problem that networking isn't setup properly in these boxes. Verify that authentication configurations are also setup properly, as well. If the box appears to be booting properly, you may want to increase the timeout ("config.vm.boot_timeout") value. box_add_exists: |- The box you're attempting to add already exists. Remove it before adding it again or add it with the `--force` flag. Name: %{name} Provider: %{provider} Version: %{version} box_add_direct_version: |- You specified a box version constraint with a direct box file path. Box version constraints only work with boxes from Vagrant Cloud or a custom box host. Please remove the version constraint and try again. box_add_metadata_multi_url: |- Multiple URLs for a box can't be specified when adding versioned boxes. Please specify a single URL to the box metadata (JSON) information. The full list of URLs you specified is shown below: %{urls} box_add_name_mismatch: |- The box you're adding has a name different from the name you requested. For boxes with metadata, you cannot override the name. If you're adding a box using `vagrant box add`, don't specify the `--name` parameter. If the box is being added via a Vagrantfile, change the `config.vm.box` value to match the name below. Requested name: %{requested_name} Actual name: %{actual_name} box_add_name_required: |- A name is required when adding a box file directly. Please pass the `--name` parameter to `vagrant box add`. See `vagrant box add -h` for more help. box_checksum_invalid_type: |- The specified checksum type is not supported by Vagrant: %{type}. Vagrant supports the following checksum types: %{types} box_checksum_mismatch: |- The checksum of the downloaded box did not match the expected value. Please verify that you have the proper URL setup and that you're downloading the proper file. Expected: %{expected} Received: %{actual} box_config_changing_box: |- While loading the Vagrantfile, the provider override specified a new box. This box, in turn, specified a different box. This isn't allowed, as it could lead to infinite recursion of Vagrant configuration loading. Please fix this. box_file_not_exist: |- The file you are attempting to upload does not exist. Please recheck that the file exists and was passed in correctly. File: %{file} box_metadata_corrupted: |- The metadata associated with the box '%{name}' appears corrupted. This is most often caused by a disk issue or system crash. Please remove the box, re-add it, and try again. box_metadata_missing_required_fields: |- The metadata associated with the box '%{name}' appears to be missing the required field '%{required_field}'. Please ensure `metadata.json` has all required fields. Required fields: %{all_fields} box_metadata_download_error: |- There was an error while downloading the metadata for this box. The error message is shown below: %{message} box_metadata_file_not_found: |- The "metadata.json" file for the box '%{name}' was not found. Boxes require this file in order for Vagrant to determine the provider it was made for. If you made the box, please add a "metadata.json" file to it. If someone else made the box, please notify the box creator that the box is corrupt. Documentation for box file format can be found at the URL below: https://www.vagrantup.com/docs/boxes/format.html box_metadata_malformed: |- The metadata for the box was malformed. The exact error is shown below. Please contact the maintainer of the box so that this issue can be fixed. %{error} box_metadata_malformed_version: |- A version of the box you're loading is formatted in a way that Vagrant cannot parse: '%{version}'. Please reformat the version to be properly formatted. It should be in the format of X.Y.Z. box_not_found: |- The box '%{name}' does not exist. Please double check and try again. You can see the boxes that are installed with `vagrant box list`. box_not_found_with_provider: |- The box '%{name}' isn't installed for the provider '%{provider}'. Please double-check and try again. The installed providers for the box are shown below: %{providers} box_not_found_with_provider_architecture: |- The box '%{name}' for the provider '%{provider}' isn't installed for the architecture '%{architecture}'. Please double-check and try again. The installed architectures for the box are shown below: %{architectures} box_not_found_with_provider_and_version: |- The box '%{name}' (v%{version}) with provider '%{provider}' could not be found. Please double check and try again. You can see all the boxes that are installed with `vagrant box list`. box_provider_doesnt_match: |- The box you attempted to add doesn't match the provider you specified. Provider expected: %{expected} Provider of box: %{actual} box_remove_multi_provider: |- You requested to remove the box '%{name}' version '%{version}'. This box has multiple providers. You must explicitly select a single provider to remove with `--provider` or specify the `--all-providers` flag to remove all providers. Available providers: %{providers} box_remove_multi_version: |- You requested to remove the box '%{name}'. This box has multiple versions. You must explicitly specify which version you want to remove with 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: |- The box you requested to be removed could not be found. No boxes named '%{name}' could be found. box_remove_provider_not_found: |- You requested to remove the box '%{name}' version '%{version}' with provider '%{provider}'. The box '%{name}' exists but not with the provider specified. Please double-check and try again. The providers for this are: %{providers} box_remove_version_not_found: |- You requested to remove the box '%{name}' version '%{version}', but that specific version of the box is not installed. Please double-check and try again. The available versions for this box are: %{versions} box_remove_multi_architecture: |- You requested to remove the box '%{name}' version '%{version}' with provider '%{provider}'. This box has multiple architectures. You must explicitly select a single architecture to remove with `--architecture` or specify the `--all-architectures` flag to remove all architectures. The available architectures for this box are: %{architectures} box_remove_architecture_not_found: |- You requested to remove the box '%{name}' version '%{version}' with provider '%{provider}' and architecture '%{architecture}' but that specific architecture is not installed. Please double-check and try again. The available architectures are: %{architectures} box_server_not_set: |- A URL to a Vagrant Cloud server is not set, so boxes cannot be added with a shorthand ("mitchellh/precise64") format. You may also be seeing this error if you meant to type in a path to a box file which doesn't exist locally on your system. To set a URL to a Vagrant Cloud server, set the `VAGRANT_SERVER_URL` environmental variable. Or, if you meant to use a file path, make sure the path to the file is valid. box_update_multi_provider: |- You requested to update the box '%{name}'. This box has multiple providers. You must explicitly select a single provider to update with `--provider`. Available providers: %{providers} box_update_multi_architecture: |- You requested to update the box '%{name}' (v%{version}) with provider '%{provider}'. This box has multiple architectures. You must explicitly select a single architecture to update with `--architecture`. Available architectures: %{architectures} box_update_no_box: |- Box '%{name}' not installed, can't check for updates. box_update_no_metadata: |- The box '%{name}' is not a versioned box. The box was added directly instead of from a box catalog. Vagrant can only check the versions of boxes that were added from a catalog such as from the public Vagrant Server. box_update_no_name: |- This machine doesn't have a box. Won't update anything. box_version_invalid: |- The format of box version provided (%{version}) is incorrect. The version must follow the semantic versioning format or semantic versioning compatible constraint format. Examples of valid values: 2.0 2.1.4 >= 2 < 3.0.0 bundler_disabled: |- Vagrant's built-in bundler management mechanism is disabled because Vagrant is running in an external bundler environment. In these cases, plugin management does not work with Vagrant. To install plugins, use your own Gemfile. To load plugins, either put the plugins in the `plugins` group in your Gemfile or manually require them in a Vagrantfile. bundler_error: |- Vagrant failed to properly resolve required dependencies. These errors can commonly be caused by misconfigured plugin installations or transient network issues. The reported error is: %{message} source_spec_not_found: |- Vagrant failed to properly initialize its internal library dependencies. Please try running the command again. If this error persists, please report a bug. 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. This is an internal error that users should never see. Please report a bug. capability_host_not_detected: |- The capability host could not be detected. This is an internal error that users should never see. Please report a bug. capability_invalid: |- The capability '%{cap}' is invalid. This is an internal error that users should never see. Please report a bug. capability_not_found: |- The capability '%{cap}' could not be found. This is an internal error that users should never see. Please report a bug. cfengine_bootstrap_failed: |- Failed to bootstrap CFEngine. Please see the output above to see what went wrong and address the issue. cfengine_cant_autodetect_ip: |- Vagrant was unable to automatically detect the IP address of the running machine to use as the policy server address. Please specify the policy server address manually, or verify that the networks are configured properly internally. cfengine_install_failed: |- After installing CFEngine, Vagrant still can't detect a proper CFEngine installation. Please verify that CFEngine was properly installed, as the installation may have failed. cfengine_not_installed: |- CFEngine appears not to be installed on the guest machine. Vagrant can attempt to install CFEngine for you if you set the "install" setting to "true", but this setting was not set. Please install CFEngine on the guest machine or enable the "install" setting for Vagrant to attempt to install it for you. cli_invalid_options: |- An invalid option was specified. The help for this command is available below. %{help} cli_invalid_usage: |- This command was not invoked properly. The help for this command is 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. cloud_init_not_found: |- cloud-init is not found. Please ensure that cloud-init is installed and available on path for guest '%{guest_name}'. cloud_init_command_failed: |- cloud init command '%{cmd}' failed on guest '%{guest_name}'. command_deprecated: |- The command 'vagrant %{name}' has been deprecated and is no longer functional within Vagrant. command_suspend_all_arguments: |- The suspend command with the `--all-global` flag does not take any arguments. command_unavailable: |- The executable '%{file}' Vagrant is trying to run was not found in the PATH variable. This is an error. Please verify this software is installed and on the path. command_unavailable_windows: |- The executable '%{file}' Vagrant is trying to run was not found in the %PATH% variable. This is an error. Please verify this software is installed and on the path. communicator_not_found: |- The requested communicator '%{comm}' could not be found. Please verify the name is correct and try again. config_invalid: |- There are errors in the configuration of this machine. Please fix the following errors and try again: %{errors} config_upgrade_errors: |- Because there were errors upgrading your Vagrantfiles, Vagrant can no longer continue. Please fix the errors above and try again. copy_private_key_failed: |- Vagrant failed to copy the default insecure private key into your home directory. This is usually caused by a permissions error. Please make sure the permissions of the source is readable and the destination is writable. Source: %{source} Destination: %{destination} corrupt_machine_index: |- The machine index which stores all required information about running Vagrant environments has become corrupt. This is usually caused by external tampering of the Vagrant data folder. Vagrant cannot manage any Vagrant environments if the index is corrupt. Please attempt to manually correct it. If you are unable to manually correct it, then remove the data file at the path below. This will leave all existing Vagrant environments "orphaned" and they'll have to be destroyed manually. Path: %{path} create_iso_host_cap_not_found: |- Vagrant cannot create an iso due to the host capability for creating isos not existing. Vagrant will now exit. darwin_mount_failed: |- Failed to mount folders in Darwin guest. The command attempted was: %{command} The error output from the last command was: %{output} darwin_version_failed: |- Failed to determine macOS version. Version string: %{version} Error information: %{error} destroy_requires_force: |- Destroy doesn't have a TTY to ask for confirmation. Please pass the `--force` flag to force a destroy, otherwise attach a TTY so that the destroy can be confirmed. dotfile_upgrade_json_error: |- A Vagrant 1.0.x local state file was found. Vagrant is able to upgrade this to the latest format automatically, however various checks are put in place to verify data isn't incorrectly deleted. In this case, the old state file was not valid JSON. Vagrant 1.0.x would store state as valid JSON, meaning that this file was probably tampered with or manually edited. Vagrant's auto-upgrade process cannot continue in this case. In most cases, this can be resolve by simply removing the state file. Note however though that if Vagrant was previously managing virtual machines, they may be left in an "orphan" state. That is, if they are running or exist, they'll have to manually be removed. If you're unsure what to do, ask the Vagrant mailing list or contact support. State file path: %{state_file} download_already_in_progress_error: |- Download to global Vagrant location already in progress. This may be caused by other Vagrant processes attempting to download a file to the same location. Download path: %{dest_path} Lock file path: %{lock_file_path} downloader_error: |- An error occurred while downloading the remote file. The error message, if any, is reproduced below. Please fix this error and try again. %{message} downloader_interrupted: |- The download was interrupted by an external signal. It did not complete. downloader_checksum_error: |- The calculated checksum of the requested file does not match the expected checksum! File source: %{source} Checksum type: %{type} Expected checksum: %{expected_checksum} Calculated checksum: %{actual_checksum} env_inval: |- Vagrant received an "EINVAL" error while attempting to set some environment variables. This is usually caused by the total size of your environment variables being too large. Vagrant sets a handful of environment variables to function and requires this to work. Please delete some environment variables prior to executing Vagrant to fix this. environment_locked: |- Vagrant attempted to acquire a lock named '%{name}', but this lock is being held by another instance of Vagrant already. Please wait and try again. environment_non_existent_cwd: |- The working directory for Vagrant doesn't exist! This is the specified working directory: %{cwd} forward_port_adapter_not_found: |- The adapter to attach a forwarded port to was not found. Please verify that the given adapter is setup on the machine as a NAT interface. Host port: %{host} Guest port: %{guest} Adapter: %{adapter} freebsd_nfs_whitespace: |- FreeBSD hosts do not support sharing directories with whitespace in their path. Please adjust your path accordingly. guest_capability_invalid: |- The registered guest capability '%{cap}' for the detected guest OS '%{guest}' is invalid. The capability does not implement the proper method. This is a bug with Vagrant or the plugin that implements this capability. Please report a bug. guest_capability_not_found: |- Vagrant attempted to execute the capability '%{cap}' on the detect guest OS '%{guest}', but the guest doesn't support that capability. This capability is required for your configuration of Vagrant. Please either reconfigure Vagrant to avoid this capability or fix the issue by creating the capability. guest_explicit_not_detected: |- The guest implementation explicitly specified in your Vagrantfile ("%{value}") could not be found. Please verify that the plugin is installed which implements this guest and that the value you used for `config.vm.guest` is correct. guest_not_detected: |- The guest operating system of the machine could not be detected! Vagrant requires this knowledge to perform specific tasks such as mounting shared folders and configuring networks. Please add the ability to detect this guest operating system to Vagrant by creating a plugin or reporting a bug. home_dir_later_version: |- It appears that a newer version of Vagrant was run on this machine at some point. The current version of Vagrant is unable to read the configuration structure of this newer version. Please upgrade to the latest version of Vagrant. home_dir_not_accessible: |- The home directory you specified is not accessible. The home directory that Vagrant uses must be both readable and writable. You specified: %{home_path} home_dir_unknown_version: |- The Vagrant app data directory (%{path}) is in a structure Vagrant doesn't understand. This is a rare exception. Please report an issue or ask the mailing list for help. host_explicit_not_detected: |- The host implementation explicitly specified in your Vagrantfile ("%{value}") could not be found. Please verify that the plugin is installed which implements this host and that the value you used for `config.vagrant.host` is correct. iso_build_failed: |- Failed to build iso image. The following command returned an error: %{cmd} Stdout from the command: %{stdout} Stderr from the command: %{stderr} hyperv_virtualbox_error: |- Hyper-V and VirtualBox cannot be used together and will result in a system crash. Vagrant will now exit. Please disable Hyper-V if you wish to use VirtualBox. interrupted: |- Vagrant exited after cleanup due to external interrupt. local_data_dir_not_accessible: |- The directory Vagrant will use to store local environment-specific state is not accessible. The directory specified as the local data directory must be both readable and writable for the user that is running Vagrant. Local data directory: %{local_data_path} linux_mount_failed: |- Failed to mount folders in Linux guest. This is usually because the "vboxsf" file system is not available. Please verify that the guest additions are properly installed in the guest and can work properly. The command attempted was: %{command} The error output from the last command was: %{output} 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. Vagrant locks each machine for access by only one process at a time. Please wait until the other Vagrant process finishes modifying this machine, then try again. If you believe this message is in error, please check the process listing for any "ruby" or "vagrant" processes and kill them. Then try again. machine_folder_not_accessible: |- Vagrant attempted to clean the machine folder for the machine '%{name}' but does not have permission to read the following path: %{path} Please ensure that Vagrant has the proper permissions to access the path above. You may need to grant this permission to the terminal emulator running Vagrant as well. machine_guest_not_ready: |- Guest-specific operations were attempted on a machine that is not ready for guest communication. This should not happen and a bug should be reported. machine_locked: |- Vagrant can't use the requested machine because it is locked! This means that another Vagrant process is currently reading or modifying the machine. Please wait for that Vagrant process to end and try again. Details about the machine are shown below: Name: %{name} Provider: %{provider} machine_not_found: |- The machine with the name '%{name}' was not found configured for this Vagrant environment. machine_state_invalid: |- An internal error has occurred! The provider of the machine you're trying to work with reported an invalid state. This is a bug with the provider you're using, and not with Vagrant itself or with any configuration you may have done. Please report this bug to the proper location. multi_vm_target_required: |- This command requires a specific VM name to target in a multi-VM environment. netplan_no_available_renderers: |- No renderers compatible with netplan are available on guest. Please install a compatible renderer. net_ssh_exception: |- An error occurred in the underlying SSH library that Vagrant uses. The error message is shown below. In many cases, errors from this library are caused by ssh-agent issues. Try disabling your SSH agent or removing some keys and try again. If the problem persists, please report a bug to the net-ssh project. %{message} network_type_not_supported: |- The %{type} network type is not supported for this box or guest. network_address_invalid: |- Network settings specified in your Vagrantfile define an invalid IP address. Please review the error message below and update your Vagrantfile network settings: Address: %{address} Netmask: %{mask} Error: %{error} network_manager_not_installed: |- Vagrant was instructed to configure the %{device} network device to be managed by NetworkManager. However, the configured guest VM does not have NetworkManager installed. To fix this error please remove the `nm_controlled` setting from local Vagrantfile. If NetworkManager is required to manage the network devices, please use a box with NetworkManager installed. nfs_bad_exports: |- NFS is reporting that your exports file is invalid. Vagrant does this check before making any changes to the file. Please correct the issues below and execute "vagrant reload": %{output} nfs_exports_failed: |- Vagrant failed to install an updated NFS exports file. This may be due to overly restrictive permissions on your NFS exports file. Please validate them and try again. command: %{command} stdout: %{stdout} stderr: %{stderr} nfs_dupe_permissions: |- You have attempted to export the same nfs host path at %{hostpath} with different nfs permissions. Please pick one permission and reload your guest. nfs_cant_read_exports: |- Vagrant can't read your current NFS exports! The exports file should be readable by any user. This is usually caused by invalid permissions on your NFS exports file. Please fix them and try again. nfs_mount_failed: |- Mounting NFS shared folders failed. This is most often caused by the NFS client software not being installed on the guest machine. Please verify 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. nfs_no_guest_ip: |- No guest IP was given to the Vagrant core NFS helper. This is an internal error that should be reported as a bug. nfs_no_host_ip: |- No host IP was given to the Vagrant core NFS helper. This is an internal error that should be reported as a bug. nfs_no_hostonly_network: |- NFS requires a host-only network to be created. Please add a host-only network to the machine (with either DHCP or a static IP) for NFS to work. nfs_no_valid_ids: |- No valid IDs were given to the NFS synced folder implementation to prune. This is an internal bug with Vagrant and an issue should be filed. nfs_not_supported: |- It appears your machine doesn't support NFS, or there is not an adapter to enable NFS on this machine for Vagrant. Please verify that `nfsd` is installed on your machine, and try again. If you're on Windows, NFS isn't supported. If the problem persists, please contact Vagrant support. nfs_client_not_installed_in_guest: |- No NFS client was detected as installed in your guest machine. This is required for NFS synced folders to work. In addition to this, Vagrant doesn't know how to automatically install NFS for your machine, so you must do this manually. If this message is erroneous, you may disable this check by setting `config.nfs.verify_installed` to `false` in your Vagrantfile. no_default_provider: |- No usable default provider could be found for your system. Vagrant relies on interactions with 3rd party systems, known as "providers", to provide Vagrant with resources to run development environments. Examples are VirtualBox, VMware, Hyper-V. The easiest solution to this message is to install VirtualBox, which is available for free on all major platforms. If you believe you already have a provider available, make sure it is properly installed and configured. You can see more details about why a particular provider isn't working by forcing usage with `vagrant up --provider=PROVIDER`, which should give you a more specific error message for that particular provider. no_default_synced_folder_impl: |- No synced folder implementation is available for your synced folders! Please consult the documentation to learn why this may be the case. You may force a synced folder implementation by specifying a "type:" option for the synced folders. Available synced folder implementations are listed below. %{types} no_env: |- A Vagrant environment or target machine is required to run this command. Run `vagrant init` to create a new Vagrant environment. Or, get an ID of a target machine from `vagrant global-status` to run this command on. A final option is to change to a directory with a Vagrantfile and to try again. oscdimg_command_missing: |- Vagrant failed to locate the oscdimg.exe executable which is required for creating ISO files. Please ensure the oscdimg.exe executable is available on the configured PATH. If the oscdimg.exe executable is not found on the local system, it can be installed with the Windows Assessment and Deployment Kit: https://go.microsoft.com/fwlink/?linkid=2196127 plugin_gem_not_found: |- The plugin '%{name}' could not be installed because it could not be found. Please double check the name and try again. plugin_install_license_not_found: |- The license file to install could not be found. Please verify the path you gave is correct. The path to the license file given was: '%{path}' plugin_install_failed: |- Vagrant was unable to install the requested plugin: '%{name}' Please try to install this plugin again. If Vagrant is still unable to install the requested plugin, please report this error. plugin_install_space: |- The directory where plugins are installed (the Vagrant home directory) has a space in it. On Windows, there is a bug in Ruby when compiling plugins into directories with spaces. Please move your Vagrant home directory to a path without spaces and try again. plugin_install_version_conflict: |- The plugin(s) can't be installed due to the version conflicts below. This means that the plugins depend on a library version that conflicts with other plugins or Vagrant itself, creating an impossible situation where Vagrant wouldn't be able to load the plugins. You can fix the issue by either removing a conflicting plugin or by contacting a plugin author to see if they can address the conflict. %{conflicts} plugin_init_error: |- The plugins failed to initialize correctly. This may be due to manual modifications made within the Vagrant home directory. Vagrant can attempt to automatically correct this issue by running: vagrant plugin repair If Vagrant was recently updated, this error may be due to incompatible versions of dependencies. To fix this problem please remove and re-install all plugins. Vagrant can attempt to do this automatically by running: vagrant plugin expunge --reinstall Or you may want to try updating the installed plugins to their latest versions: vagrant plugin update Error message given during initialization: %{message} plugin_load_error: |- The plugins failed to load properly. The error message given is shown below. %{message} plugin_not_installed: |- The plugin '%{name}' is not currently installed. plugin_state_file_not_parsable: |- Failed to parse the state file "%{path}": %{message} Please remove the file and reinstall the plugins. If this error recurs, please report a bug. plugin_uninstall_system: |- The plugin you're attempting to uninstall ('%{name}') is a system plugin. This means that the plugin is part of the installation of Vagrant. These plugins cannot be removed. You can however, install a plugin with the same name to replace these plugins. User-installed plugins take priority over system-installed plugins. plugin_source_error: |- Vagrant failed to load a configured plugin source. This can be caused by a variety of issues including: transient connectivity issues, proxy filtering rejecting access to a configured plugin source, or a configured plugin source not responding correctly. Please review the error message below to help resolve the issue: %{error_msg} Source: %{source} plugin_no_local_error: |- Vagrant is not currently working within a Vagrant project directory. Local plugins are only supported within a Vagrant project directory. plugin_missing_local_error: |- Vagrant is missing plugins required by the currently loaded Vagrantfile. Please install the configured plugins and run this command again. The following plugins are currently missing: %{plugins} To install the plugins configured in the current Vagrantfile run the following command: vagrant plugin install --local plugin_needs_developer_tools: |- Vagrant failed to install the requested plugin because development tools are required for installation but are not currently installed on this machine. Please install development tools and then try this command again. plugin_missing_library: |- Vagrant failed to install the requested plugin because it depends on development files for a library which is not currently installed on this system. The following library is required by the '%{name}' plugin: %{library} If a package manager is used on this system, please install the development package for the library. The name of the package will be similar to: %{library}-dev or %{library}-devel After the library and development files have been installed, please run the command again. plugin_missing_ruby_dev: |- Vagrant failed to install the requested plugin because the Ruby header files could not be found. Install the ruby development package for your system and then run this command again. powershell_not_found: |- Failed to locate the powershell executable on the available PATH. Please ensure powershell is installed and available on the local PATH, then run the command again. powershell_invalid_version: |- The version of powershell currently installed on this host is less than the required minimum version. Please upgrade the installed version of powershell to the minimum required version and run the command again. Installed version: %{installed_version} Minimum required version: %{minimum_version} pushes_not_defined: |- The Vagrantfile does not define any 'push' strategies. In order to use `vagrant push`, you must define at least one push strategy: config.push.define "ftp" do |push| # ... push-specific options end push_strategy_not_defined: |- The push strategy '%{name}' is not defined in the Vagrantfile. Defined strategy names are: %{pushes} push_strategy_not_loaded: |- There are no push strategies named '%{name}'. Please make sure you spelled it correctly. If you are using an external push strategy, you may need to install a plugin. Loaded push strategies are: %{pushes} push_strategy_not_provided: |- The Vagrantfile defines more than one 'push' strategy. Please specify a strategy. Defined strategy names are: %{pushes} package_include_symlink: |- 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. package_invalid_info: |- The information file that you've attempted to include doesn't exist or isn't a valid JSON file. Please check that the file exists and is titled 'info.json' 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_checksum_mismatch: |- The checksum of the downloaded provider '%{provider}' did not match the expected value. If the problem persists, please install the provider manually. Expected: %{expected} Received: %{actual} 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. Vagrant knows about the following providers: %{providers} provider_not_found_suggestion: |- The provider '%{provider}' could not be found, but was requested to back the machine '%{machine}'. Please use a provider that exists. Did you mean '%{suggestion}'? Vagrant knows about the following providers: %{providers} provider_not_usable: |- The provider '%{provider}' that was requested to back the machine '%{machine}' is reporting that it isn't usable on this system. The reason is shown below: %{message} provisioner_flag_invalid: |- '%{name}' is not a known provisioner. Please specify a valid provisioner. provisioner_winrm_unsupported: |- The provisioner '%{name}' doesn't support provisioning on Windows guests via WinRM. This is likely not a limitation of the provisioner itself but rather that Vagrant doesn't know how to run this provisioner over WinRM. If you'd like this provisioner to work over WinRM, please take a look at the Vagrant source code linked below and try to contribute back support. Thank you! https://github.com/hashicorp/vagrant rsync_error: |- There was an error when attempting to rsync a synced folder. Please inspect the error message below for more info. Host path: %{hostpath} Guest path: %{guestpath} Command: %{command} Error: %{stderr} rsync_post_command_error: |- There was an error while attempting to run the post rsync command for a synced folder. Please inspect the error message below for more info. Host path: %{hostpath} Guest path: %{guestpath} Error: %{message} rsync_guest_install_error: |- Installation of rsync into the guest has failed! The stdout and stderr are shown below. Please read the error output, resolve it and try again. If the problem persists, please install rsync manually within the guest. Command: %{command} Stdout: %{stdout} Stderr: %{stderr} rsync_not_found: |- "rsync" could not be found on your PATH. Make sure that rsync is properly installed on your system and available on the PATH. rsync_not_installed_in_guest: |- "rsync" was not detected as installed in your guest machine. This is required for rsync synced folders to work. In addition to this, Vagrant doesn't know how to automatically install rsync for your machine, so you must do this manually. scp_permission_denied: |- Failed to upload a file to the guest VM via SCP due to a permissions error. This is normally because the SSH user doesn't have permission to write to the destination location. Alternately, the user running Vagrant on the host machine may not have permission to read the file. Source: %{from} Dest: %{to} scp_unavailable: |- SSH server on the guest doesn't support SCP. Please install the necessary software to enable SCP on your guest operating system. shared_folder_create_failed: |- Failed to create the following shared folder on the host system. This is usually because Vagrant does not have sufficient permissions to create the folder. %{path} Please create the folder manually or specify another path to share. shell_expand_failed: |- Vagrant failed to determine the shell expansion of a guest path (probably for one of your shared folders). This is an extremely rare error case and most likely indicates an unusual configuration of the guest system. Please report a bug with your Vagrantfile and debug log. snapshot_force: |- You must include the `--force` option to replace an existing 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. snapshot_not_found: |- The snapshot name `%{snapshot_name}` was not found for the virtual machine `%{machine}`. ssh_authentication_failed: |- SSH authentication failed! This is typically caused by the public/private keypair for the SSH user not being properly set on the guest VM. Please verify that the guest VM is setup with the proper public key, and that the private key path for Vagrant is setup properly as well. ssh_bad_exit_status: |- The following SSH command responded with a non-zero exit status. Vagrant assumes that this means the command failed! %{command} Stdout from the command: %{stdout} Stderr from the command: %{stderr} ssh_bad_exit_status_muted: |- The SSH command responded with a non-zero exit status. Vagrant assumes that this means the command failed. The output for this command should be in the log above. Please read the output to determine what went wrong. ssh_channel_open_fail: |- Failed to open an SSH channel on the remote end! This typically means that the maximum number of active sessions was hit on the SSH server. Please configure your remote SSH server to resolve this issue. ssh_connect_eacces: |- SSH is getting permission denied errors when attempting to connect to the IP for SSH. This is usually caused by network rules and not being able to connect to the specified IP. Please try changing the IP on which the guest machine binds to for SSH. ssh_connection_refused: |- SSH connection was refused! This usually happens if the VM failed to boot properly. Some steps to try to fix this: First, try reloading your VM with `vagrant reload`, since a simple restart sometimes fixes things. If that doesn't work, destroy your VM and recreate it with a `vagrant destroy` followed by a `vagrant up`. If that doesn't work, contact a Vagrant maintainer (support channels listed on the website) for more assistance. ssh_connection_aborted: |- SSH connection was aborted! This usually happens when the machine is taking too long to reboot or the SSH daemon is not properly configured on the VM. First, try reloading your machine with `vagrant reload`, since a simple restart sometimes fixes things. If that doesn't work, destroy your machine and recreate it with a `vagrant destroy` followed by a `vagrant up`. If that doesn't work, contact support. ssh_connection_reset: |- SSH connection was reset! This usually happens when the machine is taking too long to reboot. First, try reloading your machine with `vagrant reload`, since a simple restart sometimes fixes things. If that doesn't work, destroy your machine and recreate it with a `vagrant destroy` followed by a `vagrant up`. If that doesn't work, contact support. ssh_connection_timeout: |- Vagrant timed out while attempting to connect via SSH. This usually means that the VM booted, but there are issues with the SSH configuration or network connectivity issues. Please try to `vagrant reload` or `vagrant up` again. ssh_disconnected: |- The SSH connection was unexpectedly closed by the remote end. This usually indicates that SSH within the guest machine was unable to properly start up. Please boot the VM in GUI mode to check whether it is booting properly. ssh_no_exit_status: |- The SSH command completed, but Vagrant did not receive an exit status. This indicates that the command was terminated unexpectedly. Please check that the VM has sufficient memory to run the command and that no processes were killed by the guest operating system. ssh_no_route: |- While attempting to connect with SSH, a "no route to host" (EHOSTUNREACH) error was received. Please verify your network settings are correct and try again. ssh_host_down: |- While attempting to connect with SSH, a "host is down" (EHOSTDOWN) error was received. Please verify your SSH settings are correct and try again. ssh_invalid_shell: |- The configured shell (config.ssh.shell) is invalid and unable to properly execute commands. The most common cause for this is using a shell that is unavailable on the system. Please verify you're using the full path to the shell and that the shell is executable by the SSH user. ssh_insert_key_unsupported: |- Vagrant is configured to generate a random keypair and insert it onto the guest machine, but it appears Vagrant doesn't know how to do this with your guest OS. Please disable key insertion by setting `config.ssh.insert_key = false` in the Vagrantfile. After doing so, run `vagrant reload` for the setting to take effect. If you'd like Vagrant to learn how to insert keys on this OS, please open an issue with details about your environment. ssh_is_putty_link: |- The `ssh` executable found in the PATH is a PuTTY Link SSH client. Vagrant is only compatible with OpenSSH SSH clients. Please install an OpenSSH SSH client or manually SSH in using your existing client using the information below. Host: %{host} Port: %{port} Username: %{username} Private key: %{key_path} ssh_key_bad_owner: |- The private key to connect to the machine via SSH must be owned by the user running Vagrant. This is a strict requirement from SSH itself. Please fix the following key to be owned by the user running Vagrant: %{key_path} ssh_key_bad_permissions: |- The private key to connect to this box via SSH has invalid permissions set on it. The permissions of the private key should be set to 0600, otherwise SSH will ignore the key. Vagrant tried to do this automatically for you but failed. Please set the permissions on the following file to 0600 and then try running this command again: %{key_path} Note that this error occurs after Vagrant automatically tries to do this for you. The likely cause of this error is a lack of filesystem permissions or even filesystem functionality. For example, if your Vagrant data is on a USB stick, a common case is that chmod is not supported. The key will need to be moved to a filesystem that supports chmod. ssh_key_type_not_supported: |- The private key you're attempting to use with this Vagrant box uses an unsupported encryption type. The SSH library Vagrant uses does not support this key type. Please use `ssh-rsa` or `ssh-dss` instead. Note that sometimes keys in your ssh-agent can interfere with this as well, so verify the keys are valid there in addition to standard file paths. ssh_key_type_not_supported_by_server: |- The private key you are attempting to generate is not supported by the guest SSH server. Please use one of the available key types defined below that is supported by the guest SSH server. Requested: %{requested_key_type} Available: %{available_key_types} ssh_not_ready: |- The provider for this Vagrant-managed machine is reporting that it is not yet ready for SSH. Depending on your provider this can carry different meanings. Make sure your machine is created and running and try again. Additionally, check the output of `vagrant status` to verify that the machine is in the state that you expect. If you continue to get this error message, please view the documentation for the provider you're using. ssh_run_requires_keys: |- Using `vagrant ssh -c` requires key-based SSH authentication, but your Vagrant environment is configured to use only password-based authentication. Please configure your Vagrantfile with a private key to use this feature. Note that Vagrant can automatically insert a keypair and use that keypair for you. Just set `config.ssh.insert_key = true` in your Vagrantfile. ssh_unavailable: "`ssh` binary could not be found. Is an SSH client installed?" ssh_unavailable_windows: |- `ssh` executable not found in any directories in the %PATH% variable. Is an SSH client installed? Try installing Cygwin, MinGW or Git, all of which contain an SSH client. Or use your favorite SSH client with the following authentication information shown below: Host: %{host} Port: %{port} Username: %{username} Private key: %{key_path} synced_folder_unusable: |- The synced folder type '%{type}' is reporting as unusable for your current setup. Please verify you have all the proper prerequisites for using this shared folder type and try again. test_key: |- test value triggers_run_fail: |- Trigger run failed triggers_guest_not_running: |- Could not run remote script on %{machine_name} because its state is %{state} triggers_guest_not_exist: |- Could not run remote script on guest because it does not exist. triggers_bad_exit_codes: |- A script exited with an unacceptable exit code %{code}. triggers_no_block_given: |- There was an error parsing the Vagrantfile: No config was given for the trigger(s) %{command}. triggers_no_stage_given: |- The incorrect stage was given to the trigger plugin: Guest: %{guest_name} Name: %{name} Type: %{type} Stage: %{stage} This is an internal error that should be reported as a bug. ui_expects_tty: |- Vagrant is attempting to interface with the UI in a way that requires a TTY. Most actions in Vagrant that require a TTY have configuration switches to disable this requirement. Please do that or run Vagrant with TTY. unimplemented_provider_action: |- Vagrant attempted to call the action '%{action}' on the provider '%{provider}', but this provider doesn't support this action. This is probably a bug in either the provider or the plugin calling this action, and should be reported. upload_invalid_compression_type: |- The compression type requested for upload (`%{type}`) is not a supported value. Try uploading again using a valid compression type. Supported types: %{valid_types} upload_missing_extract_capability: |- The guest does not provide extraction capability for the requested compression type (`%{type}`). Try a different compression type or upload without compression. upload_missing_temp_capability: |- The guest does not provide temporary path capabilities. Please try the upload again without requesting a temporary path. upload_source_missing: |- The source path provided for upload cannot be found. Please validate the source location for upload an try again. Source Path: %{source} uploader_error: |- An error occurred while uploading the file. The error message, if any, is reproduced below. Please fix this error and try again. exit code: %{exit_code} %{message} uploader_interrupted: |- The upload was interrupted by an external signal. It did not complete. vagrant_locked: |- The requested Vagrant action is locked. This may be caused by other Vagrant processes attempting to do a similar action. Lock file path: %{lock_file_path} vagrantfile_exists: |- `Vagrantfile` already exists in this directory. Remove it before running `vagrant init`. vagrantfile_load_error: |- There was an error loading a Vagrantfile. The file being loaded and the error message are shown below. This is usually caused by a syntax error. Path: %{path} Line number: %{line} Message: %{exception_class}: %{message} vagrantfile_name_error: |- There was an error loading a Vagrantfile. The file being loaded and the error message are shown below. This is usually caused by an invalid or undefined variable. Path: %{path} Line number: %{line} Message: %{message} vagrantfile_syntax_error: |- There is a syntax error in the following Vagrantfile. The syntax error message is reproduced below for convenience: %{file} vagrantfile_template_not_found_error: |- The Vagrantfile template '%{path}' does not exist. Please double check the template path and try again. vagrantfile_write_error: |- The user that is running Vagrant doesn't have the proper permissions to write a Vagrantfile to the specified location. Please ensure that you call `vagrant init` in a location where the proper permissions are in place to create a Vagrantfile. vagrant_version_bad: |- This Vagrant environment has specified that it requires the Vagrant version to satisfy the following version requirements: %{requirements} You are running Vagrant %{version}, which does not satisfy these requirements. Please change your Vagrant version or update the Vagrantfile to allow this Vagrant version. However, be warned that if the Vagrantfile has specified another version, it probably has good reason to do so, and changing that may cause the environment to not function properly. vboxmanage_error: |- There was an error while executing `VBoxManage`, a CLI used by Vagrant for controlling VirtualBox. The command and stderr is shown below. Command: %{command} Stderr: %{stderr} vboxmanage_launch_error: |- There was an error running VBoxManage. This is usually a permissions problem or installation problem with VirtualBox itself, and not Vagrant. Please note the error message below (if any), resolve the issue, and try Vagrant again. %{message} vboxmanage_not_found_error: |- The "VBoxManage" command or one of its dependencies could not be found. Please verify VirtualBox is properly installed. You can verify everything is okay by running "VBoxManage --version" and verifying that the VirtualBox version is outputted. If you're on Windows and just installed VirtualBox, you have to log out and log back in for the new environmental variables to take effect. If you're on Linux or Mac, verify your PATH contains the folder that has VBoxManage in it. virtualbox_broken_version_040214: |- Vagrant detected you have VirtualBox 4.2.14 installed. VirtualBox 4.2.14 contains a critical bug which prevents it from working with Vagrant. VirtualBox 4.2.16+ fixes this problem. Please upgrade VirtualBox. virtualbox_config_not_found: |- Vagrant was unable to locate the configuration file for the requested VirtualBox VM. Verify the requested VM exists and try again. UUID provided: %{uuid} virtualbox_disks_controller_not_found: |- Vagrant expected to find a storage controller called '%{name}', but there is no controller with this name attached to the current VM. If you have changed or removed any storage controllers, please restore them to their previous configuration. virtualbox_disks_no_supported_controllers: |- Vagrant was unable to detect a disk controller with any of the following supported types: %{supported_types} Please add one of the supported controller types in order to use the disk configuration feature. virtualbox_disks_defined_exceed_limit: |- VirtualBox only allows up to %{limit} disks to be attached to the storage controller '%{name}'. Please remove some disks from your disk configuration, or attach a new storage controller. virtualbox_disks_primary_not_found: |- Vagrant was not able to detect a primary disk attached to the main controller. Vagrant Disks requires that the primary disk be attached to the first port of the main controller in order to manage it. virtualbox_disks_unsupported_controller: |- An disk operation was attempted on the controller '%{controller_name}', but Vagrant doesn't support this type of disk controller. virtualbox_guest_property_not_found: |- Could not find a required VirtualBox guest property: %{guest_property} This is an internal error that should be reported as a bug. virtualbox_invalid_version: |- Vagrant has detected that you have a version of VirtualBox installed that is not supported by this version of Vagrant. Please install one of the supported versions listed below to use Vagrant: %{supported_versions} A Vagrant update may also be available that adds support for the version you specified. Please check www.vagrantup.com/downloads.html to download the latest version. virtualbox_kernel_module_not_loaded: |- VirtualBox is complaining that the kernel module is not loaded. Please run `VBoxManage --version` or open the VirtualBox GUI to see the error message which should contain instructions on how to fix this error. virtualbox_install_incomplete: |- VirtualBox is complaining that the installation is incomplete. Please run `VBoxManage --version` to see the error message which should contain instructions on how to fix this error. virtualbox_machine_folder_not_found: |- Vagrant failed to determine the machine folder on this host from the VirtualBox system information. Try running the command again. If this error persists, please report this error as a bug. virtualbox_mount_failed: |- Vagrant was unable to mount VirtualBox shared folders. This is usually because the filesystem "vboxsf" is not available. This filesystem is made available via the VirtualBox Guest Additions and kernel module. Please verify that these guest additions are properly installed in the guest. This is not a bug in Vagrant and is usually caused by a faulty Vagrant box. For context, the command attempted was: %{command} The error output from the command was: %{output} virtualbox_mount_not_supported_bsd: |- Vagrant is not able to mount VirtualBox shared folders on BSD-based guests. BSD-based guests do not support the VirtualBox filesystem at this time. To change the type of the default synced folder, specify the type as rsync or nfs: config.vm.synced_folder ".", "/vagrant", type: "nfs" # or "rsync" Alternatively, if you do not need to mount the default synced folder, you can also disable it entirely: config.vm.synced_folder ".", "/vagrant", disabled: true You can read more about Vagrant's synced folder types and the various configuration options on the Vagrant website. This is not a bug in Vagrant. virtualbox_name_exists: |- The name of your virtual machine couldn't be set because VirtualBox is reporting another VM with that name already exists. Most of the time, this is because of an error with VirtualBox not cleaning up properly. To fix this, verify that no VMs with that name do exist (by opening the VirtualBox GUI). If they don't, then look at the folder in the error message from VirtualBox below and remove it if there isn't any information you need in there. VirtualBox error: %{stderr} virtualbox_no_name: |- Vagrant was unable to determine the recommended name for your VirtualBox VM. This is usually an issue with VirtualBox. The output from VirtualBox is shown below, which may contain an error to fix. The best way to fix this issue is usually to uninstall VirtualBox, restart your computer, then reinstall VirtualBox. VirtualBox output: %{output} virtualbox_no_room_for_high_level_network: |- There is no available slots on the VirtualBox VM for the configured high-level network interfaces. "private_network" and "public_network" network configurations consume a single network adapter slot on the VirtualBox VM. VirtualBox limits the number of slots to 8, and it appears that every slot is in use. Please lower the number of used network adapters. virtualbox_not_detected: |- Vagrant could not detect VirtualBox! Make sure VirtualBox is properly installed. Vagrant uses the `VBoxManage` binary that ships with VirtualBox, and requires this to be available on the PATH. If VirtualBox is installed, please find the `VBoxManage` binary and add it to the PATH environmental variable. virtualbox_user_mismatch: |- The VirtualBox VM was created with a user that doesn't match the current user running Vagrant. VirtualBox requires that the same user be used to manage the VM that was created. Please re-run Vagrant with that user. This is not a Vagrant issue. The UID used to create the VM was: %{original_uid} Your UID is: %{uid} virtualbox_version_empty: |- Vagrant detected that VirtualBox appears installed on your system, but calls to detect the version are returning empty. This is often indicative of installation issues with VirtualBox. Please verify that VirtualBox is properly installed. As a final verification, please run the following command manually and verify a version is outputted: %{vboxmanage} --version virtualbox_invalid_host_subnet: |- The IP address configured for the host-only network is not within the allowed ranges. Please update the address used to be within the allowed ranges and run the command again. Address: %{address} Ranges: %{ranges} Valid ranges can be modified in the /etc/vbox/networks.conf file. For more information including valid format see: https://www.virtualbox.org/manual/ch06.html#network_hostonly vm_creation_required: |- VM must be created before running this command. Run `vagrant up` first. vm_inaccessible: |- Your VM has become "inaccessible." Unfortunately, this is a critical error with VirtualBox that Vagrant can not cleanly recover from. Please open VirtualBox and clear out your inaccessible virtual machines or find a way to fix them. vm_name_exists: |- A VirtualBox machine with the name '%{name}' already exists. Please use another name or delete the machine with the existing name, and try again. vm_no_match: |- No virtual machines matched the regular expression given. vm_not_found: |- A VM by the name of %{name} was not found. vm_not_running: |- VM must be running to open SSH connection. Run `vagrant up` to start the virtual machine. winrm_invalid_communicator: |- The winrm command requires a WinRM communicator to be used when connecting to the guest. Please update your configuration and try the command again. wsl_vagrant_version_mismatch: |- Vagrant cannot currently enable access to manage machines within the Windows environment because the version of Vagrant installed on Windows does not match this version of Vagrant running within the Windows Subsystem for Linux. Please ensure both installation of Vagrant are the same. If you do not want update your Vagrant installations you can disable Windows access by unsetting the `VAGRANT_WSL_ACCESS_WINDOWS_USER` environment variable. Windows Vagrant version: %{windows_version} Windows Subsystem for Linux Vagrant version: %{wsl_version} wsl_vagrant_access_error: |- Vagrant will not operate outside the Windows Subsystem for Linux unless explicitly instructed. Due to the inability to enforce expected Linux file ownership and permissions on the Windows system, Vagrant will not make modifications to prevent unexpected errors. To learn more about this, and the options that are available, please refer to the Vagrant documentation: https://www.vagrantup.com/docs/other/wsl.html wsl_virtualbox_windows_access: |- Vagrant is unable to use the VirtualBox provider from the Windows Subsystem for Linux without access to the Windows environment. Enabling this access must be done with caution and an understanding of the implications. For more information on enabling Windows access and using VirtualBox from the Windows Subsystem for Linux, please refer to the Vagrant documentation: https://www.vagrantup.com/docs/other/wsl.html wsl_rootfs_not_found_error: |- Vagrant is unable to determine the location of this instance of the Windows Subsystem for Linux. If this error persists it may be resolved by destroying this subsystem and installing it again. #------------------------------------------------------------------------------- # Translations for config validation errors #------------------------------------------------------------------------------- config: disk: dvd_type_file_required: A 'file' option is required when defining a disk of type ':dvd' for guest '%{machine}'. dvd_type_primary: |- Disks of type ':dvd' cannot be defined as a primary disks. Please remove the 'primary' argument for disk '%{name}' on guest '%{machine}'. invalid_ext: |- Disk type '%{ext}' is not a valid disk extension for '%{name}'. Please pick one of the following supported disk types: %{exts} invalid_type: |- Disk type '%{type}' is not a valid type. Please pick one of the following supported disk types: %{types} invalid_size: |- Config option 'size' for disk '%{name}' on guest '%{machine}' is not an integer invalid_file_type: |- Disk config option 'file' for '%{machine}' is not a string. missing_file: |- Disk file '%{file_path}' for disk '%{name}' on machine '%{machine}' does not exist. missing_provider: |- Guest '%{machine}' using provider '%{provider_name}' has provider specific config options for a provider other than '%{provider_name}'. These provider config options will be ignored for this guest no_name_set: |- A 'name' option is required when defining a disk for guest '%{machine}'. common: bad_field: "The following settings shouldn't exist: %{fields}" chef: cookbooks_path_empty: |- Missing required value for `chef.cookbooks_path'. nodes_path_empty: |- Missing required value for `chef.nodes_path'. environment_path_missing: |- Environment path not found: %{path} nodes_path_missing: |- Path specified for `nodes_path` does not exist: %{path} environment_path_required: |- When 'environment' is specified, you must provide 'environments_path'. cookbooks_path_missing: |- Cookbook path doesn't exist: %{path} custom_config_path_missing: |- Path specified for "custom_config_path" does not exist. server_url_empty: "Chef Server URL must be populated." validation_key_path: "Validation key path must be valid path to your Chef Server validation key." loader: bad_v1_key: |- Unknown configuration section '%{key}'. If this section was part of a Vagrant 1.0.x plugin, note that 1.0.x plugins are incompatible with 1.1+. root: sensitive_bad_type: |- Invalid type provided for `sensitive`. The sensitive option expects a string or an array of strings. plugins_invalid_format: |- Invalid type provided for `plugins`. plugins_bad_key: |- Invalid plugin configuration detected for `%{plugin_name}` plugin. Unknown keys: %{plugin_key} bad_key: |- Unknown configuration section '%{key}'. ssh: private_key_missing: "`private_key_path` file must exist: %{path}" paranoid_deprecated: |- The key `paranoid` is deprecated. Please use `verify_host_key`. Supported values are exactly the same, only the name of the option has changed. ssh_config_missing: "`config` file must exist: %{path}" connect_timeout_invalid_type: |- The `connect_timeout` key only accepts values of Integer type. Received `%{given}` type which cannot be converted to an Integer type. connect_timeout_invalid_value: |- The `connect_timeout` key only accepts values greater than or equal to 1 (received `%{given}`) connect_retries_invalid_type: |- The `connect_retries` key only accepts values of Integer type. Received `%{given}` type which cannot be converted to an Integer type. connect_retries_invalid_value: |- The `connect_retries` key only accepts values greater than or equal to 0 (received `%{given}`) connect_retry_delay_invalid_type: |- The `connect_retry_delay` key only accepts values of Numeric type. Received `%{given}` type which cannot be converted to a Numeric type. connect_retry_delay_invalid_value: |- The `connect_retry_delay` key only accepts values greater than or equal to 0 (received `%{given}`) connect_invalid_key_type: |- Invalid SSH key type set ('%{given}'). Supported types: %{supported} triggers: bad_trigger_type: |- The type '%{type}' defined for trigger '%{trigger}' is not valid. Must be one of the following types: '%{types}' bad_command_warning: |- The command '%{cmd}' was not found for this trigger. name_bad_type: |- Invalid type set for `name` on trigger for command '%{cmd}'. `name` should be a String. info_bad_type: |- Invalid type set for `info` on trigger for command '%{cmd}'. `info` should be a String. warn_bad_type: |- Invalid type set for `warn` on trigger for command '%{cmd}'. `warn` should be a String. on_error_bad_type: |- Invalid type set for `on_error` on trigger for command '%{cmd}'. `on_error` can only be `:halt` (default) or `:continue`. abort_bad_type: |- Trigger option `abort` for command '%{cmd}' must be either set to `true` (defaults to exit code 1) or an integer to use as an exit code. abort_false_type: |- `false` is not a valid option for the `abort` option for a trigger. This will be ignored... exit_codes_bad_type: |- Invalid type set for `exit_codes` on trigger for command '%{cmd}'. `exit_codes` can only be a single integer or an array of integers. only_on_bad_type: |- Invalid type found for `only_on`. All values must be a `String` or `Regexp`. ruby_bad_type: |- Invalid type for `ruby` option on trigger for command '%{cmd}'. Only `proc` types are allowed. privileged_ignored: |- The `privileged` setting for option `run` for trigger command '%{command}' will be ignored and set to false. powershell_args_ignored: |- The setting `powershell_args` is not supported for the trigger option `run` and will be ignored. run: bad_type: |- Invalid type set for `run` on trigger for command '%{cmd}'. `run` must be a Hash. run_remote: bad_type: |- Invalid type set for `run` on trigger for command '%{cmd}'. `run` must be a Hash. vm: bad_version: |- Invalid box version constraints: %{version} box_download_ca_cert_not_found: |- "box_download_ca_cert" file not found: %{path} box_download_ca_path_not_found: |- "box_download_ca_path" directory not found: %{path} box_download_checksum_blank: |- Checksum type specified but "box_download_checksum" is blank box_download_checksum_notblank: |- Checksum specified but must also specify "box_download_checksum_type" box_empty: "Box value for guest '%{machine_name}' is an empty string." box_missing: "A box must be specified." box_download_options_type: |- Found "box_download_options" specified as type '%{type}', should be a Hash box_download_options_not_converted: |- Something went wrong converting VM config `box_download_options`. Value for provided key '%{missing_key}' is invalid type. Should be String or Bool clone_and_box: "Only one of clone or box can be specified." config_type: "Found '%{option}' specified as type '%{given}', should be '%{required}'" hostname_invalid_characters: |- The hostname set for the VM '%{name}' should only contain letters, numbers, hyphens or dots. It cannot start with a hyphen or dot. ignore_provider_config: |- Ignoring provider config for validation... multiple_primary_disks_error: |- There are more than one primary disks defined for guest '%{name}'. Please ensure that only one disk has been defined as a primary disk. multiple_disk_files_error: |- The following disk files are used multiple times in the disk configuration for the VM '%{name}': %{disk_files} Each disk file may only be attached to a VM once. multiple_disk_names_error: |- The following disks names are defined multiple times in the disk configuration for the VM '%{name}': %{disk_names} Disk names must be unique. multiple_networks_set_hostname: "Multiple networks have set `:hostname`" name_invalid: |- The sub-VM name '%{name}' is invalid. Please don't use special characters. network_ip_ends_in_one: |- You assigned a static IP ending in ".1" or ":1" to this machine. This is very often used by the router and can cause the network to not work properly. If the network doesn't work properly, try changing this IP. network_ip_required: |- An IP is required for a private network. network_fp_invalid_port: |- Ports to forward must be 1 to 65535 network_fp_host_not_unique: |- Forwarded port '%{host}' (host port) is declared multiple times with the protocol '%{protocol}'. network_fp_requires_ports: |- Forwarded port definitions require a "host" and "guest" value network_type_invalid: |- Network type '%{type}' is invalid. Please use a valid network type. network_with_hostname_must_set_ip: "Network specified with `:hostname` must provide a static ip" provisioner_not_found: |- The '%{name}' provisioner could not be found. shared_folder_guestpath_duplicate: |- A shared folder guest path is used multiple times. Shared folders must all map to a unique guest path: %{path} shared_folder_guestpath_relative: |- The shared folder guest path must be absolute: %{path} shared_folder_hostpath_missing: |- The host path of the shared folder is missing: %{path} shared_folder_invalid_option_type: |- The type '%{type}' is not a valid synced folder type. If 'type' is not specified, Vagrant will automatically choose the best synced folder option for your guest. Otherwise, please pick from the following available options: %{options} shared_folder_nfs_owner_group: |- Shared folders that have NFS enabled do not support owner/group attributes. Host path: %{path} shared_folder_mount_options_array: |- Shared folder mount options specified by 'mount_options' must be an array of options. shared_folder_requires_guestpath_or_name: |- Shared folder options must include a guestpath and/or name. shared_folder_wsl_not_drvfs: |- The host path of the shared folder is not supported from WSL. Host path of the shared folder must be located on a file system with DrvFs type. Host path: %{path} #------------------------------------------------------------------------------- # Translations for guests #------------------------------------------------------------------------------- guests: capabilities: rebooting: |- Waiting for machine to reboot... #------------------------------------------------------------------------------- # Translations for commands. e.g. `vagrant x` #------------------------------------------------------------------------------- commands: deprecated: |- [DEPRECATION WARNING]: The Vagrant command 'vagrant %{name}' is scheduled be deprecated in an upcoming Vagrant release. common: vm_already_running: |- VirtualBox VM is already running. vm_not_created: "VM not created. Moving on..." vm_not_running: "VM is not currently running. Please, first bring it up with `vagrant up` then run this command." box: no_installed_boxes: "There are no installed boxes! Use `vagrant box add` to add some." remove_in_use_query: |- Box '%{name}' (v%{version}) with provider '%{provider}' and architectures '%{architecture}' appears to still be in use by at least one Vagrant environment. Removing the box could corrupt the environment. We recommend destroying these environments first: %{users} Are you sure you want to remove this box? [y/N] removing: |- Removing box '%{name}' (v%{version}) with provider '%{provider}'... destroy: confirmation: "Are you sure you want to destroy the '%{name}' VM? [y/N] " will_not_destroy: |- The VM '%{name}' will not be destroyed, since the confirmation was declined. warning: |- Destroying guests with `--parallel` automatically enables `--force`. Press ctrl-c to cancel. init: success: |- A `Vagrantfile` has been placed in this directory. You are now ready to `vagrant up` your first virtual environment! Please read the comments in the Vagrantfile as well as documentation on `vagrantup.com` for more information on using Vagrant. plugin: expunge_confirm: |- This command permanently deletes all currently installed user plugins. It should only be used when a repair command is unable to properly fix the system. Continue? expunge_request_reinstall: |- Would you like Vagrant to attempt to reinstall current plugins? expunge_complete: |- All user installed plugins have been removed from this Vagrant environment! expunge_reinstall: |- Vagrant will now attempt to reinstall user plugins that were removed. expunge_aborted: |- Vagrant expunge has been declined. Skipping removal of plugins. installed_license: |- The license for '%{name}' was successfully installed! installing_license: |- Installing license for '%{name}'... no_plugins: |- No plugins installed. plugin_require: " - Custom entrypoint: %{require}" plugin_version: " - Version Constraint: %{version}" installed: |- Installed the plugin '%{name} (%{version})'! installing: |- Installing the '%{name}' plugin. This can take a few minutes... uninstalling: |- Uninstalling the '%{name}' plugin... up_to_date: |- All plugins are up to date. updated: |- Updated '%{name}' to version '%{version}'! updating: |- Updating installed plugins... updating_specific: |- Updating plugins: %{names}. This may take a few minutes... post_install: |- Post install message from the '%{name}' plugin: %{message} repairing: |- Repairing currently installed global plugins. This may take a few minutes... repairing_local: |- Repairing currently installed local project plugins. This may take a few minutes... repair_complete: |- Installed plugins successfully repaired! repair_local_complete: |- Local project plugins successfully repaired! repair_failed: |- Failed to automatically repair installed Vagrant plugins. To fix this problem remove all user installed plugins and reinstall. Vagrant can do this for you automatically by running the following command: vagrant plugin expunge --reinstall Failure message received during repair: %{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. no_push_snapshot: |- No pushed snapshot found! Use `vagrant snapshot push` to push a snapshot to restore to. save: vm_not_created: |- Machine '%{name}' has not been created yet, and therefore cannot save snapshots. Skipping... status: aborted: |- The VM is in an aborted state. This means that it was abruptly stopped without properly closing the session. Run `vagrant up` to resume this virtual machine. If any problems persist, you may have to destroy and restart the virtual machine. gurumeditation: |- The VM is in the "guru meditation" state. This is a rare case which means that an internal error in VirtualBox caused the VM to fail. This is always the sign of a bug in VirtualBox. You can try to bring your VM back online with a `vagrant up`. inaccessible: |- The VM is inaccessible! This is a rare case which means that VirtualBox can't find your VM configuration. This usually happens when upgrading VirtualBox, moving to a new computer, etc. Please consult VirtualBox for how to handle this issue. output: |- Current machine states: %{states} %{message} not_created: |- The environment has not yet been created. Run `vagrant up` to create the environment. If a machine is not created, only the default provider will be shown. So if a provider is not listed, then the machine is not created for that environment. paused: |- The VM is paused. This VM may have been paused via the VirtualBox GUI or the VBoxManage command line interface. To unpause, please use the VirtualBox GUI and/or VBoxManage command line interface so that vagrant would be able to control the VM again. poweroff: |- The VM is powered off. To restart the VM, simply run `vagrant up` stopping: |- The VM is stopping. running: |- The VM is running. To stop this VM, you can run `vagrant halt` to shut it down forcefully, or you can run `vagrant suspend` to simply suspend the virtual machine. In either case, to restart it again, simply run `vagrant up`. saving: |- The VM is currently saving its state. In a few moments this state should transition to "saved." Please run `vagrant status` again in a few seconds. saved: |- To resume this VM, simply run `vagrant up`. stuck: |- The VM is "stuck!" This is a very rare state which means that VirtualBox is unable to recover the current state of the VM. The only known solution to this problem is to restart your machine, sorry. listing: |- This environment represents multiple VMs. The VMs are all listed above with their current state. For more information about a specific VM, run `vagrant status NAME`. up: upping: |- Bringing machine '%{name}' up with '%{provider}' provider... upload: compress: |- Compressing %{source} to %{type} for upload... complete: |- Upload has completed successfully! Source: %{source} Destination: %{destination} decompress: |- Decompressing %{type} upload to %{destination}... start: |- Uploading %{source} to %{destination} validate: success: |- Vagrantfile validated successfully. #------------------------------------------------------------------------------- # Translations for Vagrant middleware actions #------------------------------------------------------------------------------- cap: cleanup_disks: disk_cleanup: |- Disk '%{name}' no longer exists in Vagrant config. Removing and closing medium from guest... disk_not_found: |- Disk '%{name}' could not be found, and could not be properly removed. Please remove this disk manually if it still exists configure_disks: create_disk: |- Disk '%{name}' not found in guest. Creating and attaching disk to guest... floppy_not_supported: "Floppy disk configuration not yet supported. Skipping disk '%{name}'..." shrink_size_not_supported: |- VirtualBox does not support shrinking disk sizes. Cannot shrink '%{name}' disks size. resize_disk: |- Disk '%{name}' needs to be resized. Resizing disk... recovery_from_resize: |- Vagrant has encountered an exception while trying to resize a disk. It will now attempt to reattach the original disk, as to prevent any data loss. The original disk is located at %{location} If Vagrant fails to reattach the original disk, it is recommended that you open the VirtualBox GUI and navigate to the current guests settings for '%{name}' and look at the 'storage' section. Here is where you can reattach a missing disk if Vagrant fails to do so... recovery_attached_disks: |- Disk has been reattached. Vagrant will now continue on an raise the exception receieved start: "Configuring storage mediums..." cloud_init: content_type_not_set: |- 'content_type' must be defined for a cloud_init config. Guest '%{machine}' has not defined a 'content_type' for its cloud_init config. Valid options supported by Vagrant are listed below: %{accepted_types} incorrect_content_type: |- Content-type "%{content_type}" is not supported by Vagrant and cloud-init on machine '%{machine}'. Valid options supported by Vagrant are listed below: %{accepted_types} incorrect_inline_type: |- The inline config cloud_init option for guest '%{machine}' is of type '%{type}', but should be a String. incorrect_path_type: |- The path '%{path}' given for guest '%{machine}' is of type '%{type}'. Config option 'path' must be a String. incorrect_type_set: |- The type '%{type}' defined for a cloud_init config with guest '%{machine}' is not a valid type. Currently, Vagrant only supports type: '%{default_type}'. path_and_inline_set: |- Guest '%{machine}' defines both a 'path' and 'inline' for its cloud_init config, however only one config option should be defined for cloud_init. path_invalid: |- The path '%{path}' defined for guest '%{machine}' cloud_init config was not found on the system. actions: runner: waiting_cleanup: "Waiting for cleanup before exiting..." exit_immediately: "Exiting immediately, without cleanup!" disk: cleanup_provider_unsupported: |- Guest provider '%{provider}' does not support the cleaning up disks, and will not attempt to clean up attached disks on the guest.. provider_unsupported: |- Guest provider '%{provider}' does not support the disk feature, and will not use the disk configuration defined. vm: boot: booting: Booting VM... bridged_networking: available: |- Available bridged network interfaces: bridging: |- Bridging adapter #%{adapter} to '%{bridge}' enabling: |- Enabling bridged network... preparing: |- Preparing bridged networking... choice_help: |- When choosing an interface, it is usually the one that is being used to connect to the internet. select_interface: |- Which interface should the network bridge to? specific_not_found: |- Specific bridge '%{bridge}' not found. You may be asked to specify which network to bridge to. check_box: not_found: |- Box '%{name}' was not found. Fetching box from specified URL for the provider '%{provider}'. Note that if the URL does not have a box for this provider, you should interrupt Vagrant now and add the box yourself. Otherwise Vagrant will attempt to download the full box prior to discovering this error. check_guest_additions: not_detected: |- No guest additions were detected on the base box for this VM! Guest additions are required for forwarded ports, shared folders, host only networking, and more. If SSH fails on this machine, please install the guest additions and repackage the box to continue. This is not an error message; everything may continue to work properly, in which case you may ignore this message. version_mismatch: |- The guest additions on this VM do not match the installed version of VirtualBox! In most cases this is fine, but in rare cases it can prevent things such as shared folders from working properly. If you see shared folder errors, please make sure the guest additions within the virtual machine match the version of VirtualBox you have installed on your host and reload your VM. Guest Additions Version: %{guest_version} VirtualBox Version: %{virtualbox_version} clear_forward_ports: deleting: Clearing any previously set forwarded ports... clear_network_interfaces: deleting: Clearing any previously set network interfaces... clear_shared_folders: deleting: Cleaning previously set shared folders... 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: Cloning VM... failure: Creation of the linked clone failed. create_master: failure: |- Failed to create lock-file for master VM creation for box %{box}. cloud_init_user_data_setup: |- Preparing user data for cloud-init... customize: failure: |- A customization command failed: %{command} The following error was experienced: %{error} Please fix this customization and try again. running: Running '%{event}' VM customizations... destroy: destroying: Destroying VM and associated drives... destroy_network: destroying: Destroying unused networking interface... disable_networks: disabling: Disabling host only networks... discard_state: discarding: Discarding saved state of VM... export: create_dir: Creating temporary directory for export... exporting: Exporting VM... power_off: "The Vagrant virtual environment you are trying to package must be powered off." forward_ports: auto_empty: |- Vagrant found a port collision for the specified port and virtual machine. While this port was marked to be auto-corrected, the ports in the auto-correction range are all also used. VM: %{vm_name} Forwarded port: %{guest_port} => %{host_port} collision_error: |- Vagrant cannot forward the specified ports on this VM, since they would collide with some other application that is already listening on these ports. The forwarded port to %{host_port} is already in use on the host machine. To fix this, modify your current project's Vagrantfile to use another port. Example, where '1234' would be replaced by a unique host port: config.vm.network :forwarded_port, guest: %{guest_port}, host: 1234 Sometimes, Vagrant will attempt to auto-correct this for you. In this case, Vagrant was unable to. This is usually because the guest machine is in a state which doesn't allow modifying port forwarding. You could try 'vagrant reload' (equivalent of running a halt followed by an up) so vagrant can attempt to auto-correct this upon booting. Be warned that any unsaved work might be lost. fixed_collision: |- Fixed port collision for %{guest_port} => %{host_port}. Now on port %{new_port}. forwarding: Forwarding ports... forwarding_entry: |- %{guest_port} (guest) => %{host_port} (host) (adapter %{adapter}) host_ip_not_found: |- You are trying to forward a host IP that does not exist. Please set `host_ip` to the address of an existing IPv4 network interface, or remove the option from your port forward configuration. VM: %{name} Host IP: %{host_ip} non_nat: |- VirtualBox adapter #%{adapter} not configured as "NAT". Skipping port forwards on this adapter. privileged_ports: |- You are trying to forward to privileged ports (ports <= 1024). Most operating systems restrict this to only privileged process (typically processes running as an administrative user). This is a warning in case the port forwarding doesn't work. If any problems occur, please try a port higher than 1024. halt: force: |- Forcing shutdown of VM... graceful: |- Attempting graceful shutdown of VM... guest_not_ready: |- Guest communication could not be established! This is usually because SSH is not running, the authentication information was changed, or some other networking issue. Vagrant will force halt, if capable. hostname: setting: "Setting hostname..." import: importing: Importing base box '%{name}'... failure: |- The VM import failed! Try running `VBoxManage import` on the box file manually for more verbose error output. match_mac: matching: Matching MAC address for NAT networking... generating: Generating MAC address for NAT networking... no_base_mac: |- No base MAC address was specified. This is required for the NAT networking to work properly (and hence port forwarding, SSH, etc.). Specifying this MAC address is typically up to the box and box maintainer. Please contact the relevant person to solve this issue. network: configuring: |- Configuring and enabling network interfaces... dhcp_already_attached: |- A host only network interface you're attempting to configure via DHCP already has a conflicting host only adapter with DHCP enabled. The DHCP on this adapter is incompatible with the DHCP settings. Two host only network interfaces are not allowed to overlap, and each host only network interface can have only one DHCP server. Please reconfigure your host only network or remove the virtual machine using the other host only network. preparing: |- Preparing network interfaces based on configuration... cleanup_vbox_default_dhcp: |- Found default DHCP server from initial VirtualBox install. Cleaning it up... host_only_network: collides: |- The specified host network collides with a non-hostonly network! This will cause your specified IP to be inaccessible. Please change the IP or name of your host only network so that it no longer matches that of a bridged or non-hostonly network. Bridged Network Address: '%{netaddr}' Host-only Network '%{interface_name}': '%{that_netaddr}' creating: "Creating new host only network for environment..." enabling: "Enabling host only network..." not_found: |- The specified host network could not be found: '%{name}.' If the name specification is removed, Vagrant will create a new host only network for you. Alternatively, please create the specified network manually. preparing: "Preparing host only network..." nfs: exporting: Exporting NFS shared folders... installing: "Installing NFS client..." mounting: Mounting NFS shared folders... mounting_entry: "%{hostpath} => %{guestpath}" v4_with_udp_warning: |- WARNING: Invalid NFS settings detected! Detected UDP enabled with NFSv4. NFSv4 does not support UDP. If folder mount fails disable UDP using the following option: nfs_udp: false For more information see: RFC5661: https://tools.ietf.org/html/rfc5661#section-2.9.1 RFC7530: https://tools.ietf.org/html/rfc7530#section-3.1 smb: mfsymlink_warning: |- Vagrant is currently configured to mount SMB folders with the `mfsymlink` option enabled. This is equivalent to adding the following to your Vagrantfile: config.vm.synced_folder '/host/path', '/guest/path', type: "smb", mount_options: ['mfsymlink'] This option may be globally disabled with an environment variable: VAGRANT_DISABLE_SMBMFSYMLINKS=1 provision: beginning: "Running provisioner: %{provisioner}..." disabled_by_config: |- Machine not provisioned because `--no-provision` is specified. disabled_by_sentinel: |- Machine already provisioned. Run `vagrant provision` or use the `--provision` flag to force provisioning. Provisioners marked to run always will still run. file: locations: "%{src} => %{dst}" resume: resuming: Resuming suspended VM... unpausing: |- Unpausing the VM... share_folders: creating: Creating shared folders metadata... mounting: Mounting shared folders... mounting_entry: "%{hostpath} => %{guestpath}" nomount_entry: "Automounting disabled: %{hostpath}" set_default_nic_type: e1000_warning: |- Vagrant has detected a configuration issue which exposes a vulnerability with the installed version of VirtualBox. The current guest is configured to use an E1000 NIC type for a network adapter which is vulnerable in this version of VirtualBox. Ensure the guest is trusted to use this configuration or update the NIC type using one of the methods below: https://www.vagrantup.com/docs/virtualbox/configuration.html#default-nic-type https://www.vagrantup.com/docs/virtualbox/networking.html#virtualbox-nic-type set_name: setting_name: |- Setting the name of the VM: %{name} snapshot: deleting: |- 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: |- 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... box: unpackage: untar_failure: |- The box failed to unpackage properly. Please verify that the box file you're trying to add is not corrupted and that enough disk space is available and then try again. The output from attempting to unpackage (if any): %{output} add: adding: |- Extracting box... checksumming: |- Calculating and comparing box checksum... destroy: destroying: "Deleting box '%{name}'..." download: cleaning: "Cleaning up downloaded box..." download_failed: |- Download failed. Will try another box URL if there is one. interrupted: "Box download was interrupted. Exiting." resuming: "Box download is resuming from prior download progress" verify: verifying: "Verifying box..." failed: |- The box file you're attempting to add is invalid. This can be commonly attributed to typos in the path given to the box add command. Another common case of this is invalid packaging of the box itself. general: package: packaging: "Packaging additional file: %{file}" compressing: "Compressing package to: %{fullpath}" box_folder: "Creating new folder: %{folder_path}" output_exists: |- The specified file '%{filename}' to save the package as already exists. Please remove this file or specify a different file name for outputting. output_is_directory: |- The specified output is a directory. Please specify a path including a filename. requires_directory: |- A directory was not specified to package. This should never happen and is a result of an internal inconsistency. Please report a bug on the Vagrant bug tracker. include_file_missing: |- Package include file doesn't exist: %{file} hosts: bsd: nfs_export: |- 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... nfs_prune: |- Pruning invalid NFS exports. Administrator privileges will be required... 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: base: both_before_after_set: |- Dependency provisioners cannot currently set both `before` and `after` options. dependency_provisioner_dependency: |- Dependency provisioner "%{name}" relies on another dependency provisioner "%{dep_name}". This is currently not supported. invalid_alias_value: |- Provisioner option `%{opt}` is not set as a valid type. Must be a string, or one of the alias shortcuts: %{alias} missing_provisioner_name: |- Could not find provisioner name `%{name}` defined for machine `%{machine_name}` to run provisioner "%{provisioner_name}" `%{action}`. wrong_type: |- Provisioner option `%{opt}` is not set as a valid type. Must be a %{type}. chef: chef_not_detected: |- The chef binary (either `chef-solo` or `chef-client`) was not found on the VM and is required for chef provisioning. Please verify that chef is installed and that the binary is available on the PATH. cookbooks_folder_not_found_warning: "The cookbook path '%{path}' doesn't exist. Ignoring..." nodes_folder_not_found_warning: "The node path '%{path}' doesn't exist. Ignoring..." data_bags_folder_not_found_warning: "The databag path '%{path}' doesn't exist. Ignoring..." roles_folder_not_found_warning: "The role path '%{path}' doesn't exist. Ignoring..." environments_folder_not_found_warning: "The environment path '%{path}' doesn't exist. Ignoring..." json: "Generating chef JSON and uploading..." client_key_folder: "Creating folder to hold client key..." generating_node_name: |- Auto-generating node name for Chef... using_hostname_node_name: |- Using hostname "%{hostname}" as node name for Chef... install_failed: |- Vagrant could not detect Chef on the guest! Even after Vagrant attempted to install Chef, it could still not find Chef on the system. Please make sure you are connected to the Internet and can access Chef's package distribution servers. If you already have Chef installed on this guest, you can disable the automatic Chef detection by setting the 'install' option in the Chef configuration section of your Vagrantfile: chef.install = false log_level_empty: |- The Chef provisioner requires a log level. If you did not set a log level, this is probably a bug and should be reported. upload_validation_key: "Uploading chef client validation key..." upload_encrypted_data_bag_secret_key: "Uploading chef encrypted data bag secret key..." recipe_empty: |- Chef Apply provisioning requires that the `config.chef.recipe` be set to a string containing the recipe contents you want to execute on the guest. running_client: "Running chef-client..." running_client_again: "Running chef-client again (failed to converge)..." running_apply: "Running chef-apply..." running_solo: "Running chef-solo..." running_solo_again: "Running chef-solo again (failed to converge)..." running_zero: "Running chef-client (local-mode)..." running_zero_again: "Running chef-client (local-mode) again (failed to converge)..." missing_shared_folders: |- Shared folders that Chef requires are missing on the virtual machine. This is usually due to configuration changing after already booting the machine. The fix is to run a `vagrant reload` so that the proper shared folders will be prepared and mounted on the VM. no_convergence: |- Chef never successfully completed! Any errors should be visible in the output above. Please fix your recipes so that they properly complete. not_detected: |- The `%{binary}` binary appears not to be in the PATH of the guest. This could be because the PATH is not properly setup or perhaps chef is not installed on this guest. Chef provisioning can not continue without chef properly installed. server_url_required: |- Chef server provisioning requires that the `config.chef.chef_server_url` be set to the URL of your chef server. Examples include "http://12.12.12.12:4000" and "http://example.com:4000" (the port of course can be different, but 4000 is the default) server_validation_key_required: |- Chef server provisioning requires that the `config.chef.validation_key_path` configuration be set to a path on your local machine of the validation key used to register the VM with the chef server. server_validation_key_doesnt_exist: |- The validation key set for `config.chef.validation_key_path` does not exist! This file needs to exist so it can be uploaded to the virtual machine. upload_path_empty: |- The Chef Apply provisioner requires that the `config.chef.upload_path` be set to a non-nil, non-empty value. missing_node_name: |- The Chef Client provisioner cannot delete the %{deletable} from the Chef server because Vagrant does not know the `node_name'! You need to manually delete the %{deletable} from the Chef server. You can specify the `node_name' in the Chef configuration to prevent this in the future. deleting_from_server: "Deleting %{deletable} \"%{name}\" from Chef server..." file: no_dest_file: "File destination must be specified." no_source_file: "File source must be specified." path_invalid: "File upload source file %{path} must exist" puppet: not_detected: |- The `%{binary}` 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. running_puppet: "Running Puppet with %{manifest}..." running_puppet_env: "Running Puppet with environment %{environment}..." manifest_missing: |- The configured Puppet manifest is missing. Please specify a path to an existing manifest: %{manifest} environment_missing: |- The configured Puppet environment folder %{environment} was not found in the specified environmentpath %{environmentpath}. Please specify a path to an existing Puppet directory environment. environment_path_missing: "The environment path specified for Puppet does not exist: %{path}" manifests_path_missing: "The manifests path specified for Puppet does not exist: %{path}" missing_shared_folders: |- Shared folders that Puppet requires are missing on the virtual machine. This is usually due to configuration changing after already booting the machine. The fix is to run a `vagrant reload` so that the proper shared folders will be prepared and mounted on the VM. module_path_missing: "The configured module path doesn't exist: %{path}" puppet_server: cert_requires_node: |- "puppet_node" is required when a client cert or key is specified client_cert_and_private_key: |- Both a client certificate and private key must be specified, if any client_cert_not_found: |- The specified client cert path could not be found client_private_key_not_found: |- The specified client private key path could not be found not_detected: |- The `%{binary}` 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. running_puppetd: "Running Puppet agent..." uploading_client_cert: |- Uploading client certificate and private key... shell: args_bad_type: "Shell provisioner `args` must be a string or array." invalid_encoding: |- Invalid encoding '%{actual}' for script at '%{path}'. Must be '%{default}' or UTF-8. env_must_be_a_hash: |- The shell provisioner's `env' option must be a hash. no_path_or_inline: "One of `path` or `inline` must be set." path_and_inline_set: "Only one of `path` or `inline` may be set." path_invalid: "`path` for shell provisioner does not exist on the host system: %{path}" 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: ansible_host_pattern_detected: |- Vagrant has detected a host range pattern in the `groups` option. Vagrant doesn't fully check the validity of these parameters! Please check https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#inventory-basics-formats-hosts-and-groups for more information. 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. If you'd like this provisioner to be improved, please take a look at the Vagrant source code linked below and try to contribute back support. Thank you! https://github.com/hashicorp/vagrant errors: ansible_command_failed: |- Ansible failed to complete successfully. Any error output should be visible above. Please fix these errors and try again. ansible_compatibility_mode_conflict: |- The requested Ansible compatibility mode (%{compatibility_mode}) is in conflict with the Ansible installation on your Vagrant %{system} system (currently: %{ansible_version}). See https://docs.vagrantup.com/v2/provisioning/ansible_common.html#compatibility_mode for more information. ansible_not_found_on_guest: |- The Ansible software could not be found! Please verify that Ansible is correctly installed on your guest system. If you haven't installed Ansible yet, please install Ansible on your Vagrant basebox, or enable the automated setup with the ansible_local provisioner `install` option. Please check https://docs.vagrantup.com/v2/provisioning/ansible_local.html#install for more information. ansible_not_found_on_host: |- The Ansible software could not be found! Please verify 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 https://docs.ansible.com for more information. ansible_programming_error: |- Ansible Provisioner Programming Error: %{message} Internal Details: %{details} Sorry, but this Vagrant error should never occur. Please check https://github.com/hashicorp/vagrant/issues for any existing bug report. If needed, please create a new issue. Thank you! cannot_support_pip_install: |- Unfortunately Vagrant does not support yet installing Ansible from pip for the guest OS running in the machine. If you'd like this provisioner to be improved, please take a look at the Vagrant source code linked below and try to contribute back support. Thank you! https://github.com/hashicorp/vagrant ansible_version_mismatch: |- The requested Ansible version (%{required_version}) was not found on the %{system}. Please check the Ansible installation on your Vagrant %{system} system (currently: %{current_version}), or adapt the provisioner `version` option in your Vagrantfile. See https://docs.vagrantup.com/v2/provisioning/ansible_common.html#version for more information. config_file_not_found: |- `%{config_option}` does not exist on the %{system}: %{path} extra_vars_invalid: |- `extra_vars` must be a hash or a path to an existing file. Received: %{value} (as %{type}) no_compatibility_mode: |- `compatibility_mode` must be a valid mode (possible values: %{valid_modes}). no_playbook: |- `playbook` file path must be set. raw_arguments_invalid: |- `raw_arguments` must be an array of strings. Received: %{value} (as %{type}) raw_ssh_args_invalid: |- `raw_ssh_args` must be an array of strings. Received: %{value} (as %{type}) installing: "Installing Ansible..." installing_pip: "Installing pip... (for Ansible installation)" running_galaxy: "Running ansible-galaxy..." running_playbook: "Running ansible-playbook..." windows_not_supported_for_control_machine: |- Windows is not officially supported for the Ansible Control Machine. Please check https://docs.ansible.com/intro_installation.html#control-machine-requirements compatibility_mode_not_detected: |- Vagrant gathered an unknown Ansible version: %{gathered_version} and falls back on the compatibility mode '%{compatibility_mode}'. Alternatively, the compatibility mode can be specified in your Vagrantfile: https://www.vagrantup.com/docs/provisioning/ansible_common.html#compatibility_mode compatibility_mode_warning: |- Vagrant has automatically selected the compatibility mode '%{compatibility_mode}' according to the Ansible version installed (%{ansible_version}). Alternatively, the compatibility mode can be specified in your Vagrantfile: https://www.vagrantup.com/docs/provisioning/ansible_common.html#compatibility_mode docker: wrong_provisioner: |- The Docker post-install provisioner cannot also take a Docker post-install provisioner not_running: "Docker is not running on the guest VM." install_failed: "Docker installation failed." podman: wrong_provisioner: |- The Podman post-install provisioner cannot also take a Podman post-install provisioner install_failed: "Podman installation failed." salt: minion_config_nonexist: |- The specified minion_config '%{missing_config_file}' file could not be found. master_config_nonexist: |- The specified master_config '%{missing_config_file}' file could not be found. grains_config_nonexist: |- The specified grains_config file could not be found. missing_key: |- You must include both public and private keys. must_accept_keys: |- You must accept keys when running highstate with master! args_array: |- You must set `args_array` value as an array. python_version: |- You must set `python_version` as an integer or string that represents an integer. version_type_missing: |- You must set the option `install_type` when specifying a `version`. salt_invalid_shasum_error: |- The bootstrap-salt script downloaded from '%{source}' couldn't be verified. Expected SHA256 '%{expected_sha}', but computed '%{computed_sha}' pushes: file: no_destination: "File destination must be specified." autocomplete: installed: |- Autocomplete installed at paths: - %{paths} not_installed: "Autocomplete not installed" ================================================ FILE: templates/locales/guest_windows.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: vagrant_windows: errors: public_key_directory_failure: |- Vagrant failed to properly discover the correct paths for the temporary directory and user profile directory on the Windows guest. Please ensure the guest is properly configured. network_winrm_required: |- Configuring networks on Windows requires the communicator to be set to WinRM. To do this, add the following to your Vagrantfile: config.vm.communicator = "winrm" Note that the Windows guest must be configured to accept insecure WinRM connections, and the WinRM port must be forwarded properly from the guest machine. This is not always done by default. rename_computer_failed: |- Renaming the Windows guest failed. Most often this is because you've specified a FQDN instead of just a host name. Ensure the new guest name is properly formatted. Standard names may contain letters (a-z, A-Z), numbers (0-9), and hyphens (-), but no spaces or periods (.). The name may not consist entirely of digits. ================================================ FILE: templates/locales/providers_docker.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: docker_provider: already_built: |- Image is already built from the Dockerfile. `vagrant reload` to rebuild. build_image_destroy: |- Removing built image... build_image_destroy_in_use: |- Build image couldn't be destroyed because the image is in use. The image must be destroyed manually in the future if you want to remove it. build_image_invalid: |- Build image no longer exists. Rebuilding... building: |- Building the container from a Dockerfile... building_git_repo: |- Building the container from the git repository: %{repo}... building_named_dockerfile: |- Building the container from the named Dockerfile: %{file}... building_git_repo_named_dockerfile: |- Building the container from the named Dockerfile: %{file} in the git repository: %{repo}... creating: |- Creating the container... created: |- Container created: %{id} host_machine_disabling_folders: |- Removing synced folders... host_machine_forwarded_ports: |- Warning: When using a remote Docker host, forwarded ports will NOT be immediately available on your machine. They will still be forwarded on the remote machine, however, so if you have a way to access the remote machine, then you should be able to access those ports there. This is not an error, it is only an informational message. host_machine_needed: |- Docker host is required. One will be created if necessary... host_machine_ready: |- Docker host VM is already ready. host_machine_starting: |- Vagrant will now create or start a local VM to act as the Docker host. You'll see the output of the `vagrant up` for this VM below. host_machine_syncing_folders: |- Syncing folders to the host VM... logging_in: |- Logging in to Docker server... logs_host_state_unknown: |- This container requires a host VM, and the state of that VM is unknown. Run `vagrant up` to verify that the container and its host VM is running, then try again. network_bridge_gateway_invalid: |- The provided gateway IP address is invalid (%{gateway}). Please provide a valid IP address. network_bridge_gateway_outofbounds: |- The provided gateway IP (%{gateway}) is not within the defined subnet (%{subnet}). Please provide an IP address within the defined subnet. network_bridge_gateway_request: |- Gateway IP address for %{interface} interface [%{default_gateway}]: network_bridge_iprange_info: |- When an explicit address is not provided to a container attached to this bridged network, docker will supply an address to the container. This is independent of the local DHCP service that may be available on the network. network_bridge_iprange_invalid: |- The provided IP address range is invalid (%{range}). Please provide a valid range. network_bridge_iprange_outofbounds: |- The provided IP address range (%{range}) is not within the defined subnet (%{subnet}). Please provide an address range within the defined subnet. network_bridge_iprange_request: |- Available address range for assignment on %{interface} interface [%{default_range}]: network_create: |- Creating and configuring docker networks... network_connect: |- Enabling network interfaces... network_destroy: |- Removing network %{network_name} ... not_created_skip: |- Container not created. Skipping. not_docker_provider: |- Not backed by Docker provider. Skipping. pull: |- Pulling image '%{image}'... run_command_required: |- `vagrant docker-run` requires a command to execute. This command must be specified after a `--` in the command line. This is used to separate possible machine names and options from the actual command to execute. An example is shown below: vagrant docker-run web -- rails new . running: |- Container is starting. Output will stream in below... running_detached: |- Container is started detached. ssh_through_host_vm: |- SSH will be proxied through the Docker virtual machine since we're not running Docker natively. This is just a notice, and not an error. subnet_exists: |- A network called '%{network_name}' using subnet '%{subnet}' is already in use. Using '%{network_name}' instead of creating a new network... synced_folders_changed: |- Vagrant has noticed that the synced folder definitions have changed. With Docker, these synced folder changes won't take effect until you destroy the container and recreate it. waiting_for_running: |- Waiting for container to enter "running" state... volume_path_not_expanded: |- Host path `%{host}` exists as a `volumes` key and is a folder on disk. Vagrant will not expand this key like it used to and instead leave it as is defined. If this folder is intended to be used, make sure its full path is defined in your `volumes` config. More information can be found on volumes in the docker compose documentation. messages: destroying: |- Deleting the container... not_created: |- The container hasn't been created yet. not_created_original: |- The original container hasn't been created yet. Run `vagrant up` for this machine first. not_running: |- The container is not currently running. preparing: |- Preparing to start the container... provision_no_ssh: |- Provisioners will not be run since container doesn't support SSH. will_not_destroy: |- The container will not be destroyed, since the confirmation was declined. starting: |- Starting container... stopping: |- Stopping container... container_ready: |- Container started and ready for use! not_provisioning: |- The following provisioners require a communicator, though none is available (this container does not support SSH). Not running the following provisioners: - %{provisioners} status: host_state_unknown: |- The host VM for the Docker containers appears to not be running or is currently inaccessible. Because of this, we can't determine the state of the containers on that host. Run `vagrant up` to bring up the host VM again. not_created: |- The environment has not yet been created. Run `vagrant up` to create the environment. If a machine is not created, only the default provider will be shown. So if a provider is not listed, then the machine is not created for that environment. preparing: |- Vagrant is preparing to start this Docker container. Run `vagrant up` to continue. running: |- The container is created and running. You can stop it using `vagrant halt`, see logs with `vagrant docker-logs`, and kill/destroy it with `vagrant destroy`. stopped: |- The container is created but not running. You can run it again with `vagrant up`. If the container always goes to "stopped" right away after being started, it is because the command being run exits and doesn't keep running. errors: build_error: |- Vagrant received unknown output from `docker build` while building a container: %{result} compose_lock_timeout: |- Vagrant encountered a timeout waiting for the docker compose driver to become available. Please try to run your command again. If you continue to experience this error it may be resolved by disabling parallel execution. docker_compose_not_installed: |- Vagrant has been instructed to use to use the Compose driver for the Docker plugin but was unable to locate the `docker-compose` executable. Ensure that `docker-compose` is installed and available on the PATH. not_created: |- The container hasn't been created yet. not_running: |- The container is not currently running. communicator_non_docker: |- The "docker_hostvm" communicator was specified on a machine that is not provided by the Docker provider. This is a bug with your Vagrantfile. Please contact the creator of your Vagrant environment and notify them to not use this communicator for anything except the "docker" provider. config: both_build_and_image_and_git: |- Only one of "build_dir", "git_repo" or "image" can be set build_dir_invalid: |- "build_dir" must exist and contain a Dockerfile git_repo_invalid: |- "git_repo" must be a valid repository URL build_dir_or_image: |- One of "build_dir", "git_repo" or "image" must be set compose_configuration_hash: |- "compose_configuration" must be a hash compose_force_vm: |- Docker compose is not currently supported from within proxy VM. git_repo_invalid: |- "git_repo" must be a valid git URL create_args_array: |- "create_args" must be an array invalid_link: |- Invalid link (should be 'name:alias'): "%{link}" invalid_vagrantfile: |- "vagrant_vagrantfile" must point to a Vagrantfile that exists. docker_provider_nfs_without_privileged: |- You've configured a NFS synced folder but didn't enable privileged mode for the container. Please set the `privileged` option to true on the provider block from your Vagrantfile, recreate the container and try again. docker_provider_image_not_configured: |- The base Docker image has not been set for the '%{name}' VM! execute_error: |- A Docker command executed by Vagrant didn't complete successfully! The command run along with the output from the command is shown below. Command: %{command} Stderr: %{stderr} Stdout: %{stdout} exec_command_required: |- The "docker-exec" command requires a command to execute. This command must be specified after a "--" in the command line. This is used to separate machine name and options from the actual command to execute. An example is show below: $ vagrant docker-exec -t nginx -- bash host_vm_communicator_not_ready: |- The Docker provider was able to bring up the host VM successfully but the host VM is still reporting that SSH is unavailable. This sometimes happens with certain providers due to bugs in the underlying hypervisor, and can be fixed with a `vagrant reload`. The ID for the host VM is shown below for convenience. If this does not fix it, please verify that the host VM provider is functional and properly configured. Host VM ID: %{id} network_address_invalid: |- The configured network address is not valid within the configured subnet of the defined network. Please update the network settings and try again. Configured address: %{address} Network name: %{network_name} network_address_required: |- An IP address is required if not using `type: "dhcp"` or not specifying a `subnet`. network_invalid_option: |- Invalid option given for docker network for guest "%{container}". Must specify either a `subnet` or use `type: "dhcp"`. network_name_missing: |- The Docker provider is unable to connect the container to the defined network due to a missing network name. Please validate your configuration and try again. Container: %{container} Network Number: %{index} network_name_undefined: |- The Docker provider was unable to configure networking using the provided network name `%{network_name}`. Please ensure the network name is correct and exists, then try again. network_no_interfaces: |- The Docker provider was unable to list any available interfaces to bridge the public network with. network_subnet_invalid: |- The configured network subnet is not valid for the defined network. Please update the network settings and try again. Configured subnet: %{subnet} Network name: %{network_name} package_not_supported: |- The "package" command is not supported with the Docker provider. If you'd like to commit or push your Docker container, please SSH into the host VM (if there is one), and run `docker commit` and so on manually. state_not_running: |- The container never entered the "running" state, or entered it briefly but reverted back to another state. Please verify that the configuration of the container is correct. If you meant for this container to not remain running, please set the Docker provider configuration "remains_running" to "false": config.vm.provider "docker" do |d| d.remains_running = false end state_stopped: |- The container started either never left the "stopped" state or very quickly reverted to the "stopped" state. This is usually because the container didn't execute a command that kept it running, and usually indicates a misconfiguration. If you meant for this container to not remain running, please set the Docker provider configuration "remains_running" to "false": config.vm.provider "docker" do |d| d.remains_running = false end suspend_not_supported: |- The "suspend" command is not supported with the Docker provider. Docker containers don't natively support suspend. If you're using a host machine, you can suspend the host machine by finding it in `vagrant global-status` and using `vagrant suspend `. synced_folder_non_docker: |- The "docker" synced folder type can't be used because the provider in use is not Docker. This synced folder type only works with the Docker provider. The provider this machine is using is: %{provider} vagrantfile_not_found: |- The configured host VM Vagrantfile could not be found. Please fix the Vagrantfile for this Docker environment to point to a valid host VM. ================================================ FILE: templates/locales/providers_hyperv.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: vagrant_hyperv: choose_switch: |- Please choose a switch to attach to your Hyper-V instance. If none of these are appropriate, please open the Hyper-V manager to create a new virtual switch. message_already_running: |- Hyper-V instance already running. message_not_created: |- VM not created. Moving on... message_not_running: |- Hyper-V machine isn't running. Can't SSH in! config: invalid_auto_start_action: |- The requested auto start action for the Hyper-V VM is not a valid action. Please provide a valid action and run the command again. Received: %{action} Allowed: %{allowed_actions} invalid_auto_stop_action: |- The requested auto stop action for the Hyper-V VM is not a valid action. Please provide a valid action and run the command again. Received: %{action} Allowed: %{allowed_actions} invalid_integration_services_type: |- Invalid type provided for `vm_integration_services`. Type received is `%{received}` but `Hash` was expected. invalid_integration_services_entry: |- The `%{entry_name}` entry in the `vm_integration_services` is set to an unexpected value. Received: %{entry_value} Allowed: true, false differencing_disk_deprecation: |- The `differencing_disk` configuration option is deprecated and should no longer be used. The `linked_clone` configuration option should be used instead. errors: admin_required: |- The Hyper-V provider requires that Vagrant be run with administrative privileges. This is a limitation of Hyper-V itself. Hyper-V requires administrative privileges for management commands. Please restart your console with administrative privileges and try again. box_invalid: |- The box you're using with the Hyper-V provider ('%{name}') is invalid. A Hyper-V box should contain both a "Virtual Machines" and a "Virtual Hard Disks" folder that are created as part of exporting a Hyper-V machine. Within these directories, Vagrant expects to find the virtual machine configuration as well as the root hard disk. The box you're attempting to use is missing one or both of these directories or does not contain the files expected. Verify that you added the correct box. If this problem persists, please contact the creator of the box for assistance. ip_addr_timeout: |- Hyper-V failed to determine your machine's IP address within the configured timeout. Please verify the machine properly booted and the network works. To do this, open the Hyper-V manager, find your virtual machine, and connect to it. The most common cause for this error is that the running virtual machine doesn't have the latest Hyper-V integration drivers. Please research for your operating system how to install these in order for the VM to properly communicate its IP address to Hyper-V. no_switches: |- There are no virtual switches created for Hyper-V! Please open the Hyper-V Manager, go to the "Virtual Switch Manager", and create at least one virtual switch. A virtual switch is required for Vagrant to create a Hyper-V machine that is connected to a network so it can access it. For more help, please see the documentation on the Vagrant website for Hyper-V. powershell_features_disabled: |- The Hyper-V cmdlets for PowerShell are not available! Vagrant requires these to control Hyper-V. Please enable them in the "Windows Features" control panel and try again. 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} powershell_required: |- The Vagrant Hyper-V provider requires PowerShell to be available. Please make sure "powershell.exe" is available on your PATH. windows_required: |- The Hyper-V provider only works on Windows. Please try to use another provider. system_access_required: |- Hyper-V access check has failed for the configured destination. This is usually caused by running on a non-system drive which is missing required permissions. Running the following command may resolve the problem: icacls.exe %{root_dir} /T /Q /grant "NT AUTHORITY\SYSTEM:(IO)(CI)(F)" ================================================ FILE: templates/locales/synced_folder_smb.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 en: vagrant_sf_smb: mounting: |- Mounting SMB shared folders... mounting_single: |- %{host} => %{guest} preparing: |- Preparing SMB shared folders... warning_password: |- You will be asked for the username and password to use for the SMB folders shortly. Please use the proper username/password of your account. incorrect_credentials: |- Credentials incorrect. Please try again. uac: prune_warning: |- Vagrant requires administrator access for pruning SMB shares and may request access to complete removal of stale shares. create_warning: |- Vagrant requires administrator access to create SMB shares and may request access to complete setup of configured shares. errors: not_supported: |- It appears your machine doesn't support SMB, has not been properly configured for SMB, or there is not an adapter to enable SMB on this machine for Vagrant. Ensure SMB host functionality is available on this machine and try again. start_failed: |- Vagrant failed to automatically start the SMB service. Ensure the required services can be started and try again. Command: %{command} Stderr: %{stderr} Stdout: %{stdout} credentials_missing: |- Vagrant SMB synced folders require the account password to be stored in an NT compatible format. Please update your sharing settings to enable a Windows compatible password and try again. credentials_request_error: |- Vagrant failed to receive credential information required for preparing an SMB share. define_share_failed: |- Exporting an SMB share failed! Details about the failure are shown below. Please inspect the error message and correct any problems. Host path: %{host} Stderr: %{stderr} Stdout: %{stdout} prune_share_failed: |- Pruning an SMB share failed! Details about the failure are shown below. Please inspect the error message and correct any problems. Share name: %{name} Stderr: %{stderr} Stdout: %{stdout} name_error: |- Vagrant is unable to setup a requested SMB share. An SMB share already exists with the given name. Share name: %{name} Current path: %{existing_path} Requested path: %{path} list_failed: |- Vagrant failed to generate a list of local SMB shares. Please try running the command again. no_routable_host_addr: |- We couldn't detect an IP address that was routable to this machine from the guest machine! Please verify networking is properly setup in the guest machine and that it is able to access this host. As another option, you can manually specify an IP for the machine to mount from using the `smb_host` option to the synced folder. powershell_version: |- PowerShell version 3 or later is required for SMB synced folders to work on Windows. You have version: '%{version}'. Please update your PowerShell installation. windows_admin_required: |- SMB shared folders require running Vagrant with administrative privileges. This is a limitation of Windows, since creating new network shares requires admin privileges. Please try again in a console with proper permissions or use another synced folder type. windows_host_required: |- SMB shared folders are only available when Vagrant is running on Windows. The guest machine can be running non-Windows. Please use another synced folder type. ================================================ FILE: templates/networking/network_manager/network_manager_device.erb ================================================ [connection] id=<%= options[:interface_name] %> uuid=<%= options[:uuid] %> type=ethernet autoconnect-priority=-100 autoconnect-retries=1 interface-name=<%= options[:interface_name] %> [ethernet] <% if options[:mac_address] -%> mac-address=<%= options[:mac_address] %> <% end -%> <% if options[:ipv4] -%> [ipv4] <% if options[:type] == "dhcp" -%> dhcp-timeout=90 method=auto required-timeout=20000 <% elsif options[:ipv4] -%> method=manual addresses=<%= options[:ipv4] %>/<%= options[:ipv4_mask] %> gateway=<%= options[:ipv4_gateway] %> <% end -%> <% end -%> <% if options[:ipv6] -%> [ipv6] <% if options[:type] == "dhcp" -%> addr-gen-mode=eui64 dhcp-timeout=90 method=auto <% elsif options[:ipv6] -%> method=manual addresses=<%= options[:ipv6] %>/<%= options[:ipv6_mask] %> gateway=<%= options[:ipv6_gateway] %> <% end -%> <% end -%> [user] org.freedesktop.NetworkManager.origin=vagrant ================================================ FILE: templates/nfs/exports_bsd.erb ================================================ # VAGRANT-BEGIN: <%= user %> <%= uuid %> <% folders.each do |dirs, opts| %> <%= dirs.map { |d| "#{d}" }.join(" ") %> <%=opts[:bsd__compiled_nfs_options] %> <%= ips.join(" ") %> <% end %> # VAGRANT-END: <%= user %> <%= uuid %> ================================================ FILE: templates/nfs/exports_darwin.erb ================================================ # VAGRANT-BEGIN: <%= user %> <%= uuid %> <% folders.each do |dirs, opts| %> <% dirs.each do |d| %> "<%= d %>" <%=opts[:bsd__compiled_nfs_options] %> <%= ips.join(" ") %> <% end %> <% end %> # VAGRANT-END: <%= user %> <%= uuid %> ================================================ FILE: templates/nfs/exports_linux.erb ================================================ # VAGRANT-BEGIN: <%= user %> <%= uuid %> <% ips.each do |ip| %> <% folders.each do |name, opts| %> "<%= opts[:hostpath] %>" <%= ip %>(<%= opts[:linux__nfs_options].join(",") %>) <% end %> <% end %> # VAGRANT-END: <%= user %> <%= uuid %> ================================================ FILE: templates/package_Vagrantfile.erb ================================================ Vagrant::Config.run do |config| # This Vagrantfile is auto-generated by `vagrant package` to contain # the MAC address of the box. Custom configuration should be placed in # the actual `Vagrantfile` in this box. config.vm.base_mac = "<%= base_mac %>" end # Load include vagrant file if it exists after the auto-generated # so it can override any of the settings include_vagrantfile = File.expand_path("../include/_Vagrantfile", __FILE__) load include_vagrantfile if File.exist?(include_vagrantfile) ================================================ FILE: templates/provisioners/chef_client/client.erb ================================================ log_level <%= log_level.inspect %> log_location STDOUT verbose_logging <%= verbose_logging.inspect %> <% if node_name %> node_name "<%= node_name %>" <% end %> ssl_verify_mode :verify_none chef_server_url "<%= chef_server_url %>" validation_client_name "<%= validation_client_name %>" validation_key "<%= validation_key %>" client_key "<%= client_key %>" encrypted_data_bag_secret <%= encrypted_data_bag_secret.inspect %> <% if environment %> environment "<%= environment %>" <% end %> file_cache_path "<%= file_cache_path %>" file_backup_path "<%= file_backup_path %>" http_proxy <%= http_proxy.inspect %> http_proxy_user <%= http_proxy_user.inspect %> http_proxy_pass <%= http_proxy_pass.inspect %> https_proxy <%= https_proxy.inspect %> https_proxy_user <%= https_proxy_user.inspect %> https_proxy_pass <%= https_proxy_pass.inspect %> no_proxy <%= no_proxy.inspect %> pid_file "/var/run/chef/chef-client.pid" Mixlib::Log::Formatter.show_time = true <% if formatter %> add_formatter "<%= formatter %>" <% end %> <% if custom_configuration -%> Chef::Config.from_file "<%= custom_configuration %>" <% end -%> ================================================ FILE: templates/provisioners/chef_solo/solo.erb ================================================ <% if node_name %> node_name "<%= node_name %>" <% end %> file_cache_path "<%= file_cache_path %>" file_backup_path "<%= file_backup_path %>" cookbook_path <%= cookbooks_path.inspect %> <% if roles_path && !roles_path.empty? -%> role_path <%= roles_path.size == 1 ? roles_path.first.inspect : roles_path.inspect %> <% end -%> log_level <%= log_level.inspect %> verbose_logging <%= verbose_logging.inspect %> encrypted_data_bag_secret <%= encrypted_data_bag_secret.inspect %> <% if data_bags_path && !data_bags_path.empty? -%> data_bag_path <%= data_bags_path.size == 1 ? data_bags_path.first.inspect : data_bags_path.inspect %> <% end %> <% if recipe_url -%> recipe_url "<%= recipe_url %>" <% end -%> <% if environments_path && !environments_path.empty? -%> environment_path <%= environments_path.inspect %> <% end -%> <% if environment -%> environment "<%= environment %>" <% end -%> <% if local_mode -%> local_mode true <% end -%> <% if nodes_path && !nodes_path.empty? -%> node_path <%= nodes_path.inspect %> <% end -%> http_proxy <%= http_proxy.inspect %> http_proxy_user <%= http_proxy_user.inspect %> http_proxy_pass <%= http_proxy_pass.inspect %> https_proxy <%= https_proxy.inspect %> https_proxy_user <%= https_proxy_user.inspect %> https_proxy_pass <%= https_proxy_pass.inspect %> no_proxy <%= no_proxy.inspect %> <% if formatter -%> add_formatter "<%= formatter %>" <% end %> <% if custom_configuration -%> Chef::Config.from_file "<%= custom_configuration %>" <% end -%> ================================================ FILE: templates/provisioners/chef_zero/zero.erb ================================================ <% if node_name %> node_name "<%= node_name %>" <% end %> file_cache_path "<%= file_cache_path %>" file_backup_path "<%= file_backup_path %>" cookbook_path <%= cookbooks_path.inspect %> <% if roles_path %> role_path <%= roles_path.size == 1 ? roles_path.first.inspect : roles_path.inspect %> <% end %> log_level <%= log_level.inspect %> verbose_logging <%= verbose_logging.inspect %> <% if !enable_reporting %> enable_reporting <%= enable_reporting.inspect %> <% end %> encrypted_data_bag_secret <%= encrypted_data_bag_secret.inspect %> <% if data_bags_path -%> data_bag_path <%= data_bags_path.size == 1 ? data_bags_path.first.inspect : data_bags_path.inspect %> <% end %> <% if environments_path %> environment_path <%= environments_path.inspect %> <% end -%> <% if environment %> environment "<%= environment %>" <% end -%> <% if local_mode -%> chef_zero.enabled true local_mode true <% end -%> <% if nodes_path -%> node_path <%= nodes_path.inspect %> <% end -%> <% if formatter %> add_formatter "<%= formatter %>" <% end %> <% if custom_configuration -%> Chef::Config.from_file "<%= custom_configuration %>" <% end -%> ================================================ FILE: templates/rgloader.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This file loads the proper rgloader/loader.rb file that comes packaged # with Vagrant so that encoded files can properly run with Vagrant. if ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"] require File.expand_path( "rgloader/loader", ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"]) else raise "Encoded files can't be read outside of the Vagrant installer." end ================================================ FILE: test/acceptance/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant-spec/acceptance" require_relative "shared/context_virtualbox" ================================================ FILE: test/acceptance/provider-docker/lifecycle_spec.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This tests the basic functionality of a provider: that it can run # a machine, provide SSH access, and destroy that machine. shared_examples "provider/docker/lifecycle" do |provider, options| if provider != "docker" raise ArgumentError, "provider must be docker to run tests" end if !options[:image] raise ArgumentError, "box option must be specified for provider: #{provider}" end include_context "acceptance" before do environment.skeleton("basic_docker") ENV["VAGRANT_SPEC_DOCKER_IMAGE"] = options[:image] end let(:opts) { options } after do # Just always do this just in case execute("vagrant", "destroy", "--force", log: false) end def assert_running result = execute("docker", "ps", "--filter", "name=dockertest") expect(result).to exit_with(0) expect(result.stdout).to match(/#{opts[:image]}/) end def assert_not_running result = execute("docker", "ps", "--filter", "name=dockertest") expect(result).to exit_with(0) # Check the output ends with the last column of the header from the `docker ps` # command, indicating no images found. expect(result.stdout).to match(/NAMES\n$/) end context "after an up" do before do assert_execute("vagrant", "up", "--provider=#{provider}") end after do assert_execute("vagrant", "destroy", "--force") end it "can manage machine lifecycle" do status("Test: machine is running after up") assert_running status("Test: halt") assert_execute("vagrant", "halt") status("Test: ssh doesn't work during halted state") assert_not_running status("Test: up after halt") assert_execute("vagrant", "up") assert_running end end end ================================================ FILE: test/acceptance/provider-virtualbox/linked_clone_spec.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This tests that VM is up as a linked clone shared_examples 'provider/linked_clone' do |provider, options| if !options[:box] raise ArgumentError, "box option must be specified for provider: #{provider}" end include_context 'acceptance' before do environment.skeleton('linked_clone') assert_execute('vagrant', 'box', 'add', 'box', options[:box]) end after do assert_execute('vagrant', 'destroy', '--force') end it 'creates machine as linked clone' do status('Test: machine is created successfully') result = execute('vagrant', 'up', "--provider=#{provider}") expect(result).to exit_with(0) status('Test: master VM is created') expect(result.stdout).to match(/master VM/) status('Test: machine is a master VM clone') expect(result.stdout).to match(/Cloning/) status('Test: machine is available by ssh') result = execute('vagrant', 'ssh', '-c', 'echo foo') expect(result).to exit_with(0) expect(result.stdout).to match(/foo\n$/) end end ================================================ FILE: test/acceptance/provider-virtualbox/network_intnet_spec.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_examples "provider/network/intnet" do |provider, options| if !options[:box] raise ArgumentError, "box option must be specified for provider: #{provider}" end include_context "acceptance" before do environment.skeleton("network_intnet") assert_execute("vagrant", "box", "add", "box", options[:box]) assert_execute("vagrant", "up", "--provider=#{provider}") end after do assert_execute("vagrant", "destroy", "--force", log: false) end it "properly configures an internal network" do end end ================================================ FILE: test/acceptance/shared/context_virtualbox.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_context "provider-context/virtualbox" do let(:extra_env) {{ "VBOX_USER_HOME" => "{{homedir}}", }} end ================================================ FILE: test/acceptance/skeletons/basic_docker/Vagrantfile ================================================ Vagrant.configure("2") do |config| config.vm.define "dockertest" do |c| c.vm.provider "docker" do |d| d.image = ENV["VAGRANT_SPEC_DOCKER_IMAGE"] d.remains_running = true end end end ================================================ FILE: test/acceptance/skeletons/linked_clone/Vagrantfile ================================================ Vagrant.configure('2') do |config| config.vm.box = 'basic' config.vm.provider 'virtualbox' do |v| v.linked_clone = true end end ================================================ FILE: test/acceptance/skeletons/network_intnet/Vagrantfile ================================================ Vagrant.configure("2") do |config| config.vm.box = "box" config.vm.network "private_network", ip: "192.168.50.4", virtualbox__intnet: true end ================================================ FILE: test/config/acceptance_boxes.yml ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # This is the list of required boxes for acceptance tests, and # their locations. The locations are used to download the boxes # on the fly, if necessary. Additionally, a SHA1 checksum is # given to determine if the box needs to be updated. - name: default url: http://files.vagrantup.com/test/boxes/default.box checksum: 1b0a7eb6e152c5b43f45485654ff4965a7f9f604 ================================================ FILE: test/support/isolated_environment.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "pathname" require "tmpdir" require "log4r" require "vagrant/util/platform" # This class manages an isolated environment for Vagrant to # run in. It creates a temporary directory to act as the # working directory as well as sets a custom home directory. # # This class also provides various helpers to create Vagrantfiles, # boxes, etc. class IsolatedEnvironment attr_reader :homedir attr_reader :workdir # Initializes an isolated environment. You can pass in some # options here to configure running custom applications in place # of others as well as specifying environmental variables. # # @param [Hash] apps A mapping of application name (such as "vagrant") # to an alternate full path to the binary to run. # @param [Hash] env Additional environmental variables to inject # into the execution environments. def initialize @logger = Log4r::Logger.new("test::isolated_environment") # Create a temporary directory for our work @tempdir = Vagrant::Util::Platform.fs_real_path(Dir.mktmpdir("vagrant-iso-env")) @logger.info("Initialize isolated environment: #{@tempdir}") # Setup the home and working directories @homedir = Pathname.new(File.join(@tempdir, "home")) @workdir = Pathname.new(File.join(@tempdir, "work")) @homedir.mkdir @workdir.mkdir end # This closes the environment by cleaning it up. def close @logger.info("Removing isolated environment: #{@tempdir}") FileUtils.rm_rf(@tempdir) end end ================================================ FILE: test/unit/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tmpdir" require "rubygems" # Gems require "checkpoint" require "rspec/its" # Require Vagrant itself so we can reference the proper # classes to test. require "vagrant" require "vagrant/util/platform" # Include patches for fake ftp require "vagrant/patches/fake_ftp" # Add the test directory to the load path $:.unshift File.expand_path("../../", __FILE__) # Load in helpers require "unit/support/dummy_communicator" require "unit/support/dummy_provider" require "unit/support/shared/base_context" require "unit/support/shared/action_synced_folders_context" require "unit/support/shared/capability_helpers_context" require "unit/support/shared/plugin_command_context" require "unit/support/shared/virtualbox_context" # Do not buffer output $stdout.sync = true $stderr.sync = true # Create a temporary directory where test vagrant will run. The reason we save # this to a constant is so we can clean it up later. VAGRANT_TEST_CWD = Dir.mktmpdir("vagrant-test-cwd") # Configure RSpec RSpec.configure do |c| c.formatter = :progress c.color_mode = :on if Vagrant::Util::Platform.windows? c.filter_run_excluding :skip_windows else c.filter_run_excluding :windows end if !Vagrant::Util::Which.which("bsdtar") c.filter_run_excluding :bsdtar end c.after(:suite) do FileUtils.rm_rf(VAGRANT_TEST_CWD) end end # Configure VAGRANT_CWD so that the tests never find an actual # Vagrantfile anywhere, or at least this minimizes those chances. ENV["VAGRANT_CWD"] = VAGRANT_TEST_CWD # Set the dummy provider to the default for tests ENV["VAGRANT_DEFAULT_PROVIDER"] = "dummy" # Unset all host plugins so that we aren't executing subprocess things # to detect a host for every test. Vagrant.plugin("2").manager.registered.dup.each do |plugin| if plugin.components.hosts.to_hash.length > 0 Vagrant.plugin("2").manager.unregister(plugin) end end # Disable checkpoint Checkpoint.disable! ================================================ FILE: test/unit/bin/vagrant_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) describe "vagrant bin" do include_context "unit" let(:env) { isolated_environment } let(:ui) { double(:ui) } let(:argv) { [] } let(:exit_code) { 0 } let(:run_vagrant) { lambda { begin instance_eval( File.read( File.expand_path( "../../../../bin/vagrant", __FILE__))) rescue SystemExit => e e.status end }.call } before do allow(env).to receive(:ui).and_return(ui) allow(ARGV).to receive(:dup).and_return(argv) allow(env).to receive(:unload) allow(env).to receive(:cli).and_return(exit_code) allow(Kernel).to receive(:at_exit) allow(Kernel).to receive(:exit) allow(Vagrant::Environment).to receive(:new).and_return(env) allow(Vagrant).to receive(:in_installer?).and_return(true) allow(self).to receive(:require_relative) end after { expect(run_vagrant).to eq(exit_code) } it "should run the CLI and exit successfully" do expect(env).to receive(:cli).with(argv).and_return(exit_code) expect(run_vagrant).to eq(exit_code) end context "with flag" do describe "--version" do let(:argv) { ["--version"] } before { allow(self).to receive(:require_relative).with(/version/) } it "should output the current version" do expect($stdout).to receive(:puts).with(/#{Regexp.escape(Vagrant::VERSION.to_s)}/) end end describe "--timestamp" do let(:argv) { ["--timestamp"] } it "should enable timestamps on logs" do expect(ENV).to receive(:[]=).with("VAGRANT_LOG_TIMESTAMP", "1") end end describe "--debug-timestamp" do let(:argv) { ["--debug-timestamp"] } it "should enable debugging and log timestamps" do expect(ENV).to receive(:[]=).with("VAGRANT_LOG_TIMESTAMP", "1") expect(ENV).to receive(:[]=).with("VAGRANT_LOG", "debug") end end describe "--no-color" do let(:argv) { ["--no-color"] } it "should remove flag from argv" do expect(env).to receive(:cli).with([]).and_return(exit_code) end it "should pass a Basic UI instance" do expect(Vagrant::Environment).to receive(:new). with(hash_including(ui_class: Vagrant::UI::Basic)) end end describe "--color" do let(:argv) { ["--color"] } it "should remove flag from argv" do expect(env).to receive(:cli).with([]).and_return(exit_code) end it "should pass a Colored UI instance" do expect(Vagrant::Environment).to receive(:new). with(hash_including(ui_class: Vagrant::UI::Colored)) end end describe "--no-tty" do let(:argv) { ["--no-tty"] } it "should enable less verbose progress output" do expect(Vagrant::Environment).to receive(:new). with(hash_including(ui_class: Vagrant::UI::NonInteractive)) end end end context "default CLI flags" do let(:argv) { ["--help"] } before do allow(env).to receive(:ui).and_return(ui) allow(ARGV).to receive(:dup).and_return(argv) allow(Kernel).to receive(:at_exit) allow(Kernel).to receive(:exit) allow(Vagrant::Environment).to receive(:new).and_call_original # Include this to intercept checkpoint instance setup # since it is a singleton allow(Vagrant::Util::CheckpointClient). to receive_message_chain(:instance, :setup, :check) end it "should include default CLI flags in command help output" do expect($stdout).to receive(:puts).with(/--debug/) end end context "when not in installer" do let(:warning) { "INSTALLER WARNING" } before do expect(Vagrant).to receive(:in_installer?).and_return(false) allow(I18n).to receive(:t).with(/not_in_installer/, any_args).and_return(warning) end context "when tool is missing" do before { expect(Vagrant).to receive(:detect_missing_tools).and_return(["tool"]) } it "should output a warning" do expect(env.ui).to receive(:warn).with(/#{warning}/, any_args) end end context "when tool is not missing" do before { expect(Vagrant).to receive(:detect_missing_tools).and_return([]) } it "should not output a warning" do expect(env.ui).not_to receive(:warn).with(/#{warning}/, any_args) end end end context "plugin commands" do let(:argv) { ["plugin"] } before do allow(ENV).to receive(:[]=) allow(ENV).to receive(:[]) end it "should unset vagrantfile" do expect(Vagrant::Environment).to receive(:new). with(hash_including(vagrantfile_name: "")).and_return(env) end it "should set the no plugins environment variable" do expect(ENV).to receive(:[]=).with("VAGRANT_NO_PLUGINS", "1") end it "should set the disable plugin init environment variable" do expect(ENV).to receive(:[]=).with("VAGRANT_DISABLE_PLUGIN_INIT", "1") end context "list" do let(:argv) { ["plugin", "list"] } it "should not set the disable plugin init environment variable" do expect(ENV).not_to receive(:[]=).with("VAGRANT_DISABLE_PLUGIN_INIT", "1") end end context "--local" do let(:argv) { ["plugin", "install", "--local"] } it "should not unset vagrantfile" do expect(Vagrant::Environment).to receive(:new). with(hash_excluding(vagrantfile_name: "")).and_return(env) end end context "with VAGRANT_LOCAL_PLUGINS_LOAD enabled" do before { expect(ENV).to receive(:[]).with("VAGRANT_LOCAL_PLUGINS_LOAD").and_return("1") } it "should not unset vagrantfile" do expect(Vagrant::Environment).to receive(:new). with(hash_excluding(vagrantfile_name: "")).and_return(env) end end end end ================================================ FILE: test/unit/plugins/commands/autocomplete/commands/install_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/autocomplete/command/install") describe VagrantPlugins::CommandAutocomplete::Command::Install do include_context "unit" let(:argv) { [] } 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(:homedir) { Dir.mktmpdir("homedir") } before { allow(Dir).to receive(:home) { homedir } } after { FileUtils.rm_rf(homedir) } subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) end context "with no arguments" do it "returns without errors" do expect(subject.execute).to be(0) end end end ================================================ FILE: test/unit/plugins/commands/box/command/add_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/box/command/add") describe VagrantPlugins::CommandBox::Command::Add do include_context "unit" let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with one argument" do let(:argv) { ["foo"] } it "executes the runner with the proper actions" do expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_name]).to be_nil expect(opts[:box_url]).to eq("foo") true } subject.execute end end context "with two arguments" do let(:argv) { ["foo", "bar"] } it "executes the runner with the proper actions" do expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_name]).to eq("foo") expect(opts[:box_url]).to eq("bar") true } subject.execute end end context "with more than two arguments" do let(:argv) { ["one", "two", "three"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with architecture flag" do let(:argv) { ["foo", "--architecture", "test-arch"] } it "executes the runner with box architecture set" do expect(action_runner).to receive(:run) do |_, opts| expect(opts[:box_architecture]).to eq("test-arch") end subject.execute end end end ================================================ FILE: test/unit/plugins/commands/box/command/outdated_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/box/command/outdated") describe VagrantPlugins::CommandBox::Command::Outdated do include_context "unit" let(:argv) { [] } let(:iso_env) do env = isolated_environment env.vagrantfile("") env.create_vagrant_env end subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) end context "with force argument" do let(:argv) { ["--force"] } it "passes along the force update option" do expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_outdated_force]).to be_truthy true } subject.execute end end context "with global argument" do let(:argv) { ["--global"] } it "calls outdated_global" do expect(subject).to receive(:outdated_global) subject.execute end describe ".outdated_global" do let(:test_iso_env) { isolated_environment } let(:md) { md = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) { "name": "foo", "versions": [ { "version": "1.0" }, { "version": "1.1", "providers": [ { "name": "virtualbox", "url": "bar" } ] }, { "version": "1.2", "providers": [ { "name": "vmware", "url": "baz" } ] } ] } RAW } let(:collection) do collection = double("collection") allow(collection).to receive(:all).and_return([box]) allow(collection).to receive(:find).and_return(box) collection end context "when latest version is available for provider" do let(:box) do box_dir = test_iso_env.box3("foo", "1.0", :vmware) box = Vagrant::Box.new( "foo", :vmware, "1.0", box_dir, metadata_url: "foo") allow(box).to receive(:load_metadata).and_return(md) box end it "displays the latest version" do allow(iso_env).to receive(:boxes).and_return(collection) expect(I18n).to receive(:t).with(/box_outdated$/, hash_including(latest: "1.2")) subject.outdated_global({}) end end context "when latest version isn't available for provider" do let(:box) do box_dir = test_iso_env.box3("foo", "1.0", :virtualbox) box = Vagrant::Box.new( "foo", :virtualbox, "1.0", box_dir, metadata_url: "foo") allow(box).to receive(:load_metadata).and_return(md) box end it "displays the latest version for that provider" do allow(iso_env).to receive(:boxes).and_return(collection) expect(I18n).to receive(:t).with(/box_outdated$/, hash_including(latest: "1.1")) subject.outdated_global({}) end end context "when no versions are available for provider" do let(:box) do box_dir = test_iso_env.box3("foo", "1.0", :libvirt) box = Vagrant::Box.new( "foo", :libvirt, "1.0", box_dir, metadata_url: "foo") allow(box).to receive(:load_metadata).and_return(md) box end it "displays up to date message" do allow(iso_env).to receive(:boxes).and_return(collection) expect(I18n).to receive(:t).with(/box_up_to_date$/, hash_including(version: "1.0")) subject.outdated_global({}) end end context "with architectures" do let(:md) { md = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) { "name": "foo", "versions": [ { "version": "1.0", "providers": [ { "name": "vmware", "architecture": "amd64", "url": "bar" }, { "name": "virtualbox", "architecture": "unknown", "url": "foo" } ] }, { "version": "1.1", "providers": [ { "name": "vmware", "architecture": "amd64", "url": "bar" }, { "name": "virtualbox", "architecture": "unknown", "url": "foo" }, { "name": "docker", "architecture": "amd64", "url": "foo" } ] }, { "version": "1.2", "providers": [ { "name": "vmware", "architecture": "arm64", "url": "baz" }, { "name": "virtualbox", "architecture": "unknown", "url": "bat" }, { "name": "docker", "architecture": "unknown", "url": "foo" } ] } ] } RAW } context "when latest version is available for provider with unknown architecture" do let(:box) do box_dir = test_iso_env.box3("foo", "1.0", :virtualbox) box = Vagrant::Box.new( "foo", :virtualbox, "1.0", box_dir, metadata_url: "foo") allow(box).to receive(:load_metadata).and_return(md) box end it "displays the latest version" do allow(iso_env).to receive(:boxes).and_return(collection) expect(I18n).to receive(:t).with(/box_outdated$/, hash_including(latest: "1.2")) subject.outdated_global({}) end end context "when latest version isn't available for provider with explicit architecture" do let(:box) do box_dir = test_iso_env.box3("foo", "1.0", :vmware, architecture: "amd64") box = Vagrant::Box.new( "foo", :vmware, "1.0", box_dir, metadata_url: "foo", architecture: "amd64") allow(box).to receive(:load_metadata).and_return(md) box end it "displays the latest version" do allow(iso_env).to receive(:boxes).and_return(collection) expect(I18n).to receive(:t).with(/box_outdated$/, hash_including(latest: "1.1")) subject.outdated_global({}) end end context "when no versions are available provider with explicit architecture" do let(:box) do box_dir = test_iso_env.box3("foo", "1.1", :vmware) box = Vagrant::Box.new( "foo", :vmware, "1.1", box_dir, metadata_url: "foo", architecture: "amd64") allow(box).to receive(:load_metadata).and_return(md) box end it "displays up to date message" do allow(iso_env).to receive(:boxes).and_return(collection) expect(I18n).to receive(:t).with(/box_up_to_date$/, hash_including(version: "1.1")) subject.outdated_global({}) end end context "when newer version does not have an explicit architecture" do let(:box) do box_dir = test_iso_env.box3("foo", "1.1", :docker) box = Vagrant::Box.new( "foo", :docker, "1.1", box_dir, metadata_url: "foo", architecture: :auto) allow(box).to receive(:load_metadata).and_return(md) box end it "displays up to date message" do allow(iso_env).to receive(:boxes).and_return(collection) expect(I18n).to receive(:t).with(/box_up_to_date$/, hash_including(version: "1.1")) subject.outdated_global({}) end end end end end end ================================================ FILE: test/unit/plugins/commands/box/command/prune_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/box/command/prune") describe VagrantPlugins::CommandBox::Command::Prune do include_context "unit" include_context "command plugin helpers" let(:entry_klass) { Vagrant::MachineIndex::Entry } let(:iso_env) do # We have to create a Vagrantfile so there is a root path isolated_environment.tap do |env| env.vagrantfile("") end end let(:iso_vagrant_env) { iso_env.create_vagrant_env } let(:argv) { [] } # Seems this way of providing a box version triggers box in use. def new_entry(name, box_name, box_provider, version) entry_klass.new.tap do |e| e.name = name e.vagrantfile_path = "/bar" e.extra_data["box"] = { "name" => box_name, "provider" => box_provider, "version" => version, } end end subject { described_class.new(argv, iso_vagrant_env) } describe "execute" do context "with no args" do it "removes the old version and keeps the current one" do # Let's put some things in the index iso_env.box3("foobox", "1.0", :virtualbox); iso_env.box3("foobox", "1.1", :virtualbox); iso_env.box3("barbox", "1.0", :vmware); iso_env.box3("barbox", "1.1", :vmware); iso_vagrant_env.machine_index.set(new_entry("foo", "foobox", "virtualbox", 1)) output = "" allow(iso_vagrant_env.ui).to receive(:info) do |data| output << data end expect(iso_vagrant_env.boxes.all.count).to eq(4) expect(subject.execute).to eq(0) expect(iso_vagrant_env.boxes.all.count).to eq(2) expect(output).to include("barbox (vmware, 1.1)") expect(output).to include("Removing box 'barbox' (v1.0) with provider 'vmware'...") expect(output).to include("foobox (virtualbox, 1.1)") expect(output).to include("Removing box 'foobox' (v1.0) with provider 'virtualbox'...") end it "removes nothing" do # Let's put some things in the index iso_env.box3("foobox", "1.0", :virtualbox); iso_env.box3("barbox", "1.0", :vmware); iso_vagrant_env.machine_index.set(new_entry("foo", "foobox", "virtualbox", 1)) allow(iso_vagrant_env.ui).to receive(:info).and_call_original expect(iso_vagrant_env.ui).to receive(:info).with(/No old versions of boxes/). and_call_original expect(iso_vagrant_env.boxes.all.count).to eq(2) expect(subject.execute).to eq(0) expect(iso_vagrant_env.boxes.all.count).to eq(2) end end context "with --provider" do let(:argv) { ["--provider", "virtualbox"] } it "removes the old versions of the specified provider" do # Let's put some things in the index iso_env.box3("foobox", "1.0", :virtualbox); iso_env.box3("foobox", "1.1", :virtualbox); iso_env.box3("barbox", "1.0", :vmware); iso_env.box3("barbox", "1.1", :vmware); iso_vagrant_env.machine_index.set(new_entry("foo", "foobox", "virtualbox", 1)) output = "" allow(iso_vagrant_env.ui).to receive(:info) do |data| output << "\n" + data end expect(iso_vagrant_env.boxes.all.count).to eq(4) expect(subject.execute).to eq(0) expect(iso_vagrant_env.boxes.all.count).to eq(3) expect(output).to include("foobox (virtualbox, 1.1)") expect(output).to include("Removing box 'foobox' (v1.0) with provider 'virtualbox'...") end end context "with --dry-run" do let(:argv) { ["--dry-run"] } it "removes the old versions of the specified provider" do # Let's put some things in the index iso_env.box3("foobox", "1.0", :virtualbox); iso_env.box3("foobox", "1.1", :virtualbox); iso_vagrant_env.machine_index.set(new_entry("foo", "foobox", "virtualbox", 1)) output = "" allow(iso_vagrant_env.ui).to receive(:info) do |data| output << "\n" + data end expect(iso_vagrant_env.boxes.all.count).to eq(2) expect(subject.execute).to eq(0) expect(iso_vagrant_env.boxes.all.count).to eq(2) expect(output).to include("foobox (virtualbox, 1.1)") expect(output).to include("Would remove foobox virtualbox 1.0") end end context "with --name" do let(:argv) { ["--name", "barbox"] } it "removes the old versions of the specified provider" do # Let's put some things in the index iso_env.box3("foobox", "1.0", :virtualbox); iso_env.box3("foobox", "1.1", :virtualbox); iso_env.box3("barbox", "1.0", :vmware); iso_env.box3("barbox", "1.1", :vmware); iso_vagrant_env.machine_index.set(new_entry("foo", "foobox", "virtualbox", 1)) output = "" allow(iso_vagrant_env.ui).to receive(:info) do |data| output << "\n" + data end expect(iso_vagrant_env.boxes.all.count).to eq(4) expect(subject.execute).to eq(0) expect(iso_vagrant_env.boxes.all.count).to eq(3) expect(output).to include("barbox (vmware, 1.1)") expect(output).to include("Removing box 'barbox' (v1.0) with provider 'vmware'...") end end context "with --name and --provider" do let(:argv) { ["--name", "foobox", "--provider", "virtualbox"] } it "removed the old versions of that name and provider only" do # Let's put some things in the index iso_env.box3("foobox", "1.0", :virtualbox); iso_env.box3("foobox", "1.1", :virtualbox); iso_env.box3("foobox", "1.0", :vmware); iso_env.box3("foobox", "1.1", :vmware); iso_env.box3("barbox", "1.0", :vmware); iso_env.box3("barbox", "1.1", :vmware); iso_vagrant_env.machine_index.set(new_entry("foo", "foobox", "virtualbox", 1)) output = "" allow(iso_vagrant_env.ui).to receive(:info) do |data| output << "\n" + data end expect(iso_vagrant_env.boxes.all.count).to eq(6) expect(subject.execute).to eq(0) expect(iso_vagrant_env.boxes.all.count).to eq(5) expect(output).to include("Removing box 'foobox' (v1.0) with provider 'virtualbox'...") end end end end ================================================ FILE: test/unit/plugins/commands/box/command/remove_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/box/command/remove") describe VagrantPlugins::CommandBox::Command::Remove do include_context "unit" let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with one argument" do let(:argv) { ["foo"] } it "invokes the action runner" do expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_name]).to eq("foo") expect(opts[:force_confirm_box_remove]).to be(false) true } subject.execute end context "with --force" do let(:argv) { super() + ["--force"] } it "invokes the action runner with force option" do expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_name]).to eq("foo") expect(opts[:force_confirm_box_remove]).to be(true) true } subject.execute end end end context "with two arguments" do let(:argv) { ["foo", "bar"] } it "uses the 2nd arg as a provider" do expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_name]).to eq("foo") expect(opts[:box_provider]).to eq("bar") true } subject.execute end end context "with more than two arguments" do let(:argv) { ["one", "two", "three"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with architecture flag" do let(:argv) { ["foo", "--architecture", "test-arch"] } it "should execute runner with box architecture set" do expect(action_runner).to receive(:run) do |_, opts| expect(opts[:box_architecture]).to eq("test-arch") end subject.execute end end context "with all providers flag" do let(:argv) { ["foo", "--all-providers"] } it "should execute runner with all providers enabled" do expect(action_runner).to receive(:run) do |_, opts| expect(opts[:box_remove_all_providers]).to be(true) end subject.execute end end context "with all architectures flag" do let(:argv) { ["foo", "--all-architectures"] } it "should execute runner with all architectures enabled" do expect(action_runner).to receive(:run) do |_, opts| expect(opts[:box_remove_all_architectures]).to be(true) end subject.execute end end end ================================================ FILE: test/unit/plugins/commands/box/command/repackage_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/box/command/repackage") describe VagrantPlugins::CommandBox::Command::Repackage do include_context "unit" let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with one argument" do let(:argv) { ["one"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with two arguments" do let(:argv) { ["one", "two"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with three arguments" do it "repackages the box with the given provider" end context "with more than three arguments" do let(:argv) { ["one", "two", "three", "four"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end end ================================================ FILE: test/unit/plugins/commands/box/command/update_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/box/command/update") describe VagrantPlugins::CommandBox::Command::Update do include_context "unit" let(:argv) { [] } let(:iso_env) do # We have to create a Vagrantfile so there is a root path test_iso_env.vagrantfile("") test_iso_env.create_vagrant_env end let(:test_iso_env) { isolated_environment } 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 allow(iso_env).to receive(:action_runner).and_return(action_runner) machine.config.vm.box = "foo" end describe "execute" do context "updating specific box" do let(:argv) { ["--box", "foo"] } let(:scratch) { Dir.mktmpdir("vagrant-test-command-box-update-execute") } let(:metadata_url) { Pathname.new(scratch).join("metadata.json") } let(:box_args) { ["foo", "1.0", :virtualbox] } let(:box_opts) { {metadata_url: metadata_url.to_s} } before do metadata_url.open("w") do |f| f.write("") end test_iso_env.box3(*box_args, **box_opts) end after do FileUtils.rm_rf(scratch) end it "doesn't update if they're up to date" do called = false allow(action_runner).to receive(:run) do |callable, opts| if opts[:box_provider] called = true end opts end subject.execute expect(called).to be(false) end it "does the correct update if there is an update" do metadata_url.open("w") do |f| f.write( { name: "foo", versions: [ { version: "1.0" }, { version: "1.8", providers: [ { name: "virtualbox", url: "bar" } ] }, { version: "1.10", providers: [ { name: "virtualbox", url: "bar" } ] }, { version: "1.11", providers: [ { name: "virtualbox", url: "bar" } ] } ] }.to_json ) end action_called = false allow(action_runner).to receive(:run) do |action, opts| if opts[:box_provider] action_called = true expect(opts[:box_force]).to eq(nil) expect(opts[:box_url]).to eq(metadata_url.to_s) expect(opts[:box_provider]).to eq("virtualbox") expect(opts[:box_version]).to eq("1.11") expect(opts[:box_download_ca_path]).to be_nil expect(opts[:box_download_ca_cert]).to be_nil expect(opts[:box_download_client_cert]).to be_nil expect(opts[:box_download_insecure]).to be_nil end opts end subject.execute expect(action_called).to be(true) end it "raises an error if there are multiple providers" do test_iso_env.box3("foo", "1.0", :vmware) expect(action_runner).to receive(:run).never expect { subject.execute }. to raise_error(Vagrant::Errors::BoxUpdateMultiProvider) end context "with multiple providers and specifying the provider" do let(:argv) { ["--box", "foo", "--provider", "vmware"] } it "updates the proper box" do metadata_url.open("w") do |f| f.write( { name: "foo", versions: [ { version: "1.0" }, { version: "1.1", providers: [ { name: "vmware", url: "bar" } ] } ] }.to_json ) end test_iso_env.box3("foo", "1.0", :vmware) action_called = false allow(action_runner).to receive(:run) do |action, opts| if opts[:box_provider] action_called = true expect(opts[:box_url]).to eq(metadata_url.to_s) expect(opts[:box_provider]).to eq("vmware") expect(opts[:box_version]).to eq("1.1") end opts end subject.execute expect(action_called).to be(true) end it "raises an error if that provider doesn't exist" do expect(action_runner).to receive(:run).never expect { subject.execute }. to raise_error(Vagrant::Errors::BoxNotFoundWithProvider) 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( { name: "foo", versions: [ { version: "1.0" }, { version: "1.1", providers: [ { name: "virtualbox", url: "bar" } ] } ] }.to_json ) 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_download_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"] } it "raises an exception" do expect(action_runner).to receive(:run).never expect { subject.execute }. to raise_error(Vagrant::Errors::BoxNotFound) end end context "with architecture" do let(:box_opts) { {metadata_url: metadata_url.to_s, architecture: "test-arch"} } it "doesn't update if they're up to date" do called = false allow(action_runner).to receive(:run) do |callable, opts| if opts[:box_provider] called = true end opts end subject.execute expect(called).to be(false) end it "does the correct update if there is an update" do metadata_url.open("w") do |f| f.write( { name: "foo", versions: [ { version: "1.0" }, { version: "1.8", providers: [ { name: "virtualbox", url: "bar", architecture: "test-arch", default_architecture: true } ] }, { version: "1.10", providers: [ { name: "virtualbox", url: "bar", architecture: "test-arch", default_architecture: true } ] }, { version: "1.11", providers: [ { name: "virtualbox", url: "bar", architecture: "test-arch", default_architecture: true } ] } ] }.to_json ) end action_called = false allow(action_runner).to receive(:run) do |action, opts| if opts[:box_provider] action_called = true expect(opts[:box_force]).to eq(nil) expect(opts[:box_url]).to eq(metadata_url.to_s) expect(opts[:box_provider]).to eq("virtualbox") expect(opts[:box_version]).to eq("1.11") expect(opts[:box_architecture]).to eq("test-arch") expect(opts[:box_download_ca_path]).to be_nil expect(opts[:box_download_ca_cert]).to be_nil expect(opts[:box_download_client_cert]).to be_nil expect(opts[:box_download_insecure]).to be_nil end opts end subject.execute expect(action_called).to be(true) end it "raises an error if there are multiple providers" do test_iso_env.box3("foo", "1.0", :vmware) expect(action_runner).to receive(:run).never expect { subject.execute }. to raise_error(Vagrant::Errors::BoxUpdateMultiProvider) end it "raises an error if there are multiple architectures" do test_iso_env.box3("foo", "1.0", :virtualbox, architecture: "other-arch") expect(action_runner).to receive(:run).never expect { subject.execute }. to raise_error(Vagrant::Errors::BoxUpdateMultiArchitecture) end context "with multiple architectures and specifying the architecture" do let(:argv) { ["--box", "foo", "--architecture", "other-arch"] } it "updates the proper box" do metadata_url.open("w") do |f| f.write( { name: "foo", versions: [ { version: "1.0" }, { version: "1.1", providers: [ { name: "virtualbox", url: "bar", architecture: "test-arch", }, { name: "virtualbox", url: "bar", architecture: "other-arch", } ] } ] }.to_json ) end test_iso_env.box3("foo", "1.0", :virtualbox, architecture: "other-arch") action_called = false allow(action_runner).to receive(:run) do |action, opts| if opts[:box_provider] action_called = true 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_architecture]).to eq("other-arch") end opts end subject.execute expect(action_called).to be(true) end it "raises an error if that provider doesn't exist" do expect(action_runner).to receive(:run).never expect { subject.execute }. to raise_error(Vagrant::Errors::BoxNotFoundWithProviderArchitecture) end end end end context "updating environment machines" do before do allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end let(:box) do box_dir = test_iso_env.box3("foo", "1.0", :virtualbox) box = Vagrant::Box.new( "foo", :virtualbox, "1.0", box_dir, metadata_url: "foo") allow(box).to receive(:has_update?).and_return(nil) box end it "ignores machines without boxes" do expect(action_runner).to receive(:run).never subject.execute end it "doesn't update boxes if they're up-to-date" do allow(machine).to receive(:box).and_return(box) 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(nil) expect(action_runner).to receive(:run).never subject.execute end context "boxes have an update" do let(:md) { Vagrant::BoxMetadata.new( StringIO.new( { name: "foo", versions: [ { version: "1.0" }, { version: "1.1", providers: [ { name: "virtualbox", url: "bar" } ] } ] }.to_json ) ) } before { allow(machine).to receive(:box).and_return(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(any_args) { |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 "when box version is updated but previous box exists" do let(:collection) { double("collection") } it "updates the box" do # First call gets nil result to for lookup expect(machine).to receive(:box).and_return(nil) expect(Vagrant::BoxCollection).to receive(:new).and_return(collection) expect(collection).to receive(:find).and_return(box) 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(any_args) { |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 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(any_args) { |action, opts| expect(opts[:box_download_ca_cert]).to eq("oof") expect(opts[:box_download_ca_path]).to eq("rab") expect(opts[:box_download_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(any_args) { |action, opts| expect(opts[:box_download_ca_cert]).to eq("foo") expect(opts[:box_download_ca_path]).to eq("bar") expect(opts[:box_download_client_cert]).to eq("baz") expect(opts[:box_download_insecure]).to be(true) true } subject.execute end end context "ignoring boxes with no metadata" do before do allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end let(:box) do box_dir = test_iso_env.box3("foo", "1.0", :virtualbox) box = Vagrant::Box.new( "foo", :virtualbox, "1.0", box_dir, metadata_url: "foo") allow(box).to receive(:has_update?).and_raise(Vagrant::Errors::BoxUpdateNoMetadata, name: "foo") box end it "continues to update the rest of the boxes in the environment" do subject.execute end end context "force flag is specified on the command line" do let(:argv) { ["--force"].concat(download_options) } it "passes force through to action_box_add as true" 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(any_args) { |action, opts| expect(opts[:box_force]).to be(true) true } subject.execute end end end end end end end ================================================ FILE: test/unit/plugins/commands/cap/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 context "runs against target vm" do let(:argv) { ["provider", "foo", "--target-guest=dummy"] } let(:cap) { Class.new do def self.foo(m) true end end } 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 end end ================================================ FILE: test/unit/plugins/commands/cloud/auth/login_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/auth/login") describe VagrantPlugins::CloudCommand::AuthCommand::Command::Login do include_context "unit" let(:argv) { [] } let(:env) { isolated_environment.create_vagrant_env } let(:action_runner) { double("action_runner") } let(:client) { double("client", logged_in?: logged_in) } let(:logged_in) { true } before do allow(env).to receive(:action_runner). and_return(action_runner) allow(VagrantPlugins::CloudCommand::Client). to receive(:new).and_return(client) end subject { described_class.new(argv, env) } describe "#execute_check" do context "when user is logged in" do let(:logged_in) { true } it "should output a success message" do expect(env.ui).to receive(:success) subject.execute_check(client) end it "should return zero value" do expect(subject.execute_check(client)).to eq(0) end end context "when user is not logged in" do let(:logged_in) { false } it "should output an error message" do expect(env.ui).to receive(:error) subject.execute_check(client) end it "should return a non-zero value" do r = subject.execute_check(client) expect(r).not_to eq(0) expect(r).to be_a(Integer) end end end describe "#execute_token" do let(:token) { double("token") } before { allow(client).to receive(:store_token) } it "should store the token" do expect(client).to receive(:store_token).with(token) subject.execute_token(client, token) end context "when token is valid" do let(:logged_in) { true } it "should output a success message" do expect(env.ui).to receive(:success).twice subject.execute_token(client, token) end it "should return a zero value" do expect(subject.execute_token(client, token)).to eq(0) end end context "when token is invalid" do let(:logged_in) { false } it "should output an error message" do expect(env.ui).to receive(:error) subject.execute_token(client, token) end it "should return a non-zero value" do r = subject.execute_token(client, token) expect(r).not_to eq(0) expect(r).to be_a(Integer) end end end describe "#execute" do before do allow(client).to receive(:username_or_email=) allow(client).to receive(:store_token) end context "when arguments are passed" do before { argv << "argument" } it "should print help" do expect { subject.execute }.to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "when --check flag is used" do before { argv << "--check" } it "should run login check" do expect(subject).to receive(:execute_check).with(client) subject.execute end it "should return the value of the check execution" do result = double("result") expect(subject).to receive(:execute_check).with(client).and_return(result) expect(subject.execute).to eq(result) end end context "when --token flag is used" do let(:new_token) { "NEW-TOKEN" } before { argv.push("--token").push(new_token) } it "should execute the token action" do expect(subject).to receive(:execute_token).with(client, new_token) subject.execute end it "should return value of token action" do result = double("result") expect(subject).to receive(:execute_token).with(client, new_token).and_return(result) expect(subject.execute).to eq(result) end it "should store the new token" do expect(client).to receive(:store_token).with(new_token) subject.execute end end context "when user is logged in" do let(:logged_in) { true } it "should output success message" do expect(env.ui).to receive(:success) subject.execute end it "should return a zero value" do expect(subject.execute).to eq(0) end end context "when user is not logged in" do let(:logged_in) { false } it "should run the client login" do expect(subject).to receive(:client_login) subject.execute end context "when username and description flags are supplied" do let(:username) { "my-username" } let(:description) { "my-description" } before { argv.push("--username").push(username).push("--description").push(description) } it "should include login and description to login" do expect(subject).to receive(:client_login).with(env, hash_including(login: username, description: description)) subject.execute end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/auth/logout_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/auth/logout") describe VagrantPlugins::CloudCommand::AuthCommand::Command::Logout do include_context "unit" let(:argv) { [] } 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(:client) { double("client") } subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } before do allow(VagrantPlugins::CloudCommand::Client).to receive(:new).and_return(client) allow(iso_env).to receive(:action_runner).and_return(action_runner) end context "with any arguments" do let (:argv) { ["stuff", "things"] } it "shows the help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with no arguments" do it "logs you out" do expect(client).to receive(:clear_token) expect(subject.execute).to eq(0) end end end ================================================ FILE: test/unit/plugins/commands/cloud/auth/middleware/add_authentication_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/auth/middleware/add_authentication") describe VagrantPlugins::CloudCommand::AddAuthentication do include_context "unit" let(:app) { lambda { |env| } } let(:ui) { Vagrant::UI::Silent.new } let(:env) { { env: iso_env, ui: ui } } let(:iso_env) { isolated_environment.create_vagrant_env } let(:server_url) { "http://vagrantcloud.com" } let(:client) { double("client", token: token) } let(:token) { "TEST_TOKEN" } subject { described_class.new(app, env) } before do allow(Vagrant).to receive(:server_url).and_return(server_url) allow(VagrantPlugins::CloudCommand::Client).to receive(:new). with(iso_env).and_return(client) stub_env("ATLAS_TOKEN" => nil) end describe "#call" do it "does nothing if we have no server set" do allow(Vagrant).to receive(:server_url).and_return(nil) original = [token, "#{server_url}/bar"] env[:box_urls] = original.dup subject.call(env) expect(env[:box_urls]).to eq(original) end it "does nothing if we aren't logged in" do original = ["foo", "#{server_url}/bar"] env[:box_urls] = original.dup subject.call(env) expect(env[:box_urls]).to eq(original) end context "when urls are set" do it "does not modify urls" do original = ["https://example.com/boxes/test.box", "file://C:/my/box/path/local.box"] env[:box_urls] = original.dup subject.call(env) expect(env[:box_urls]).to eq(original) end it "should remove access_token parameters when found" do env[:box_urls] = ["https://example.com/boxes/test.box?access_token=TEST", "file://C:/my/box/path/local.box"] subject.call(env) expect(env[:box_urls]).to eq([ "https://example.com/boxes/test.box", "file://C:/my/box/path/local.box"]) end end context "with VAGRANT_SERVER_ACCESS_TOKEN_BY_URL set" do before { stub_env("VAGRANT_SERVER_ACCESS_TOKEN_BY_URL" => "1") } it "appends the access token to the URL of server URLs" do original = [ "http://example.com/box.box", "#{server_url}/foo.box", "#{server_url}/bar.box?arg=true", ] expected = original.dup expected[1] = "#{original[1]}?access_token=#{token}" expected[2] = "#{original[2]}&access_token=#{token}" env[:box_urls] = original.dup subject.call(env) expect(env[:box_urls]).to eq(expected) end it "does not append the access token to vagrantcloud.com URLs if Atlas" do server_url = "https://atlas.hashicorp.com" allow(Vagrant).to receive(:server_url).and_return(server_url) allow(subject).to receive(:sleep) original = [ "http://example.com/box.box", "http://vagrantcloud.com/foo.box", "http://vagrantcloud.com/bar.box?arg=true", ] expected = original.dup env[:box_urls] = original.dup subject.call(env) expect(env[:box_urls]).to eq(expected) end it "warns when adding token to custom server" do server_url = "https://example.com" allow(Vagrant).to receive(:server_url).and_return(server_url) original = [ "http://example.org/box.box", "http://vagrantcloud.com/foo.box", "http://example.com/bar.box", "http://example.com/foo.box" ] expected = original.dup expected[2] = expected[2] + "?access_token=#{token}" expected[3] = expected[3] + "?access_token=#{token}" expect(subject).to receive(:sleep).once expect(ui).to receive(:warn).once.and_call_original env[:box_urls] = original.dup subject.call(env) expect(env[:box_urls]).to eq(expected) end it "ignores urls that it cannot parse" do bad_url = "this is not a valid url" # Ensure the bad URL does cause an exception expect{ URI.parse(bad_url) }.to raise_error URI::Error env[:box_urls] = [bad_url] subject.call(env) expect(env[:box_urls].first).to eq(bad_url) end it "does not append multiple access_tokens" do original = [ "#{server_url}/foo.box?access_token=existing", "#{server_url}/bar.box?arg=true", ] env[:box_urls] = original.dup subject.call(env) expect(env[:box_urls][0]).to eq("#{server_url}/foo.box?access_token=existing") expect(env[:box_urls][1]).to eq("#{server_url}/bar.box?arg=true&access_token=#{token}") end context "when token is not set" do let(:token) { nil } it "modifies host URL to target if authorized host" do originals = VagrantPlugins::CloudCommand::AddAuthentication:: REPLACEMENT_HOSTS.map{ |h| "http://#{h}/box.box" } expected = "http://#{VagrantPlugins::CloudCommand::AddAuthentication::TARGET_HOST}/box.box" env[:box_urls] = originals subject.call(env) env[:box_urls].each do |url| expect(url).to eq(expected) end end it "returns original urls when not modified" do to_persist = "file:////path/to/box.box" to_change = VagrantPlugins::CloudCommand::AddAuthentication:: REPLACEMENT_HOSTS.map{ |h| "http://#{h}/box.box" }.first expected = "http://#{VagrantPlugins::CloudCommand::AddAuthentication::TARGET_HOST}/box.box" env[:box_urls] = [to_persist, to_change] subject.call(env) check_persist, check_change = env[:box_urls] expect(check_change).to eq(expected) expect(check_persist).to eq(to_persist) # NOTE: The behavior of URI.parse changes on Ruby 2.5 to produce # the same string value. To make the test worthwhile in checking # for the same value, check that the object IDs are still the same. expect(check_persist.object_id).to eq(to_persist.object_id) end end end context "with VAGRANT_SERVER_ACCESS_TOKEN_BY_URL unset" do before { stub_env("VAGRANT_SERVER_ACCESS_TOKEN_BY_URL" => nil) } it "returns the original urls" do box1 = "http://vagrantcloud.com/box.box" box2 = "http://app.vagrantup.com/box.box" env = { box_urls: [ box1.dup, box2.dup ] } subject.call(env) expect(env[:box_urls]).to eq([box1, box2]) end it "removes access_token parameters if set" do box1 = "http://vagrantcloud.com/box.box" box2 = "http://app.vagrantup.com/box.box" box3 = "http://app.vagrantup.com/box.box?arg1=value1" env = { box_urls: [ "#{box1}?access_token=TEST_TOKEN", box2.dup, "#{box3}&access_token=TEST_TOKEN" ] } subject.call(env) expect(env[:box_urls]).to eq([box1, box2, box3]) end end end end ================================================ FILE: test/unit/plugins/commands/cloud/auth/middleware/add_downloader_authentication_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/auth/middleware/add_downloader_authentication") require "vagrant/util/downloader" describe VagrantPlugins::CloudCommand::AddDownloaderAuthentication do include_context "unit" let(:app) { lambda { |env| } } let(:ui) { Vagrant::UI::Silent.new } let(:env) { { env: iso_env, ui: ui } } let(:iso_env) { isolated_environment.create_vagrant_env } let(:server_url) { "http://vagrantcloud.com/box.box" } let(:dwnloader) { Vagrant::Util::Downloader.new(server_url, "/some/path", {}) } subject { described_class.new(app, env) } before do allow(Vagrant).to receive(:server_url).and_return(server_url) stub_env("ATLAS_TOKEN" => nil) stub_env("VAGRANT_SERVER_ACCESS_TOKEN_BY_URL" => nil) VagrantPlugins::CloudCommand::Client.class_variable_set(:@@client, nil) end describe "#call" do context "non full paths" do let(:server_url) { "http://vagrantcloud.com" } let(:dwnloader) { Vagrant::Util::Downloader.new(server_url, "/some/path", {}) } it "does nothing if we have no server set" do allow(Vagrant).to receive(:server_url).and_return(nil) VagrantPlugins::CloudCommand::Client.new(iso_env).store_token("fooboohoo") env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].headers.empty?).to eq(true) end it "does nothing if we aren't logged in" do env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].headers.empty?).to eq(true) end end context "custom server" do let(:server_url) { "http://surprise.com/box.box" } let(:dwnloader) { Vagrant::Util::Downloader.new(server_url, "/some/path", {}) } it "warns when adding token to custom server" do server_url = "https://surprise.com" allow(Vagrant).to receive(:server_url).and_return(server_url) token = "foobarbaz" VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) expect(subject).to receive(:sleep).once expect(ui).to receive(:warn).once.and_call_original env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].headers).to eq(["Authorization: Bearer #{token}"]) end end context "replacement hosts" do let(:dwnloader) { Vagrant::Util::Downloader.new("https://app.vagrantup.com", "/some/path", {}) } it "modifies host URL to target if authorized host" do token = "foobarbaz" VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].headers).to eq(["Authorization: Bearer #{token}"]) expect(URI.parse(env[:downloader].source).host).to eq(VagrantPlugins::CloudCommand::AddDownloaderAuthentication::TARGET_HOST) end end context "malformed url" do let(:bad_url) { "this is not a valid url" } let(:dwnloader) { Vagrant::Util::Downloader.new(bad_url, "/some/path", {}) } it "ignores urls that it cannot parse" do # Ensure the bad URL does cause an exception expect{ URI.parse(bad_url) }.to raise_error URI::Error env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].source).to eq(bad_url) end end context "with an headers already added" do let(:auth_header) { "Authorization Bearer: token" } let(:other_header) {"some: thing"} let(:dwnloader) { Vagrant::Util::Downloader.new(server_url, "/some/path", {headers: [other_header]}) } it "appends the auth header" do token = "foobarbaz" VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].headers).to eq([other_header, "Authorization: Bearer #{token}"]) end context "with local file path" do let(:file_path) { "file:////path/to/box.box" } let(:dwnloader) { Vagrant::Util::Downloader.new(file_path, "/some/path", {}) } it "returns original urls when not modified" do env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].source).to eq(file_path) expect(env[:downloader].headers.empty?).to eq(true) end end it "does not append multiple access_tokens" do dwnloader.headers << auth_header token = "foobarbaz" VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].headers).to eq([other_header, auth_header]) end end it "adds a token to the headers" do token = "foobarbaz" VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].headers).to eq(["Authorization: Bearer #{token}"]) end it "does not append the access token to vagrantcloud.com URLs if Atlas" do server_url = "https://atlas.hashicorp.com" allow(Vagrant).to receive(:server_url).and_return(server_url) allow(subject).to receive(:sleep) token = "foobarbaz" VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].headers.empty?).to eq(true) end context "with VAGRANT_SERVER_ACCESS_TOKEN_BY_URL environment variable set" do before do stub_env("VAGRANT_SERVER_ACCESS_TOKEN_BY_URL" => "1") end it "does not add a token to the headers" do token = "foobarbaz" VagrantPlugins::CloudCommand::Client.new(iso_env).store_token(token) env[:downloader] = dwnloader subject.call(env) expect(env[:downloader].headers).to eq([]) end end end end ================================================ FILE: test/unit/plugins/commands/cloud/auth/whoami_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/auth/whoami") describe VagrantPlugins::CloudCommand::AuthCommand::Command::Whoami do include_context "unit" let(:argv) { [] } let(: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(:client) { double("client", token: token) } let(:token) { double("token") } let(:account_username) { "account-username" } let(:account) { double("account", username: account_username) } let(:action_runner) { double("action_runner") } subject { described_class.new(argv, env) } before do allow(env).to receive(:action_runner).and_return(action_runner) allow(VagrantPlugins::CloudCommand::Client).to receive(:new).and_return(client) allow(VagrantCloud::Account).to receive(:new).and_return(account) end describe "whoami" do context "when token is unset" do let(:token) { "" } it "should output an error" do expect(env.ui).to receive(:error) subject.whoami(token) end it "should return non-zero" do r = subject.whoami(token) expect(r).not_to eq(0) expect(r).to be_a(Integer) end end context "when token is set" do let(:token) { "my-token" } it "should load an account to validate" do expect(VagrantCloud::Account).to receive(:new). with(hash_including(access_token: token)).and_return(account) subject.whoami(token) end it "should output the account username" do expect(env.ui).to receive(:success).with(/#{account_username}/) subject.whoami(token) end it "should return zero value" do expect(subject.whoami(token)).to eq(0) end context "when error is encountered" do before { allow(VagrantCloud::Account).to receive(:new).and_raise(VagrantCloud::Error::ClientError) } it "should output an error" do expect(env.ui).to receive(:error).twice subject.execute end it "should return a non-zero value" do r = subject.execute expect(r).not_to eq(0) expect(r).to be_a(Integer) end end end end describe "#execute" do before do allow(subject).to receive(:whoami) end context "with too many arguments" do let(:argv) { ["token", "token", "token"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with no argument" do it "should use stored token via client" do expect(subject).to receive(:whoami).with(token) subject.execute end end context "with token argument" do let(:token_arg) { "TOKEN_ARG" } let(:argv) { [token_arg] } it "should use the passed token" do expect(subject).to receive(:whoami).with(token_arg) subject.execute end end end end ================================================ FILE: test/unit/plugins/commands/cloud/box/create_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/box/create") describe VagrantPlugins::CloudCommand::BoxCommand::Command::Create do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box") } describe "#create_box" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(account).to receive(:organization).with(name: org_name). and_return(organization) allow(subject).to receive(:format_box_results).with(box, env) allow(organization).to receive(:add_box).and_return(box) allow(box).to receive(:save) end subject { described_class.new(argv, env) } it "should add a new box to the organization" do expect(organization).to receive(:add_box).with(box_name). and_return(box) subject.create_box(org_name, box_name, access_token, options) end it "should save the new box" do expect(box).to receive(:save) subject.create_box(org_name, box_name, access_token, options) end it "should return a zero value on success" do expect(subject.create_box(org_name, box_name, access_token, options)). to eq(0) end it "should return a non-zero value on error" do expect(box).to receive(:save).and_raise(VagrantCloud::Error) result = subject.create_box(org_name, box_name, access_token, options) expect(result).not_to eq(0) expect(result).to be_a(Integer) end context "with option set" do let(:options) { {short: short, description: description, private: priv} } let(:short) { double("short") } let(:description) { double("description") } let(:priv) { double("private") } it "should set info into box" do expect(box).to receive(:short_description=).with(short) expect(box).to receive(:description=).with(description) expect(box).to receive(:private=).with(priv) subject.create_box(org_name, box_name, access_token, options) end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } let(:box) { double("box") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login).and_return(client) allow(subject).to receive(:format_box_results) allow(subject).to receive(:create_box) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "should create the box" do expect(subject).to receive(:create_box).with(org_name, box_name, any_args) subject.execute end context "when description flag is provided" do let(:description) { "my-description" } before { argv.push("--description").push(description) } it "should create box with given description" do expect(subject).to receive(:create_box). with(org_name, box_name, access_token, hash_including(description: description)) subject.execute end end context "when short flag is provided" do let(:description) { "my-description" } before { argv.push("--short").push(description) } it "should create box with given short description" do expect(subject).to receive(:create_box). with(org_name, box_name, access_token, hash_including(short: description)) subject.execute end end context "when private flag is provided" do before { argv.push("--private") } it "should create box as private" do expect(subject).to receive(:create_box). with(org_name, box_name, access_token, hash_including(private: true)) subject.execute end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/box/delete_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/box/delete") describe VagrantPlugins::CloudCommand::BoxCommand::Command::Delete do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box") } describe "#delete_box" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_box).with(account: account, org: org_name, box: box_name). and_yield(box) allow(account).to receive(:organization).with(name: org_name). and_return(organization) allow(box).to receive(:delete) end subject { described_class.new(argv, env) } it "should return 0 on success" do expect(subject.delete_box(org_name, box_name, access_token)).to eq(0) end it "should delete the box" do expect(box).to receive(:delete) subject.delete_box(org_name, box_name, access_token) end it "should return non-zero on error" do expect(box).to receive(:delete).and_raise(VagrantCloud::Error) result = subject.delete_box(org_name, box_name, access_token) expect(result).not_to eq(0) expect(result).to be_a(Integer) end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } let(:box) { double("box") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(iso_env.ui).to receive(:ask). and_return("y") allow(subject).to receive(:delete_box) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let (:argv) { ["#{org_name}/#{box_name}"] } it "should delete the box" do expect(subject).to receive(:delete_box). with(org_name, box_name, access_token) subject.execute end it "should prompt for confirmation" do expect(iso_env.ui).to receive(:ask).and_return("y") subject.execute end context "with force flag" do before { argv.push("--force") } it "should not prompt for confirmation" do expect(iso_env.ui).not_to receive(:ask) subject.execute end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/box/show_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/box/show") describe VagrantPlugins::CloudCommand::BoxCommand::Command::Show do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box") } describe "#show_box" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_box).with(account: account, org: org_name, box: box_name). and_yield(box) allow(account).to receive(:organization).with(name: org_name). and_return(organization) allow(subject).to receive(:format_box_results) end subject { described_class.new(argv, env) } it "should return 0 on success" do expect(subject.show_box(org_name, box_name, access_token, options)).to eq(0) end it "should display the box results" do expect(subject).to receive(:format_box_results).with(box, env, {}) subject.show_box(org_name, box_name, access_token, options) end it "should return non-zero on error" do expect(subject).to receive(:with_box).and_raise(VagrantCloud::Error) result = subject.show_box(org_name, box_name, access_token, options) expect(result).not_to eq(0) expect(result).to be_a(Integer) end context "with version defined" do let(:options) { {versions: [version]} } let(:box_version) { double("box_version", version: version) } let(:box_versions) { [box_version] } let(:version) { double("version") } before do allow(box).to receive(:versions).and_return(box_versions) end it "should print the version details" do expect(subject).to receive(:format_box_results).with(box_version, env, {}) subject.show_box(org_name, box_name, access_token, options) end context "when version is not found" do let(:box_versions) { [] } it "should return non-zero" do result = subject.show_box(org_name, box_name, access_token, options) expect(result).not_to eq(0) expect(result).to be_a(Integer) end it "should not print any box information" do expect(subject).not_to receive(:format_box_results) subject.show_box(org_name, box_name, access_token, options) end end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(subject).to receive(:show_box) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let (:argv) { ["#{org_name}/#{box_name}"] } it "should show the box" do expect(subject).to receive(:show_box).with(org_name, box_name, any_args) subject.execute end it "should create the client login quietly" do expect(subject).to receive(:client_login).with(iso_env, hash_including(quiet: true)) subject.execute end context "with auth flag" do before { argv.push("--auth") } it "should set quiet option to false when creating client" do expect(subject).to receive(:client_login).with(iso_env, hash_including(quiet: false)) subject.execute end end context "with versions flag set" do let(:version_option) { "1.0.0" } before { argv.push("--versions").push(version_option) } it "should show box with version option set" do expect(subject).to receive(:show_box). with(org_name, box_name, access_token, hash_including(versions: [version_option])) subject.execute end end context "with architectures flag set" do let(:arch_option) { "test-arch" } before { argv.push("--architectures").push(arch_option) } it "shouild show box with architectures option set" do expect(subject).to receive(:show_box) do |*_, opts| expect(opts[:architectures]).to eq([arch_option]) end subject.execute end end context "with providers flag set" do let(:provider_option) { "test-provider" } before { argv.push("--providers").push(provider_option) } it "shilud show box with providers option set" do expect(subject).to receive(:show_box) do |*_, opts| expect(opts[:providers]).to eq([provider_option]) end subject.execute end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/box/update_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/box/update") describe VagrantPlugins::CloudCommand::BoxCommand::Command::Update do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box") } describe "#update_box" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_box).with(account: account, org: org_name, box: box_name). and_yield(box) allow(account).to receive(:organization).with(name: org_name). and_return(organization) allow(subject).to receive(:format_box_results) allow(box).to receive(:save) end subject { described_class.new(argv, env) } it "should save the box" do expect(box).to receive(:save) subject.update_box(org_name, box_name, access_token, options) end it "should return 0 on success" do result = subject.update_box(org_name, box_name, access_token, options) expect(result).to eq(0) end it "should return non-zero on error" do expect(box).to receive(:save).and_raise(VagrantCloud::Error) result = subject.update_box(org_name, box_name, access_token, options) expect(result).not_to eq(0) expect(result).to be_a(Integer) end it "should display the box information" do expect(subject).to receive(:format_box_results).with(box, env) subject.update_box(org_name, box_name, access_token, options) end context "with options set" do let(:options) { {short: short, description: description, private: priv} } let(:short) { double("short") } let(:description) { double("description") } let(:priv) { double("private") } it "should set box info" do expect(box).to receive(:short_description=).with(short) expect(box).to receive(:description=).with(description) expect(box).to receive(:private=).with(priv) subject.update_box(org_name, box_name, access_token, options) end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login).and_return(client) allow(subject).to receive(:update_box) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "should show help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with description flag set" do let(:description) { "my-description" } before { argv.push("--description").push(description) } it "should update box with description" do expect(subject).to receive(:update_box). with(org_name, box_name, access_token, hash_including(description: description)) subject.execute end end context "with short flag set" do let(:description) { "my-description" } before { argv.push("--short-description").push(description) } it "should update box with short description" do expect(subject).to receive(:update_box). with(org_name, box_name, access_token, hash_including(short: description)) subject.execute end end context "with private flag set" do before { argv.push("--private") } it "should update box with private" do expect(subject).to receive(:update_box). with(org_name, box_name, access_token, hash_including(private: true)) subject.execute end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/client_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/client/client") describe VagrantPlugins::CloudCommand::Client do include_context "unit" let(:env) { isolated_environment.create_vagrant_env } let(:token) { nil } let(:vc_client) { double("vagrantcloud-client", access_token: token) } subject(:client) { described_class.new(env) } before(:all) do I18n.load_path << Vagrant.source_root.join("plugins/commands/cloud/locales/en.yml") I18n.reload! end before do stub_env("ATLAS_TOKEN" => nil) stub_env("VAGRANT_CLOUD_TOKEN" => nil) allow(VagrantCloud::Client).to receive(:new).and_return(vc_client) allow(Vagrant::Util::CredentialScrubber).to receive(:sensitive) end after do described_class.reset! Vagrant::Util::CredentialScrubber.reset! end describe "#logged_in?" do before { allow(subject).to receive(:token).and_return(token) } context "when token is not set" do it "should return false" do expect(subject.logged_in?).to be_falsey end end context "when token is set" do let(:token) { double("token") } before do allow(vc_client).to receive(:authentication_token_validate) end it "should return true when token is valid" do expect(subject.logged_in?).to be_truthy end it "should validate the set token" do expect(vc_client).to receive(:authentication_token_validate) subject.logged_in? end it "should return false when token does not validate" do expect(vc_client).to receive(:authentication_token_validate). and_raise(Excon::Error::Unauthorized.new(StandardError.new)) expect(subject.logged_in?).to be_falsey end it "should add token to scrubber" do expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(token) subject.logged_in? end end end describe "#login" do let(:new_token) { double("new-token") } let(:result) { {token: new_token} } let(:password) { double("password") } let(:username) { double("username") } before do subject.username_or_email = username subject.password = password allow(vc_client).to receive(:authentication_token_create). and_return(result) end it "should add password to scrubber" do expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(password) subject.login end it "should create an authentication token" do expect(vc_client).to receive(:authentication_token_create). and_return(result) subject.login end it "should wrap remote request to handle errors" do expect(subject).to receive(:with_error_handling) subject.login end it "should add new token to scrubber" do expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(new_token) subject.login end it "should create a new internal client" do expect(VagrantCloud::Client).to receive(:new).with(access_token: new_token, url_base: anything) subject.login end it "should create authentication token using username and password" do expect(vc_client).to receive(:authentication_token_create). with(hash_including(username: username, password: password)).and_return(result) subject.login end it "should return the new token" do expect(subject.login).to eq(new_token) end context "with description and code" do let(:description) { double("description") } let(:code) { double("code") } it "should create authentication token using description and code" do expect(vc_client).to receive(:authentication_token_create).with( hash_including(username: username, password: password, description: description, code: code)) subject.login(description: description, code: code) end end end describe "#request_code" do let(:password) { double("password") } let(:username) { double("username") } let(:delivery_method) { double("delivery-method", upcase: nil) } let(:result) { {two_factor: two_factor} } let(:two_factor) { {obfuscated_destination: obfuscated_destination} } let(:obfuscated_destination) { double("obfuscated-destination", to_s: "2FA_DESTINATION") } before do subject.password = password subject.username_or_email = username allow(vc_client).to receive(:authentication_request_2fa_code).and_return(result) end it "should add password to scrubber" do expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(password) subject.request_code(delivery_method) end it "should request the code" do expect(vc_client).to receive(:authentication_request_2fa_code).with( hash_including(username: username, password: password, delivery_method: delivery_method)) subject.request_code(delivery_method) end it "should print the destination" do expect(env.ui).to receive(:success).with(/2FA_DESTINATION/) subject.request_code(delivery_method) end end describe "#store_token" do let(:token_path) { double("token-path") } let(:new_token) { double("new-token") } before do allow(subject).to receive(:token_path).and_return(token_path) allow(token_path).to receive(:open) end it "should add token to scrubber" do expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with(new_token) subject.store_token(new_token) end it "should create a new internal client with token" do expect(VagrantCloud::Client).to receive(:new).with(access_token: new_token, url_base: anything) subject.store_token(new_token) end it "should open the token path and write the new token" do f = double("file") expect(token_path).to receive(:open).with("w").and_yield(f) expect(f).to receive(:write).with(new_token) subject.store_token(new_token) end end describe "#token" do let(:env_token) { "ENV_TOKEN" } let(:file_token) { "FILE_TOKEN" } let(:token_path) { double("token-path", read: file_token) } let(:path_exists) { false } before do allow(subject).to receive(:token).and_call_original allow(subject).to receive(:token_path).and_return(token_path) allow(token_path).to receive(:exist?).and_return(path_exists) end context "when VAGRANT_CLOUD_TOKEN env var is set" do before { stub_env("VAGRANT_CLOUD_TOKEN" => env_token) } it "should return the env token" do expect(subject.token).to eq(env_token) end context "when token path exists" do let(:path_exists) { true } it "should return the env token" do expect(subject.token).to eq(env_token) end it "should print warning of two tokens" do expect(env.ui).to receive(:warn) subject.token end it "should only print warning of two tokens once" do expect(env.ui).to receive(:warn).with(/detected/).once 3.times { subject.token } end end end context "when token path exists" do let(:path_exists) { true } it "should return the stored token" do expect(subject.token).to eq(file_token) end context "when VAGRANT_CLOUD_TOKEN env var is set" do before { stub_env("VAGRANT_CLOUD_TOKEN" => env_token) } it "should return the env token" do expect(subject.token).to eq(env_token) end end end context "when ATLAS_TOKEN env var is set" do before { stub_env("ATLAS_TOKEN" => env_token) } it "should return the env token" do expect(subject.token).to eq(env_token) end context "when VAGRANT_CLOUD_TOKEN is set" do let(:vc_token) { "VC_TOKEN" } before { stub_env("VAGRANT_CLOUD_TOKEN" => vc_token) } it "should return the VAGRANT_CLOUD_TOKEN value" do expect(subject.token).to eq(vc_token) end end context "when file exists" do let(:path_exists) { true } it "should return the file token" do expect(subject.token).to eq(file_token) end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/list_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/list") describe VagrantPlugins::CloudCommand::Command::List do include_context "unit" let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) end end ================================================ FILE: test/unit/plugins/commands/cloud/provider/create_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/provider/create") describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Create do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:box_version) { "0.1.0" } let(:provider_name) { "my-provider" } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version, providers: [provider]) } let(:provider) { double("provider", name: provider_name) } let(:provider_url) { double("provider_url") } describe "#create_provider" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_version).with(account: account, org: org_name, box: box_name, version: box_version). and_yield(version) allow(account).to receive(:organization).with(name: org_name). and_return(organization) allow(version).to receive(:add_provider).and_return(provider) allow(provider).to receive(:save) allow(provider).to receive(:url=) allow(subject).to receive(:format_box_results) end subject { described_class.new(argv, env) } it "should add a new provider to the box version" do expect(version).to receive(:add_provider).with(provider_name, nil) subject.create_provider(org_name, box_name, box_version, provider_name, provider_url, access_token, options) end it "should not set checksum or checksum_type when not provided" do expect(provider).not_to receive(:checksum=) expect(provider).not_to receive(:checksum_type=) subject.create_provider(org_name, box_name, box_version, provider_name, provider_url, access_token, options) end context "with checksum and checksum type options set" do let(:checksum) { double("checksum") } let(:checksum_type) { double("checksum_type") } let(:options) { {checksum: checksum, checksum_type: checksum_type} } it "should set the checksum and checksum type" do expect(provider).to receive(:checksum=).with(checksum) expect(provider).to receive(:checksum_type=).with(checksum_type) subject.create_provider(org_name, box_name, box_version, provider_name, provider_url, access_token, options) end end context "when URL is set" do it "should set the URL" do expect(provider).to receive(:url=).with(provider_url) subject.create_provider(org_name, box_name, box_version, provider_name, provider_url, access_token, options) end end context "when URL is not set" do let(:provider_url) { nil } it "should not set the URL" do expect(provider).not_to receive(:url=).with(provider_url) subject.create_provider(org_name, box_name, box_version, provider_name, provider_url, access_token, options) end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(subject).to receive(:create_provider) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with provider argument" do let(:provider_arg) { "my-provider" } before { argv << provider_arg } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with version argument" do let(:version_arg) { "1.0.0" } before { argv << version_arg } it "should create the provider" do expect(subject).to receive(:create_provider).with(org_name, box_name, version_arg, provider_arg, any_args) subject.execute end it "should not provide URL value" do expect(subject).to receive(:create_provider).with(org_name, box_name, version_arg, provider_arg, nil, any_args) subject.execute end context "with URL argument" do let(:url_arg) { "provider-url" } before { argv << url_arg } it "should provide the URL value" do expect(subject).to receive(:create_provider).with(org_name, box_name, version_arg, provider_arg, url_arg, any_args) subject.execute end end context "with checksum and checksum type flags" do let(:checksum_arg) { "checksum" } let(:checksum_type_arg) { "checksum_type" } before { argv.push("--checksum").push(checksum_arg).push("--checksum-type").push(checksum_type_arg) } it "should include the checksum options" do expect(subject).to receive(:create_provider). with(org_name, box_name, version_arg, provider_arg, any_args, hash_including(checksum: checksum_arg, checksum_type: checksum_type_arg)) subject.execute end end it "should include detected host architecture by default" do host_arch = double("host-arch") expect(Vagrant::Util::Platform).to receive(:architecture).and_return(host_arch) expect(subject).to receive(:create_provider) do |*_, opts| expect(opts[:architecture]).to eq(host_arch) end subject.execute end context "with architecture flag" do let(:arch_option) { "test-arch" } before { argv.push("--architecture").push(arch_option) } it "should include the architecture option" do expect(subject).to receive(:create_provider) do |*args, opts| expect(opts[:architecture]).to eq(arch_option) end subject.execute end end context "with default architecture flag" do before { argv.push(default_arch_flag) } context "when flag is enabled" do let(:default_arch_flag) { "--default-architecture" } it "should include default architecture with true value" do expect(subject).to receive(:create_provider) do |*args, opts| expect(opts[:default_architecture]).to be(true) end subject.execute end end context "when flag is disabled" do let(:default_arch_flag) { "--no-default-architecture" } it "should include default architecture with false value" do expect(subject).to receive(:create_provider) do |*args, opts| expect(opts[:default_architecture]).to be(false) end subject.execute end end end end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/provider/delete_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/provider/delete") describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Delete do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:box_version) { "1.0.0" } let(:box_version_provider) { "my-provider" } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version, providers: [provider]) } let(:provider) { double("provider", name: box_version_provider, architecture: architecture) } let(:architecture) { double("test-architecture") } describe "#delete_provider" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_provider). with( account: account, org: org_name, box: box_name, version: box_version, provider: box_version_provider, architecture: architecture ).and_yield(provider) allow(provider).to receive(:delete) end subject { described_class.new(argv, env) } it "should delete the provider" do expect(provider).to receive(:delete) subject.delete_provider(org_name, box_name, box_version, box_version_provider, architecture, account, options) end it "should return zero on success" do r = subject.delete_provider(org_name, box_name, box_version, box_version_provider, architecture, account, options) expect(r).to eq(0) end context "when error is encountered" do before do expect(provider).to receive(:delete).and_raise(VagrantCloud::Error) end it "should return non-zero" do r = subject.delete_provider(org_name, box_name, box_version, box_version_provider, architecture, account, options) expect(r).to be_a(Integer) expect(r).not_to eq(0) end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(VagrantCloud::Account).to receive(:new).and_return(account) allow(account).to receive(:organization).with(name: org_name). and_return(organization) allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(iso_env.ui).to receive(:ask). and_return("y") allow(subject).to receive(:select_provider_architecture). and_return(architecture) allow(subject).to receive(:delete_provider) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with provider argument" do let(:provider_arg) { "my-provider" } before { argv << provider_arg } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with version argument" do let(:version_arg) { "1.0.0" } before { argv << version_arg } it "should delete the provider" do expect(subject).to receive(:delete_provider). with(org_name, box_name, version_arg, provider_arg, architecture, account, anything) subject.execute end it "should prompt for confirmation" do expect(iso_env.ui).to receive(:ask).and_return("y") subject.execute end context "with architecture argument" do let(:architecture_argument) { "test-arch" } before { argv << architecture_argument } it "should delete the provider" do expect(subject).to receive(:delete_provider). with(org_name, box_name, version_arg, provider_arg, architecture_argument, account, anything) subject.execute end it "should not attempt to select the architecture" do expect(subject).not_to receive(:select_provider_architecture) subject.execute end end context "with multiple provider architectures" do let(:box_version) { double("box-version", providers: providers) } let(:providers) { [ double("dummy-provider", architecture: "amd64"), double("dummy-provider", architecture: "arm64") ] } before do expect(subject).to receive(:select_provider_architecture).and_call_original expect(subject).to receive(:with_version).and_yield(box_version) end it "should prompt for architecture selection" do expect(iso_env.ui).to receive(:ask).and_return("amd64") subject.execute end end context "with force flag" do before { argv << "--force" } it "should not prompt for confirmation" do expect(iso_env.ui).not_to receive(:ask) subject.execute end end end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/provider/update_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/provider/update") describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Update do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:box_version) { "1.0.0" } let(:box_version_provider) { "my-provider" } let(:box_architecture) { "box-architecture" } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version, provdiers: [provider]) } let(:provider) { double("provider", name: box_version_provider) } let(:provider_url) { nil } describe "#update_provider" do let(:argv) { [] } let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_provider). with( account: account, org: org_name, box: box_name, version: box_version, provider: box_version_provider, architecture: box_architecture ). and_yield(provider) allow(provider).to receive(:save) allow(subject).to receive(:format_box_results) end subject { described_class.new(argv, env) } it "should update the provider" do expect(provider).to receive(:save) subject.update_provider( org_name, box_name, box_version, box_version_provider, box_architecture, provider_url, access_token, options ) end it "should return 0 on success" do result = subject.update_provider( org_name, box_name, box_version, box_version_provider, box_architecture, provider_url, access_token, options ) expect(result).to eq(0) end it "should return non-zero result on error" do expect(provider).to receive(:save).and_raise(VagrantCloud::Error) result = subject.update_provider( org_name, box_name, box_version, box_version_provider, box_architecture, provider_url, access_token, options ) expect(result).not_to eq(0) expect(result).to be_a(Integer) end it "should not update the URL when unset" do expect(provider).not_to receive(:url=) subject.update_provider( org_name, box_name, box_version, box_version_provider, box_architecture, provider_url, access_token, options ) end context "when URL is set" do let(:provider_url) { double("provider-url") } it "should update the URL" do expect(provider).to receive(:url=).with(provider_url) subject.update_provider( org_name, box_name, box_version, box_version_provider, box_architecture, provider_url, access_token, options ) end end context "with options set" do let(:checksum) { double("checksum") } let(:checksum_type) { double("checksum_type") } let(:options) { {} } after do subject.update_provider( org_name, box_name, box_version, box_version_provider, box_architecture, provider_url, access_token, options ) end it "should not modify option controlled values when unset" do expect(provider).not_to receive(:checksum=) expect(provider).not_to receive(:checksum_type=) expect(provider).not_to receive(:architecture=) expect(provider).not_to receive(:default_architecture=) end context "with checksum options set" do let(:options) { {checksum: checksum, checksum_type: checksum_type} } it "should set checksum options before saving" do expect(provider).to receive(:checksum=).with(checksum) expect(provider).to receive(:checksum_type=).with(checksum_type) end end context "with architecture option set" do let(:architecture) { double("architecture") } let(:options) { {architecture: architecture} } it "should set architecture before saving" do expect(provider).to receive(:architecture=).with(architecture) end end context "with default architecture option set" do context "with true value" do let(:options) { {default_architecture: true} } it "should set default architecture to true" do expect(provider).to receive(:default_architecture=).with(true) end end context "with false value" do let(:options) { {default_architecture: false} } it "should set default architecture to false" do expect(provider).to receive(:default_architecture=).with(false) end end end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(subject).to receive(:update_provider) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with provider argument" do let(:provider_arg) { "my-provider" } before { argv << provider_arg } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with version argument" do let(:version_arg) { "1.0.0" } before { argv << version_arg } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with architecture argument" do let(:architecture_arg) { "box-architecture" } before { argv << architecture_arg } it "should create the provider" do expect(subject). to receive(:update_provider). with( org_name, box_name, version_arg, provider_arg, architecture_arg, any_args ) subject.execute end it "should not provide URL value" do expect(subject). to receive(:update_provider). with( org_name, box_name, version_arg, provider_arg, architecture_arg, nil, any_args ) subject.execute end context "with URL argument" do let(:url_arg) { "provider-url" } before { argv << url_arg } it "should provide the URL value" do expect(subject). to receive(:update_provider). with( org_name, box_name, version_arg, provider_arg, architecture_arg, url_arg, any_args ) subject.execute end end context "with checksum and checksum type flags" do let(:checksum_arg) { "checksum" } let(:checksum_type_arg) { "checksum_type" } before { argv.push("--checksum").push(checksum_arg).push("--checksum-type").push(checksum_type_arg) } it "should include the checksum options" do expect(subject). to receive(:update_provider). with( org_name, box_name, version_arg, provider_arg, architecture_arg, any_args, hash_including( checksum: checksum_arg, checksum_type: checksum_type_arg ) ) subject.execute end end context "with architecture flag" do let(:architecture_flag) { "test-arch" } before { argv.push("--architecture").push(architecture_flag) } it "should include the architecture flag" do expect(subject).to receive(:update_provider) do |*_, opts| expect(opts[:architecture]).to eq(architecture_flag) end subject.execute end end context "with default architecture flag" do context "enabled" do before { argv.push("--default-architecture") } it "should include default architecture set to true" do expect(subject).to receive(:update_provider) do |*_, opts| expect(opts[:default_architecture]).to be(true) end subject.execute end end context "disabled" do before { argv.push("--no-default-architecture") } it "should include default architecture set to false" do expect(subject).to receive(:update_provider) do |*_, opts| expect(opts[:default_architecture]).to be(false) end subject.execute end end end end end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/provider/upload_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/provider/upload") describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Upload do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:box_version) { "1.0.0" } let(:box_version_provider) { "my-provider" } let(:box_version_provider_arch) { "amd64" } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version, providers: [provider]) } let(:provider) { double("provider", name: box_version_provider, architecture: box_version_provider_arch) } let(:provider_file) { double("provider-file") } let(:provider_file_size) { 1 } describe "#upload_provider" do let(:argv) { [] } let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:upload_url) { double("upload-url") } let(:uploader) { double("uploader") } before do allow(I18n).to receive(:t) allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_version). with(account: account, org: org_name, box: box_name, version: box_version). and_yield(version) allow(provider).to receive(:upload).and_yield(upload_url) allow(uploader).to receive(:upload!) allow(Vagrant::UI::Prefixed).to receive(:new).with(ui, "cloud").and_return(ui) allow(Vagrant::Util::Uploader).to receive(:new).and_return(uploader) allow(File).to receive(:stat).with(provider_file). and_return(double("provider-stat", size: provider_file_size)) end subject { described_class.new(argv, env) } it "should upload the provider file" do expect(provider).to receive(:upload) subject.upload_provider(org_name, box_name, box_version, box_version_provider, box_version_provider_arch, provider_file, access_token, options) end it "should return zero on success" do r = subject.upload_provider(org_name, box_name, box_version, box_version_provider, box_version_provider_arch, provider_file, access_token, options) expect(r).to eq(0) end it "should return non-zero on API error" do expect(provider).to receive(:upload).and_raise(VagrantCloud::Error) r = subject.upload_provider(org_name, box_name, box_version, box_version_provider, box_version_provider_arch, provider_file, access_token, options) expect(r).not_to eq(0) expect(r).to be_a(Integer) end it "should return non-zero on upload error" do expect(provider).to receive(:upload).and_raise(Vagrant::Errors::UploaderError) r = subject.upload_provider(org_name, box_name, box_version, box_version_provider, box_version_provider_arch, provider_file, access_token, options) expect(r).not_to eq(0) expect(r).to be_a(Integer) end it "should should upload via uploader" do expect(uploader).to receive(:upload!) subject.upload_provider(org_name, box_name, box_version, box_version_provider, box_version_provider_arch, provider_file, access_token, options) end it "should not use direct upload by default" do expect(provider).to receive(:upload) do |**args| expect(args[:direct]).to be_falsey end subject.upload_provider(org_name, box_name, box_version, box_version_provider, box_version_provider_arch, provider_file, access_token, options) end context "with direct option" do let(:options) { {direct: true} } it "should use direct upload" do expect(provider).to receive(:upload) do |**args| expect(args[:direct]).to be_truthy end subject.upload_provider(org_name, box_name, box_version, box_version_provider, box_version_provider_arch, provider_file, access_token, options) end context "when file size is 5GB" do let(:provider_file_size) { 5368709120 } it "should use direct upload" do expect(provider).to receive(:upload) do |**args| expect(args[:direct]).to be_truthy end subject.upload_provider(org_name, box_name, box_version, box_version_provider, box_version_provider_arch, provider_file, access_token, options) end end context "when file size is greater than 5GB" do let(:provider_file_size) { 5368709121 } it "should disable direct upload" do expect(provider).to receive(:upload) do |**args| expect(args[:direct]).to be_falsey end subject.upload_provider(org_name, box_name, box_version, box_version_provider, box_version_provider_arch, provider_file, access_token, options) end end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(subject).to receive(:upload_provider) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with provider argument" do let(:provider_arg) { "my-provider" } before { argv << provider_arg } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with version argument" do let(:version_arg) { "1.0.0" } before { argv << version_arg } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with architecture argument" do let(:arch_arg) { "amd64" } before { argv << arch_arg } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with file argument" do let(:file_arg) { "/dev/null/file" } before { argv << file_arg } it "should upload the provider file" do expect(subject).to receive(:upload_provider). with(org_name, box_name, version_arg, provider_arg, arch_arg, file_arg, any_args) subject.execute end it "should do direct upload by default" do expect(subject).to receive(:upload_provider). with(any_args, hash_including(direct: true)) subject.execute end context "with --no-direct flag" do before { argv << "--no-direct" } it "should not perform direct upload" do expect(subject).to receive(:upload_provider). with(any_args, hash_including(direct: false)) subject.execute end end end end end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/publish_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/publish") describe VagrantPlugins::CloudCommand::Command::Publish do include_context "unit" let(:argv) { [] } let(:iso_env) { double("iso_env") } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box") } let(:box_size) { 1 } let(:version) { double("version") } let(:provider) { double("provider") } let(:uploader) { double("uploader") } let(:ui) { Vagrant::UI::Silent.new } let(:upload_url) { double("upload_url") } let(:access_token) { double("access_token") } let(:default_architecture) { double("default-architecture") } subject { described_class.new(argv, iso_env) } before do allow(Vagrant::Util::Platform).to receive(:architecture). and_return(default_architecture) allow(iso_env).to receive(:ui).and_return(ui) allow(File).to receive(:stat).with(box). and_return(double("box_stat", size: box_size)) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: anything). and_return(account) allow(provider).to receive(:architecture=).with(default_architecture) end describe "#upload_box_file" do before do allow(provider).to receive(:upload).and_yield(upload_url) allow(uploader).to receive(:upload!) allow(File).to receive(:absolute_path).and_return(box) allow(Vagrant::Util::Uploader).to receive(:new).and_return(uploader) end it "should get absolute path for box file" do expect(File).to receive(:absolute_path).and_return(box) subject.upload_box_file(provider, box) end it "should upload through provider" do expect(provider).to receive(:upload).and_return(upload_url) subject.upload_box_file(provider, box) end it "should create uploader with given url" do expect(Vagrant::Util::Uploader).to receive(:new). with(upload_url, any_args).and_return(uploader) subject.upload_box_file(provider, box) end it "should upload with PUT method by default" do expect(Vagrant::Util::Uploader).to receive(:new). with(upload_url, anything, hash_including(method: :put)).and_return(uploader) subject.upload_box_file(provider, box) end context "with direct upload option enabled" do it "should upload with PUT method when direct upload option set" do expect(Vagrant::Util::Uploader).to receive(:new). with(upload_url, anything, hash_including(method: :put)).and_return(uploader) subject.upload_box_file(provider, box, direct_upload: true) end context "with box size of 5GB" do let(:box_size) { 5368709120 } it "should upload using direct to storage option" do expect(provider).to receive(:upload).with(direct: true) subject.upload_box_file(provider, box, direct_upload: true) end end context "with box size greater than 5GB" do let(:box_size) { 5368709121 } it "should disable direct to storage upload" do expect(provider).to receive(:upload).with(direct: false) subject.upload_box_file(provider, box, direct_upload: true) end end end end describe "#release_version" do it "should release the version" do expect(version).to receive(:release) subject.release_version(version) end end describe "#set_box_info" do context "with no options set" do let(:options) { {} } it "should not modify the box" do subject.set_box_info(box, options) end end context "with options set" do let(:priv) { double("private") } let(:short_description) { double("short_description") } let(:description) { double("description") } let(:options) { {private: priv, description: description, short_description: short_description} } it "should set info on box" do expect(box).to receive(:private=).with(priv) expect(box).to receive(:short_description=).with(short_description) expect(box).to receive(:description=).with(description) subject.set_box_info(box, options) end end end describe "#set_version_info" do context "with no options set" do let(:options) { {} } it "should not modify the verison" do subject.set_version_info(version, options) end end context "with options set" do let(:options) { {version_description: version_description} } let(:version_description) { double("version_description") } it "should set info on version" do expect(version).to receive(:description=).with(version_description) subject.set_version_info(version, options) end end end describe "#set_provider_info" do context "with no options set" do let(:options) { {} } it "should not modify the provider" do expect(provider).not_to receive(:url=) expect(provider).not_to receive(:checksum=) expect(provider).not_to receive(:checksum_type=) expect(provider).not_to receive(:architecture=) expect(provider).not_to receive(:default_architecture=) subject.set_provider_info(provider, options) end end context "with options" do let(:options) { {} } let(:url) { double("url") } let(:checksum) { double("checksum") } let(:checksum_type) { double("checksum_type") } let(:architecture) { double("architecture") } after { subject.set_provider_info(provider, options) } context "with url set" do before { options[:url] = url } it "should set url on provider" do expect(provider).to receive(:url=).with(url) end end context "with checksum set" do before do options[:checksum] = checksum options[:checksum_type] = checksum_type end it "should set checksum on provider" do expect(provider).to receive(:checksum=).with(checksum) expect(provider).to receive(:checksum_type=).with(checksum_type) end end context "with architecture set" do before { options[:architecture] = architecture } it "should set architecture on provider" do expect(provider).to receive(:architecture=).with(architecture) end end context "with default architecture set" do context "with true value" do before { options[:default_architecture] = true } it "should set default architecture to true" do expect(provider).to receive(:default_architecture=).with(true) end end context "with false value" do before { options[:default_architecture] = false } it "should set default architecture to false" do expect(provider).to receive(:default_architecture=).with(false) end end end end end describe "load_box_version" do let(:box_version) { "1.0.0" } context "when version exists" do before do allow(box).to receive(:versions).and_return([version]) allow(version).to receive(:version).and_return(box_version) end it "should return the existing version" do expect(subject.load_box_version(box, box_version)).to eq(version) end end context "when version does not exist" do let(:new_version) { double("new_version") } before do allow(box).to receive(:versions).and_return([version]) allow(version).to receive(:version) end it "should add a new version" do expect(box).to receive(:add_version).with(box_version). and_return(new_version) expect(subject.load_box_version(box, box_version)).to eq(new_version) end end end describe "#load_box" do let(:org_name) { "org-name" } let(:box_name) { "my-box" } before do allow(account).to receive(:organization).with(name: org_name). and_return(organization) end context "when box exists" do before do allow(box).to receive(:name).and_return(box_name) allow(organization).to receive(:boxes).and_return([box]) end it "should return the existing box" do expect(subject.load_box(org_name, box_name, access_token)).to eq(box) end end context "when box does not exist" do let(:new_box) { double("new_box") } before do allow(organization).to receive(:boxes).and_return([]) end it "should add a new box to organization" do expect(organization).to receive(:add_box).with(box_name). and_return(new_box) expect(subject.load_box(org_name, box_name, access_token)).to eq(new_box) end end end context "#execute" do 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(:client) { double("client", token: "1234token1234") } let(:action_runner) { double("action_runner") } let(:box_path) { "path/to/the/virtualbox.box" } let(:full_box_path) { "/full/#{box_path}" } let(:box) { full_box_path } before do allow(iso_env).to receive(:action_runner). and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(subject).to receive(:format_box_results) allow(iso_env.ui).to receive(:ask).and_return("y") allow(File).to receive(:absolute_path).with(box_path) .and_return("/full/#{box_path}") allow(File).to receive(:file?).with(box_path) .and_return(true) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "missing required arguments" do let(:argv) { ["vagrant/box", "1.0.0", "virtualbox"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "missing box file" do let(:argv) { ["vagrant/box", "1.0.0", "virtualbox", "/notreal/file.box"] } it "raises an exception" do allow(File).to receive(:file?).and_return(false) expect { subject.execute }. to raise_error(Vagrant::Errors::BoxFileNotExist) end end context "with arguments" do let(:org_name) { "vagrant" } let(:box_name) { "box" } let(:box_version) { "1.0.0" } let(:box_version_provider) { "virtualbox" } let(:argv) { [ "#{org_name}/#{box_name}", box_version, box_version_provider, box_path ] } before do allow(account).to receive(:organization).with(name: org_name). and_return(organization) allow(subject).to receive(:load_box).and_return(box) allow(subject).to receive(:load_box_version).and_return(version) allow(subject).to receive(:load_version_provider).and_return(provider) allow(provider).to receive(:upload) allow(box).to receive(:save) end it "should prompt user for confirmation" do expect(iso_env.ui).to receive(:ask).and_return("y") expect(subject.execute).to eq(0) end context "when --force option is provided" do before { argv << "--force" } it "should not prompt user for confirmation" do expect(iso_env.ui).not_to receive(:ask) expect(subject.execute).to eq(0) end end context "when --release option is provided" do before do argv << "--release" end it "should release box version when not released" do expect(version).to receive(:released?).and_return(false) expect(version).to receive(:release) expect(subject.execute).to eq(0) end end context "when Vagrant Cloud error is encountered" do before { expect(box).to receive(:save).and_raise(VagrantCloud::Error) } it "should return non-zero result" do result = subject.execute expect(result).not_to eq(0) expect(result).to be_a(Integer) end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/search_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/search") describe VagrantPlugins::CloudCommand::Command::Search do include_context "unit" let(:token) { double("token") } let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } describe "#search" do let(:query) { double("query") } let(:options) { {} } let(:account) { double("account", searcher: searcher) } let(:searcher) { double("searcher") } let(:results) { double("results", boxes: boxes) } let(:boxes) { [] } before do allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: token).and_return(account) allow(searcher).to receive(:search).and_return(results) allow(subject).to receive(:format_search_results) end it "should perform search" do expect(searcher).to receive(:search).with(hash_including(query: query)).and_return(results) subject.search(query, token, options) end it "should print a warning when no results are found" do expect(iso_env.ui).to receive(:warn) subject.search(query, token, options) end context "with valid options" do let(:architecture) { double("architecture") } let(:provider) { double("provider") } let(:sort) { double("sort") } let(:order) { double("order") } let(:limit) { double("limit") } let(:page) { double("page") } let(:options) { { architecture: architecture, provider: provider, sort: sort, order: order, limit: limit, page: page } } it "should use options when performing search" do expect(searcher).to receive(:search) do |**args| options.each_pair do |k, v| expect(args[k]).to eq(v) end results end subject.search(query, token, options) end context "with invalid options" do before { options[:invalid_option] = "testing" } it "should only pass supported options to search" do expect(searcher).to receive(:search) do |**args| options.each_pair do |k, v| next if k == :invalid_option expect(args[k]).to eq(v) end expect(args.key?(:invalid_option)).to be_falsey results end subject.search(query, token, options) end end end context "with search results" do let(:results) { double("results", boxes: [double("result")]) } it "should format the results" do expect(subject).to receive(:format_search_results).with(results.boxes, any_args) subject.search(query, token, options) end context "with format options" do let(:options) { {short: true, json: false} } it "should pass options to format" do expect(subject).to receive(:format_search_results).with(results.boxes, true, false, iso_env) subject.search(query, token, options) end end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: token) } let(:box) { double("box") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login).and_return(client) allow(subject).to receive(:search) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with query argument" do let(:query_arg) { "search-query" } before { argv << query_arg } it "should run the search" do expect(subject).to receive(:search).with(query_arg, any_args) subject.execute end it "should setup client login quietly by default" do expect(subject).to receive(:client_login).with(iso_env, hash_including(quiet: true)) subject.execute end context "with --auth flag" do before { argv << "--auth" } it "should not setup login client quietly" do expect(subject).to receive(:client_login).with(iso_env, hash_including(quiet: false)) subject.execute end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/version/create_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/version/create") describe VagrantPlugins::CloudCommand::VersionCommand::Command::Create do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:box_version) { double("box_version") } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version) } describe "#create_version" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_box).with(account: account, org: org_name, box: box_name). and_yield(box) allow(account).to receive(:organization).with(name: org_name). and_return(organization) allow(box).to receive(:add_version).and_return(version) allow(version).to receive(:save) allow(subject).to receive(:format_box_results) end subject { described_class.new(argv, env) } it "should add a new version to the box" do expect(box).to receive(:add_version).with(box_version) subject.create_version(org_name, box_name, box_version, access_token, options) end it "should save the new version" do expect(version).to receive(:save) subject.create_version(org_name, box_name, box_version, access_token, options) end it "should return 0 on success" do result = subject.create_version(org_name, box_name, box_version, access_token, options) expect(result).to eq(0) end it "should return non-zero on error" do expect(version).to receive(:save).and_raise(VagrantCloud::Error) result = subject.create_version(org_name, box_name, box_version, access_token, options) expect(result).not_to eq(0) expect(result).to be_a(Integer) end context "with description option set" do let(:description) { double("description") } let(:options) { {description: description} } it "should set description on version" do expect(version).to receive(:description=).with(description) subject.create_version(org_name, box_name, box_version, access_token, options) end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(subject).to receive(:create_version) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with version argument" do let(:version_arg) { "1.0.0" } before { argv << version_arg } it "should create the version" do expect(subject).to receive(:create_version).with(org_name, box_name, version_arg, any_args) subject.execute end context "with description flag" do let(:description) { "my-description" } before { argv.push("--description").push(description) } it "should create version with description option set" do expect(subject).to receive(:create_version). with(org_name, box_name, version_arg, access_token, hash_including(description: description)) subject.execute end end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/version/delete_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/version/delete") describe VagrantPlugins::CloudCommand::VersionCommand::Command::Delete do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:box_version) { double("box_version") } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version) } describe "#delete_version" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_version). with(account: account, org: org_name, box: box_name, version: box_version). and_yield(version) allow(version).to receive(:delete) end subject { described_class.new(argv, env) } it "should delete the version" do expect(version).to receive(:delete) subject.delete_version(org_name, box_name, box_version, access_token, options) end it "should return 0 on success" do result = subject.delete_version(org_name, box_name, box_version, access_token, options) expect(result).to eq(0) end it "should return non-zero on error" do expect(version).to receive(:delete).and_raise(VagrantCloud::Error) result = subject.delete_version(org_name, box_name, box_version, access_token, options) expect(result).not_to eq(0) expect(result).to be_a(Integer) end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(iso_env.ui).to receive(:ask). and_return("y") allow(subject).to receive(:delete_version) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with version argument" do let(:version_arg) { "1.0.0" } before { argv << version_arg } it "should delete the version" do expect(subject).to receive(:delete_version). with(org_name, box_name, version_arg, access_token, anything) subject.execute end it "should prompt for confirmation" do expect(iso_env.ui).to receive(:ask).and_return("y") subject.execute end context "with force flag" do before { argv << "--force" } it "should not prompt for confirmation" do expect(iso_env.ui).not_to receive(:ask) subject.execute end end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/version/release_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/version/release") describe VagrantPlugins::CloudCommand::VersionCommand::Command::Release do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:box_version) { double("box_version") } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version) } describe "#release_version" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_version). with(account: account, org: org_name, box: box_name, version: box_version). and_yield(version) allow(version).to receive(:release) end subject { described_class.new(argv, env) } it "should release the version" do expect(version).to receive(:release) subject.release_version(org_name, box_name, box_version, access_token, options) end it "should return 0 on success" do result = subject.release_version(org_name, box_name, box_version, access_token, options) expect(result).to eq(0) end it "should return a non-zero on error" do expect(version).to receive(:release).and_raise(VagrantCloud::Error) result = subject.release_version(org_name, box_name, box_version, access_token, options) expect(result).not_to eq(0) expect(result).to be_a(Integer) end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(subject).to receive(:release_version) allow(iso_env.ui).to receive(:ask).and_return("y") end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with version argument" do let(:version_arg) { "1.0.0" } before { argv << version_arg } it "should release the version" do expect(subject).to receive(:release_version). with(org_name, box_name, version_arg, access_token, anything) subject.execute end it "should prompt for confirmation" do expect(iso_env.ui).to receive(:ask).and_return("y") subject.execute end context "with force flag" do before { argv << "--force" } it "should not prompt for confirmation" do expect(iso_env.ui).not_to receive(:ask) subject.execute end end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/version/revoke_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/version/revoke") describe VagrantPlugins::CloudCommand::VersionCommand::Command::Revoke do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:box_version) { double("box_version") } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version) } describe "#revoke_version" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_version). with(account: account, org: org_name, box: box_name, version: box_version). and_yield(version) allow(version).to receive(:revoke) allow(subject).to receive(:format_box_results) end subject { described_class.new(argv, env) } it "should revoke the version" do expect(version).to receive(:revoke) subject.revoke_version(org_name, box_name, box_version, access_token, options) end it "should return 0 on success" do result = subject.revoke_version(org_name, box_name, box_version, access_token, options) expect(result).to eq(0) end it "should return non-zero on error" do expect(version).to receive(:revoke).and_raise(VagrantCloud::Error) result = subject.revoke_version(org_name, box_name, box_version, access_token, options) expect(result).not_to eq(0) expect(result).to be_a(Integer) end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(subject).to receive(:revoke_version) allow(iso_env.ui).to receive(:ask). and_return("y") end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with box name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with version argument" do let(:version_arg) { "1.0.0" } before { argv << version_arg } it "should revoke the version" do expect(subject).to receive(:revoke_version). with(org_name, box_name, version_arg, access_token, anything) subject.execute end it "should prompt for confirmation" do expect(iso_env.ui).to receive(:ask).and_return("y") subject.execute end context "with force flag" do before { argv << "--force" } it "should not prompt for confirmation" do expect(iso_env.ui).not_to receive(:ask) subject.execute end end end end end end ================================================ FILE: test/unit/plugins/commands/cloud/version/update_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/cloud/version/update") describe VagrantPlugins::CloudCommand::VersionCommand::Command::Update do include_context "unit" let(:access_token) { double("token") } let(:org_name) { "my-org" } let(:box_name) { "my-box" } let(:box_version) { double("box_version") } let(:account) { double("account") } let(:organization) { double("organization") } let(:box) { double("box", versions: [version]) } let(:version) { double("version", version: box_version) } describe "#update_version" do let(:options) { {} } let(:env) { double("env", ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:argv) { [] } before do allow(env).to receive(:ui).and_return(ui) allow(VagrantCloud::Account).to receive(:new). with(custom_server: anything, access_token: access_token). and_return(account) allow(subject).to receive(:with_version). with(account: account, org: org_name, box: box_name, version: box_version). and_yield(version) allow(version).to receive(:save) allow(subject).to receive(:format_box_results) end subject { described_class.new(argv, env) } it "should update the version" do expect(version).to receive(:save) subject.update_version(org_name, box_name, box_version, access_token, options) end it "should return 0 on success" do result = subject.update_version(org_name, box_name, box_version, access_token, options) expect(result).to eq(0) end it "should return non-zero result on error" do expect(version).to receive(:save).and_raise(VagrantCloud::Error) result = subject.update_version(org_name, box_name, box_version, access_token, options) expect(result).not_to eq(0) expect(result).to be_a(Integer) end context "with options set" do let(:description) { double("description") } let(:options) { {description: description} } it "should set version info before saving" do expect(version).to receive(:description=).with(description) subject.update_version(org_name, box_name, box_version, access_token, options) end end end describe "#execute" do let(:argv) { [] } 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 subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:client) { double("client", token: access_token) } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:client_login). and_return(client) allow(subject).to receive(:update_version) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with name argument" do let(:argv) { ["#{org_name}/#{box_name}"] } it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "with version argument" do let(:version_arg) { "1.0.0" } before { argv << version_arg } it "should update the version" do expect(subject).to receive(:update_version). with(org_name, box_name, version_arg, access_token, anything) subject.execute end context "with description flag" do let(:description) { "my-description" } before { argv.push("--description").push(description) } it "should update version with description" do expect(subject).to receive(:update_version). with(org_name, box_name, version_arg, access_token, hash_including(description: description)) subject.execute end end end end end end ================================================ FILE: test/unit/plugins/commands/destroy/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/destroy/command") describe VagrantPlugins::CommandDestroy::Command do include_context "unit" let(:entry_klass) { Vagrant::MachineIndex::Entry } let(:argv) { [] } let(:vagrantfile_content) { "" } let(:iso_env) do env = isolated_environment env.vagrantfile(vagrantfile_content) env.create_vagrant_env end subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } def new_entry(name) entry_klass.new.tap do |e| e.name = name e.vagrantfile_path = "/bar" end end before do allow(iso_env).to receive(:action_runner).and_return(action_runner) end context "with no argument" do before { @state_var = :running } let(:vagrantfile_content){ "Vagrant.configure(2){|config| config.vm.box = 'dummy'}" } let(:state) { double("state", id: :running) } let(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m).to receive(:state).and_return(state) allow(m).to receive(:name).and_return("default") end end before do allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end it "should destroy the default box" do allow_any_instance_of(Vagrant::BatchAction).to receive(:action) .with(machine, :destroy, force_confirm_destroy: false, force_halt: true) expect(machine.state).to receive(:id).and_return(:running) expect(machine.state).to receive(:id).and_return(:dead) expect(subject.execute).to eq(0) end it "exits 0 if vms are not created" do allow_any_instance_of(Vagrant::BatchAction).to receive(:action) .with(machine, :destroy, anything) allow(machine.state).to receive(:id).and_return(:not_created) expect(machine.state).to receive(:id).and_return(:not_created) expect(subject.execute).to eq(0) end it "exits 1 if a vms destroy was declined" do allow_any_instance_of(Vagrant::BatchAction).to receive(:action) .with(machine, :destroy, anything) expect(machine.state).to receive(:id).and_return(:running) expect(machine.state).to receive(:id).and_return(:running) expect(subject.execute).to eq(1) end context "with multiple machines" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.box = "base" config.vm.define :machine1 config.vm.define :machine2 end VF end let(:state2) { "" } let(:machine2) do iso_env.machine(iso_env.machine_names[1], :dummy).tap do |m| allow(m).to receive(:state).and_return(state2) allow(m).to receive(:name).and_return("not_default") end end before do allow(subject).to receive(:with_target_vms).and_yield(machine).and_yield(machine2) allow_any_instance_of(Vagrant::BatchAction).to receive(:action) .with(machine2, :destroy, anything) allow_any_instance_of(Vagrant::BatchAction).to receive(:action) .with(machine, :destroy, anything) end context "second machine is not created" do let(:state2) { double("state", id: :not_created) } let(:state) { double("state", id: :not_created) } it "exits 0 if vms are successfully destroyed" do expect(machine.state).to receive(:id).and_return(:running) expect(machine.state).to receive(:id).and_return(:dead) expect(machine2.state).to receive(:id).and_return(:not_created) expect(subject.execute).to eq(0) end it "exits 0 if vms are not created" do expect(machine.state).to receive(:id).and_return(:not_created) expect(machine2.state).to receive(:id).and_return(:not_created) expect(subject.execute).to eq(0) end end context "second machine is running" do let(:state2) { double("state", id: :running) } it "exits 0 if vms are not successfully destroyed" do expect(machine.state).to receive(:id).and_return(:running) expect(machine.state).to receive(:id).and_return(:dead) expect(machine2.state).to receive(:id).and_return(:running) expect(machine2.state).to receive(:id).and_return(:dead) expect(subject.execute).to eq(0) end it "exits 1 if vms are not successfully destroyed" do expect(machine.state).to receive(:id).and_return(:running) expect(machine2.state).to receive(:id).and_return(:running) expect(subject.execute).to eq(1) end it "exits 2 if some vms are not successfully destroyed" do expect(machine.state).to receive(:id).and_return(:running) expect(machine.state).to receive(:id).and_return(:dead) expect(machine2.state).to receive(:id).and_return(:running) expect(subject.execute).to eq(2) end end end context "with VAGRANT_DEFAULT_PROVIDER set" do before do if ENV["VAGRANT_DEFAULT_PROVIDER"] @original_default = ENV["VAGRANT_DEFAULT_PROVIDER"] end ENV["VAGRANT_DEFAULT_PROVIDER"] = "unknown" end after do if @original_default ENV["VAGRANT_DEFAULT_PROVIDER"] = @original_default else ENV.delete("VAGRANT_DEFAULT_PROVIDER") end end it "should attempt to use dummy provider" do expect{ subject.execute }.to raise_error(Vagrant::Errors::UnimplementedProviderAction) end end end context "with --parallel set" do let(:argv){ ["--parallel", "--force"] } it "passes in true to batch" do batch = double("environment_batch") expect(iso_env).to receive(:batch).with(true).and_yield(batch) expect(batch).to receive(:action).with(anything, :destroy, anything) do |machine,action,args| expect(machine).to be_kind_of(Vagrant::Machine) expect(action).to eq(:destroy) end subject.execute end end context "with --graceful set" do let(:argv){ ["--graceful", "--force"] } it "passes in true to batch" do batch = double("environment_batch") expect(iso_env).to receive(:batch).and_yield(batch) expect(batch).to receive(:action).with(anything, :destroy, force_confirm_destroy: true, force_halt: false) subject.execute end end context "with a global machine" do let(:argv){ ["1234"] } it "destroys a vm with an id" do global_env = isolated_environment global_env.vagrantfile("Vagrant.configure(2){|config| config.vm.box = 'dummy'}") global_venv = global_env.create_vagrant_env global_machine = global_venv.machine(global_venv.machine_names[0], :dummy) global_machine.id = "1234" global = new_entry(global_machine.name) global.provider = "dummy" global.vagrantfile_path = global_env.workdir locked = iso_env.machine_index.set(global) iso_env.machine_index.release(locked) allow(subject).to receive(:with_target_vms) { |&block| block.call global_machine } batch = double("environment_batch") expect(iso_env).to receive(:batch).and_yield(batch) expect(batch).to receive(:action).with(global_machine, :destroy, anything) do |machine,action,args| expect(machine).to be_kind_of(Vagrant::Machine) expect(action).to eq(:destroy) end subject.execute end end context "with an argument" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.define "app" config.vm.define "db" end VF end let(:argv){ ["app"] } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } it "destroys a vm" do batch = double("environment_batch") expect(iso_env).to receive(:batch).and_yield(batch) expect(batch).to receive(:action).with(machine, :destroy, anything) do |machine,action,args| expect(machine).to be_kind_of(Vagrant::Machine) expect(action).to eq(:destroy) end subject.execute end context "with machine that does not exist" do let(:argv){ ["notweb"] } it "raises an exception" do expect { subject.execute }.to raise_error(Vagrant::Errors::MachineNotFound) end end context "with an invalid argument" do let(:argv){ [""] } it "raises an exception" do expect { subject.execute }.to raise_error(Vagrant::Errors::MachineNotFound) end end end end ================================================ FILE: test/unit/plugins/commands/global-status/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/global-status/command") describe VagrantPlugins::CommandGlobalStatus::Command do include_context "unit" let(:entry_klass) { Vagrant::MachineIndex::Entry } 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(env_opts) end let(:env_opts) { {} } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:argv) { [] } def new_entry(name) entry_klass.new.tap do |e| e.name = name e.vagrantfile_path = "/bar" end end subject { described_class.new(argv, iso_env) } describe "execute with no args" do it "succeeds" do # Let's put some things in the index iso_env.machine_index.set(new_entry("foo")) iso_env.machine_index.set(new_entry("bar")) expect(subject.execute).to eq(0) end end describe "with --machine-readable" do let(:env_opts) { {ui_class: Vagrant::UI::MachineReadable} } before do iso_env.machine_index.set(new_entry("foo")) iso_env.machine_index.set(new_entry("bar")) allow($stdout).to receive(:puts) end after { subject.execute } it "should include the machine id" do expect($stdout).to receive(:puts).with(/,machine-id,/).twice end it "should include the machine state" do expect($stdout).to receive(:puts).with(/,state,/).twice end it "should include the machine count" do expect($stdout).to receive(:puts).with(/machine-count,2/) end it "should include the machine home path" do expect($stdout).to receive(:puts).with(/,machine-home,/).twice end it "should include the provider name" do expect($stdout).to receive(:puts).with(/,provider-name,/).twice end end describe "execute with --prune" do let(:argv) { ["--prune"] } it "removes invalid entries" do # Invalid entry because vagrantfile path is gone entryA = new_entry("A") entryA.vagrantfile_path = "/i/dont/exist" locked = iso_env.machine_index.set(entryA) iso_env.machine_index.release(locked) # Invalid entry because that specific machine doesn't exist anymore. entryB_env = isolated_environment entryB_env.vagrantfile("") entryB = new_entry("B") entryB.vagrantfile_path = entryB_env.workdir locked = iso_env.machine_index.set(entryB) iso_env.machine_index.release(locked) # Valid entry because the machine does exist entryC_env = isolated_environment entryC_env.vagrantfile("") entryC_venv = entryC_env.create_vagrant_env entryC_machine = entryC_venv.machine(entryC_venv.machine_names[0], :dummy) entryC_machine.id = "foo" entryC = new_entry(entryC_machine.name) entryC.provider = "dummy" entryC.vagrantfile_path = entryC_env.workdir locked = iso_env.machine_index.set(entryC) iso_env.machine_index.release(locked) expect(subject.execute).to eq(0) # Reload the data and see that we got things correct entries = [] iso_env.machine_index.each(true) { |e| entries << e } expect(entries.length).to eq(1) expect(entries[0].name).to eq(entryC_machine.name.to_s) end end end ================================================ FILE: test/unit/plugins/commands/init/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require_relative "../../../../../plugins/commands/init/command" describe VagrantPlugins::CommandInit::Command do include_context "unit" include_context "command plugin helpers" let(:iso_env) do isolated_environment end let(:env) do iso_env.create_vagrant_env end let(:vagrantfile_path) { File.join(env.cwd, "Vagrantfile") } before do allow(Vagrant.plugin("2").manager).to receive(:commands).and_return({}) end after do iso_env.close end describe "#execute" do it "creates a Vagrantfile with no args" do described_class.new([], env).execute contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.box = "base"/) end it "creates a minimal Vagrantfile" do described_class.new(["-m"], env).execute contents = File.read(vagrantfile_path) expect(contents).to_not match(/provision/) end it "creates a custom Vagrantfile using a relative template path" do described_class.new(["--template", "test/unit/templates/commands/init/Vagrantfile"], env).execute contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.hostname = "vagrant.dev"/) end it "creates a custom Vagrantfile using an absolute template path" do described_class.new(["--template", ::Vagrant.source_root.join("test/unit/templates/commands/init/Vagrantfile").to_s], env).execute contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.hostname = "vagrant.dev"/) end it "creates a custom Vagrantfile using a provided template with the extension included" do described_class.new(["--template", ::Vagrant.source_root.join("test/unit/templates/commands/init/Vagrantfile.erb").to_s], env).execute contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.hostname = "vagrant.dev"/) end it "creates a custom Vagrant file using a template provided from the environment" do with_temp_env("VAGRANT_DEFAULT_TEMPLATE" => "test/unit/templates/commands/init/Vagrantfile") do described_class.new([], env).execute end contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.hostname = "vagrant.dev"/) end it "ignores the environmentally-set default template when a template is explicitly set" do with_temp_env("VAGRANT_DEFAULT_TEMPLATE" => "/this_file_does_not_exist") do described_class.new(["--template", "test/unit/templates/commands/init/Vagrantfile"], env).execute end contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.hostname = "vagrant.dev"/) end it "ignores the -m option when using a provided template" do described_class.new(["-m", "--template", ::Vagrant.source_root.join("test/unit/templates/commands/init/Vagrantfile").to_s], env).execute contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.hostname = "vagrant.dev"/) end it "raises an appropriate exception when the template file can't be found" do expect { described_class.new(["--template", "./a/b/c/template"], env).execute }.to raise_error(Vagrant::Errors::VagrantfileTemplateNotFoundError) end it "does not overwrite an existing Vagrantfile" do # Create an existing Vagrantfile File.open(File.join(env.cwd, "Vagrantfile"), "w+") { |f| f.write("") } expect { described_class.new([], env).execute }.to raise_error(Vagrant::Errors::VagrantfileExistsError) end it "overwrites an existing Vagrantfile with force" do # Create an existing Vagrantfile File.open(File.join(env.cwd, "Vagrantfile"), "w+") { |f| f.write("") } expect { described_class.new(["-f"], env).execute }.to_not raise_error contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.box = "base"/) end it "creates a Vagrantfile with a box" do described_class.new(["hashicorp/precise64"], env).execute contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.box = "hashicorp\/precise64"/) end it "creates a Vagrantfile with a box and box_url" do described_class.new(["hashicorp/precise64", "http://example.com"], env).execute contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.box = "hashicorp\/precise64"/) expect(contents).to match(/config.vm.box_url = "http:\/\/example.com"/) end it "creates a Vagrantfile with a box and box version" do described_class.new(["--box-version", "1.2.3", "hashicorp/precise64"], env).execute contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.box = "hashicorp\/precise64"/) expect(contents).to match(/config.vm.box_version = "1.2.3"/) end it "creates a minimal Vagrantfile with a box and box version" do described_class.new(["--minimal", "--box-version", "1.2.3", "hashicorp/precise64"], env).execute contents = File.read(vagrantfile_path) expect(contents).to match(/config.vm.box = "hashicorp\/precise64"/) expect(contents).to match(/config.vm.box_version = "1.2.3"/) end it "creates a Vagrantfile at a custom path" do described_class.new(["--output", "vf.rb"], env).execute expect(File.exist?(File.join(env.cwd, "vf.rb"))).to be(true) end end end ================================================ FILE: test/unit/plugins/commands/list-commands/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/list-commands/command") describe VagrantPlugins::CommandListCommands::Command do include_context "unit" include_context "command plugin helpers" 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(:argv) { [] } let(:commands) { {} } subject { described_class.new(argv, iso_env) } before do allow(Vagrant.plugin("2").manager).to receive(:commands).and_return(commands) end describe "execute" do it "includes all subcommands" do commands[:foo] = [command_lambda("foo", 0), { primary: true }] commands[:bar] = [command_lambda("bar", 0), { primary: true }] commands[:baz] = [command_lambda("baz", 0), { primary: false }] expect(iso_env.ui).to receive(:info).with(any_args) { |message, opts| expect(message).to include("foo") expect(message).to include("bar") expect(message).to include("baz") } subject.execute end end end ================================================ FILE: test/unit/plugins/commands/package/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require_relative "../../../../../plugins/commands/package/command" RSpec::Matchers.define :a_machine_named do |name| match{ |actual| actual.name.to_s == name.to_s } end RSpec::Matchers.define :an_existing_directory do match{ |actual| File.directory?(actual) } end describe VagrantPlugins::CommandPackage::Command do include_context "unit" let(:argv) { [] } 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(:package_command) { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } before do allow(iso_env).to receive(:action_runner).and_return(action_runner) end describe "#execute" do context "with no arguments" do it "packages default machine" do expect(package_command).to receive(:package_vm).with(a_machine_named('default'), {}) package_command.execute end end context "with single argument" do context "set to default" do let(:argv){ ['default'] } it "packages default machine" do expect(package_command).to receive(:package_vm).with(a_machine_named('default'), {}) package_command.execute end end context "set to undefined vm" do let(:argv){ ['undefined'] } it "raises machine not found error" do expect{ package_command.execute }.to raise_error(Vagrant::Errors::MachineNotFound) end end context "with --output option" do let(:argv){ ['--output', 'package-output-folder/default'] } it "packages default machine inside specified folder" do expect(package_command).to receive(:package_vm).with( a_machine_named('default'), { output: "package-output-folder/default" } ) package_command.execute end end end context "with multiple arguments" do let(:argv){ ['default', 'undefined'] } it "ignores the extra arguments" do expect(package_command).to receive(:package_vm).with(a_machine_named('default'), {}) package_command.execute end end context "with --base option" do context "and no option value" do let(:argv){ ['--base'] } it "shows help" do expect{ package_command.execute }.to raise_error(Vagrant::Errors::CLIInvalidOptions) end end context "and option value" do let(:argv){ ['--base', 'machine-id'] } it "packages vm defined within virtualbox" do expect(package_command).to receive(:package_base).with({ base: 'machine-id' }) package_command.execute end it "provides a machine data directory" do expect(Vagrant::Machine).to receive(:new).with( 'machine-id', :virtualbox, anything, nil, anything, anything, an_existing_directory, anything, anything, anything, anything).and_return(double("vm", name: "machine-id")) allow(package_command).to receive(:package_vm) package_command.execute end end end end describe "#package_vm" do context "calling the package action" do let(:options) { {output: "test.box"} } let(:expected_options) { {"package.output"=>"test.box"} } let(:machine) { double("machine") } let(:tmp_dir) { "/home/user/.vagrant.d/tmp/vagrant-package" } let(:env) { {"export.temp_dir"=>tmp_dir} } it "ensures that the package tmp dir is cleaned up" do allow(FileUtils).to receive(:rm_rf).and_return(true) allow(machine).to receive(:action).with(:package, expected_options). and_return(env) expect(FileUtils).to receive(:rm_rf).with(tmp_dir) package_command.send(:package_vm, machine, options) end end end end ================================================ FILE: test/unit/plugins/commands/plugin/action/expunge_plugins_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) describe VagrantPlugins::CommandPlugin::Action::ExpungePlugins do let(:app) { lambda { |env| } } let(:home_path){ '/fake/file/path/.vagrant.d' } let(:gems_path){ "#{home_path}/gems" } let(:force){ true } let(:env_local){ false } let(:env_local_only){ nil } let(:global_only){ nil } let(:env) {{ ui: Vagrant::UI::Silent.new, home_path: home_path, gems_path: gems_path, force: force, env_local: env_local, env_local_only: env_local_only, global_only: global_only }} let(:user_file) { double("user_file", path: user_file_pathname) } let(:user_file_pathname) { double("user_file_pathname", exist?: true, delete: true) } let(:local_file) { nil } let(:bundler) { double("bundler", plugin_gem_path: plugin_gem_path, env_plugin_gem_path: env_plugin_gem_path) } let(:plugin_gem_path) { double("plugin_gem_path", exist?: true, rmtree: true) } let(:env_plugin_gem_path) { nil } let(:manager) { double("manager", user_file: user_file, local_file: local_file) } let(:expect_to_receive) do lambda do allow(File).to receive(:exist?).with(File.join(home_path, 'plugins.json')).and_return(true) allow(File).to receive(:directory?).with(gems_path).and_return(true) expect(app).to receive(:call).with(env).once end end subject { described_class.new(app, env) } before do allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager) allow(Vagrant::Bundler).to receive(:instance).and_return(bundler) end describe "#call" do before do instance_exec(&expect_to_receive) end it "should delete all plugins" do expect(user_file_pathname).to receive(:delete) expect(plugin_gem_path).to receive(:rmtree) subject.call(env) end describe "when force is false" do let(:force){ false } it "should prompt user before deleting all plugins" do expect(env[:ui]).to receive(:ask).and_return("Y\n") subject.call(env) end describe "when user declines prompt" do let(:expect_to_receive) do lambda do expect(app).not_to receive(:call) end end it "should not delete all plugins" do expect(env[:ui]).to receive(:ask).and_return("N\n") subject.call(env) end end end context "when local option is set" do let(:env_local) { true } it "should delete plugins" do expect(user_file_pathname).to receive(:delete) expect(plugin_gem_path).to receive(:rmtree) subject.call(env) end end context "when local plugins exist" do let(:local_file) { double("local_file", path: local_file_pathname) } let(:local_file_pathname) { double("local_file_pathname", exist?: true, delete: true) } let(:env_plugin_gem_path) { double("env_plugin_gem_path", exist?: true, rmtree: true) } it "should delete user and local plugins" do expect(user_file_pathname).to receive(:delete) expect(local_file_pathname).to receive(:delete) expect(plugin_gem_path).to receive(:rmtree) expect(env_plugin_gem_path).to receive(:rmtree) subject.call(env) end context "when local option is set" do let(:env_local) { true } it "should delete local plugins" do expect(local_file_pathname).to receive(:delete) expect(env_plugin_gem_path).to receive(:rmtree) subject.call(env) end it "should delete user plugins" do expect(user_file_pathname).to receive(:delete) expect(plugin_gem_path).to receive(:rmtree) subject.call(env) end context "when local only option is set" do let(:env_local_only) { true } it "should delete local plugins" do expect(local_file_pathname).to receive(:delete) expect(env_plugin_gem_path).to receive(:rmtree) subject.call(env) end it "should not delete user plugins" do expect(user_file_pathname).not_to receive(:delete) expect(plugin_gem_path).not_to receive(:rmtree) subject.call(env) end end context "when global only option is set" do let(:global_only) { true } it "should not delete local plugins" do expect(local_file_pathname).not_to receive(:delete) expect(env_plugin_gem_path).not_to receive(:rmtree) subject.call(env) end it "should delete user plugins" do expect(user_file_pathname).to receive(:delete) expect(plugin_gem_path).to receive(:rmtree) subject.call(env) end end context "when global and local only options are set" do let(:env_local_only) { true } let(:global_only) { true } it "should delete local plugins" do expect(local_file_pathname).to receive(:delete) expect(env_plugin_gem_path).to receive(:rmtree) subject.call(env) end it "should delete user plugins" do expect(user_file_pathname).to receive(:delete) expect(plugin_gem_path).to receive(:rmtree) subject.call(env) end end end end end end ================================================ FILE: test/unit/plugins/commands/plugin/action/install_gem_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) describe VagrantPlugins::CommandPlugin::Action::InstallGem do let(:app) { lambda { |env| } } let(:env) {{ ui: Vagrant::UI::Silent.new }} let(:manager) { double("manager") } subject { described_class.new(app, env) } before do allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager) end describe "#call" do it "should install the plugin" do spec = Gem::Specification.new expect(manager).to receive(:install_plugin).with( "foo", version: nil, require: nil, sources: nil, verbose: false, env_local: nil).once.and_return(spec) expect(app).to receive(:call).with(env).once env[:plugin_name] = "foo" subject.call(env) end it "should specify the version if given" do spec = Gem::Specification.new expect(manager).to receive(:install_plugin).with( "foo", version: "bar", require: nil, sources: nil, verbose: false, env_local: nil).once.and_return(spec) expect(app).to receive(:call).with(env).once env[:plugin_name] = "foo" env[:plugin_version] = "bar" subject.call(env) end it "should specify the entrypoint if given" do spec = Gem::Specification.new expect(manager).to receive(:install_plugin).with( "foo", version: "bar", require: "baz", sources: nil, verbose: false, env_local: nil).once.and_return(spec) expect(app).to receive(:call).with(env).once env[:plugin_entry_point] = "baz" env[:plugin_name] = "foo" env[:plugin_version] = "bar" subject.call(env) end it "should specify the sources if given" do spec = Gem::Specification.new expect(manager).to receive(:install_plugin).with( "foo", version: nil, require: nil, sources: ["foo"], verbose: false, env_local: nil).once.and_return(spec) expect(app).to receive(:call).with(env).once env[:plugin_name] = "foo" env[:plugin_sources] = ["foo"] subject.call(env) end end describe "#recover" do it "should do nothing by default" do subject.recover(env) end context "with a successful plugin install" do let(:action_runner) { double("action_runner") } before do spec = Gem::Specification.new spec.name = "foo" allow(manager).to receive(:install_plugin).and_return(spec) env[:plugin_name] = "foo" subject.call(env) env[:action_runner] = action_runner end it "should uninstall the plugin" do expect(action_runner).to receive(:run).with(any_args) { |action, newenv| expect(newenv[:plugin_name]).to eql("foo") } subject.recover(env) end end end end ================================================ FILE: test/unit/plugins/commands/plugin/action/plugin_exists_check_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) describe VagrantPlugins::CommandPlugin::Action::PluginExistsCheck do let(:app) { lambda {} } let(:env) { {} } let(:manager) { double("manager") } subject { described_class.new(app, env) } before do allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager) end it "should raise an exception if the plugin doesn't exist" do allow(manager).to receive(:installed_plugins).and_return({ "foo" => {} }) expect(app).not_to receive(:call) env[:plugin_name] = "bar" expect { subject.call(env) }. to raise_error(Vagrant::Errors::PluginNotInstalled) end it "should call the app if the plugin is installed" do allow(manager).to receive(:installed_plugins).and_return({ "bar" => {} }) expect(app).to receive(:call).once.with(env) env[:plugin_name] = "bar" subject.call(env) end end ================================================ FILE: test/unit/plugins/commands/plugin/action/uninstall_plugin_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) describe VagrantPlugins::CommandPlugin::Action::UninstallPlugin do let(:app) { lambda { |env| } } let(:env) {{ ui: Vagrant::UI::Silent.new, }} let(:manager) { double("manager") } subject { described_class.new(app, env) } before do allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager) end it "uninstalls the specified plugin" do expect(manager).to receive(:uninstall_plugin).with("bar", any_args).ordered expect(app).to receive(:call).ordered env[:plugin_name] = "bar" subject.call(env) end end ================================================ FILE: test/unit/plugins/commands/plugin/action/update_gems_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) describe VagrantPlugins::CommandPlugin::Action::UpdateGems do let(:app) { lambda { |env| } } let(:env) {{ ui: Vagrant::UI::Silent.new }} let(:manager) { double("manager") } subject { described_class.new(app, env) } before do allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager) allow(manager).to receive(:installed_specs).and_return([]) end describe "#call" do it "should update all plugins if none are specified" do expect(manager).to receive(:update_plugins).with([]).once.and_return([]) expect(manager).to receive(:installed_plugins).twice.and_return({}) expect(app).to receive(:call).with(env).once subject.call(env) end it "should update specified plugins" do expect(manager).to receive(:update_plugins).with(["foo"]).once.and_return([]) expect(manager).to receive(:installed_plugins).twice.and_return({}) expect(app).to receive(:call).with(env).once env[:plugin_name] = ["foo"] subject.call(env) end end end ================================================ FILE: test/unit/plugins/commands/port/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/port/command") describe VagrantPlugins::CommandPort::Command do include_context "unit" include_context "command plugin helpers" let(:iso_env) { isolated_environment } let(:env) do iso_env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "hashicorp/precise64" config.vm.synced_folder ".", "/vagrant", disabled: true end VF iso_env.create_vagrant_env end let(:state) { double(:state, id: :running) } let(:machine) { env.machine(env.machine_names[0], :dummy) } before(:all) do I18n.load_path << Vagrant.source_root.join("plugins/commands/port/locales/en.yml") I18n.reload! end subject { described_class.new([], env) } before do allow(machine).to receive(:state).and_return(state) allow(subject).to receive(:with_target_vms) { |&block| block.call(machine) } end describe "#execute" do it "validates the configuration" do iso_env.vagrantfile <<-EOH Vagrant.configure("2") do |config| config.vm.box = "hashicorp/precise64" config.vm.synced_folder ".", "/vagrant", disabled: true config.push.define "noop" do |push| push.bad = "ham" end end EOH subject = described_class.new([], iso_env.create_vagrant_env) expect { subject.execute }.to raise_error(Vagrant::Errors::ConfigInvalid) { |err| expect(err.message).to include("The following settings shouldn't exist: bad") } end it "ensures the vm is running" do allow(state).to receive(:id).and_return(:stopped) expect(env.ui).to receive(:error).with(/does not support listing forwarded ports/). and_call_original expect(subject.execute).to eq(1) end it "shows a friendly error when the capability is not supported" do allow(machine.provider).to receive(:capability?).and_return(false) expect(env.ui).to receive(:error).with(/does not support listing forwarded ports/). and_call_original expect(subject.execute).to eq(1) end it "returns a friendly message when there are no forwarded ports" do allow(machine.provider).to receive(:capability?).and_return(true) allow(machine.provider).to receive(:capability).with(:forwarded_ports) .and_return([]) expect(env.ui).to receive(:info).with(/there are no forwarded ports/). and_call_original expect(subject.execute).to eq(0) end it "returns the list of ports" do allow(machine.provider).to receive(:capability?).and_return(true) allow(machine.provider).to receive(:capability).with(:forwarded_ports) .and_return([[2222,22], [1111,11]]) output = "" allow(env.ui).to receive(:info) do |data| output << data end expect(subject.execute).to eq(0) expect(output).to include("forwarded ports for the machine") expect(output).to include("22 (guest) => 2222 (host)") expect(output).to include("11 (guest) => 1111 (host)") end it "prints the matching host port when --guest is given" do argv = ["--guest", "22"] subject = described_class.new(argv, env) allow(machine.provider).to receive(:capability?).and_return(true) allow(machine.provider).to receive(:capability).with(:forwarded_ports) .and_return([[2222,22]]) output = "" allow(env.ui).to receive(:info) do |data| output << data end expect(subject.execute).to eq(0) expect(output).to eq("2222") end it "returns an error with no port is mapped to the --guest option" do argv = ["--guest", "80"] subject = described_class.new(argv, env) allow(machine.provider).to receive(:capability?).and_return(true) allow(machine.provider).to receive(:capability).with(:forwarded_ports) .and_return([[2222,22]]) expect(env.ui).to receive(:error).with(/not currently mapping port 80/). and_call_original expect(subject.execute).to_not eq(0) end end end ================================================ FILE: test/unit/plugins/commands/powershell/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/powershell/command") describe VagrantPlugins::CommandPS::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(:config) { double("config", vm: double("vm", communicator: communicator_name), winrm: double("winrm", username: winrm_username, password: winrm_password) ) } let(:communicator_name) { :winrm } let(:winrm_info) { {host: winrm_host, port: winrm_port} } let(:winrm_username) { double("winrm_username") } let(:winrm_password) { double("winrm_password") } let(:winrm_host) { double("winrm_host") } let(:winrm_port) { double("winrm_port") } let(:remoting_ready_result) { {} } 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 } allow(iso_env).to receive(:host).and_return(host) allow(host).to receive(:capability?).with(:ps_client).and_return(true) allow(machine.communicate).to receive(:ready?).and_return(true) allow(machine).to receive(:config).and_return(config) allow(VagrantPlugins::CommunicatorWinRM::Helper).to receive(:winrm_info).and_return(winrm_info) allow(subject).to receive(:ready_ps_remoting_for).and_return(remoting_ready_result) allow(host).to receive(:capability).with(:ps_client, any_args) # Ignore loading up translations allow_any_instance_of(Vagrant::Errors::VagrantError).to receive(:translate_error) end describe "#execute" do context "when communicator is not ready" do before { expect(machine.communicate).to receive(:ready?).and_return(false) } it "should raise error that machine is not created" do expect { subject.execute }.to raise_error(Vagrant::Errors::VMNotCreatedError) end end context "when communicator is not winrm" do let(:communicator_name) { :ssh } context "when command is provided" do let(:argv) { ["-c", "command"] } it "should raise an error that winrm is not ready" do expect { subject.execute }.to raise_error(VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady) end end context "when no command is provided" do it "should create a powershell session" do expect(host).to receive(:capability).with(:ps_client, any_args) subject.execute end end end context "when host does not support ps_client" do before { allow(host).to receive(:capability?).with(:ps_client).and_return(false) } context "when no command is provided" do it "should raise an error for unsupported host" do expect { subject.execute }.to raise_error(VagrantPlugins::CommandPS::Errors::HostUnsupported) end end context "when command is provided" do let(:argv) { ["-c", "command"] } it "should execute command when command is provided" do expect(machine.communicate).to receive(:execute).with("command", any_args).and_return(0) subject.execute end end end context "with command provided" do let(:argv) { ["-c", "command"] } it "executes the command on the guest" do expect(machine.communicate).to receive(:execute).with("command", any_args).and_return(0) subject.execute end context "with elevated flag" do let(:argv) { ["-e", "-c", "command"] } it "should execute the command with elevated option" do expect(machine.communicate).to receive(:execute). with("command", hash_including(elevated: true)).and_return(0) subject.execute end end end context "with elevated flag and no command" do let(:argv) { ["-e"] } it "should raise error that command must be provided" do expect { subject.execute }.to raise_error(VagrantPlugins::CommandPS::Errors::ElevatedNoCommand) end end it "should start a new session" do expect(host).to receive(:capability).with(:ps_client, any_args) subject.execute end context "when setup returns PreviousTrustedHosts" do let(:remoting_ready_result) { {"PreviousTrustedHosts" => true} } it "should reset the powershell remoting" do expect(subject).to receive(:reset_ps_remoting_for) subject.execute end end end end ================================================ FILE: test/unit/plugins/commands/provider/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 ================================================ FILE: test/unit/plugins/commands/push/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/push/command") describe VagrantPlugins::CommandPush::Command do include_context "unit" include_context "command plugin helpers" let(:iso_env) { isolated_environment } let(:env) do iso_env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "hashicorp/precise64" config.vm.synced_folder ".", "/vagrant", disabled: true end VF iso_env.create_vagrant_env end let(:argv) { [] } let(:pushes) { {} } subject { described_class.new(argv, env) } before do allow(Vagrant.plugin("2").manager).to receive(:pushes).and_return(pushes) end describe "#execute" do before do allow(subject).to receive(:validate_pushes!) .and_return(:noop) allow(env).to receive(:pushes) allow(env).to receive(:push) end it "validates the pushes" do expect(subject).to receive(:validate_pushes!).once subject.execute end it "delegates to Environment#push" do expect(env).to receive(:push).once subject.execute end it "validates the configuration" do iso_env.vagrantfile <<-EOH Vagrant.configure("2") do |config| config.vm.box = "hashicorp/precise64" config.vm.synced_folder ".", "/vagrant", disabled: true config.push.define "noop" do |push| push.bad = "ham" end end EOH subject = described_class.new(argv, iso_env.create_vagrant_env) allow(subject).to receive(:validate_pushes!) .and_return(:noop) expect { subject.execute }.to raise_error(Vagrant::Errors::ConfigInvalid) { |err| expect(err.message).to include("The following settings shouldn't exist: bad") } end end describe "#validate_pushes!" do context "when there are no pushes defined" do let(:pushes) { [] } context "when a strategy is given" do it "raises an exception" do expect { subject.validate_pushes!(pushes, :noop) } .to raise_error(Vagrant::Errors::PushesNotDefined) end end context "when no strategy is given" do it "raises an exception" do expect { subject.validate_pushes!(pushes) } .to raise_error(Vagrant::Errors::PushesNotDefined) end end end context "when there is one push defined" do let(:noop) { double("noop") } let(:pushes) { [:noop] } context "when a strategy is given" do context "when that strategy is not defined" do it "raises an exception" do expect { subject.validate_pushes!(pushes, :bacon) } .to raise_error(Vagrant::Errors::PushStrategyNotDefined) end end context "when that strategy is defined" do it "returns that push" do expect(subject.validate_pushes!(pushes, :noop)).to eq(:noop) end end end context "when no strategy is given" do it "returns the strategy" do expect(subject.validate_pushes!(pushes)).to eq(:noop) end end end context "when there are multiple pushes defined" do let(:noop) { double("noop") } let(:ftp) { double("ftp") } let(:pushes) { [:noop, :ftp] } context "when a strategy is given" do context "when that strategy is not defined" do it "raises an exception" do expect { subject.validate_pushes!(pushes, :bacon) } .to raise_error(Vagrant::Errors::PushStrategyNotDefined) end end context "when that strategy is defined" do it "returns the strategy" do expect(subject.validate_pushes!(pushes, :noop)).to eq(:noop) expect(subject.validate_pushes!(pushes, :ftp)).to eq(:ftp) end end end context "when no strategy is given" do it "raises an exception" do expect { subject.validate_pushes!(pushes) } .to raise_error(Vagrant::Errors::PushStrategyNotProvided) end end end end end ================================================ FILE: test/unit/plugins/commands/reload/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/reload/command") describe VagrantPlugins::CommandReload::Command do include_context "unit" let(:entry_klass) { Vagrant::MachineIndex::Entry } let(:argv) { [] } let(:vagrantfile_content){ "" } let(:iso_env) do env = isolated_environment env.vagrantfile(vagrantfile_content) env.create_vagrant_env end subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:machine2) { iso_env.machine(iso_env.machine_names[0], :dummy) } def new_entry(name) entry_klass.new.tap do |e| e.name = name e.vagrantfile_path = "/bar" end end before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end context "with no argument" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.define "app" config.vm.define "db" end VF end it "should reload all vms" do allow(subject).to receive(:with_target_vms) { |&block| block.call machine block.call machine2 } expect(machine).to receive(:action) do |name, opts| expect(name).to eq(:reload) end expect(machine2).to receive(:action) do |name, opts| expect(name).to eq(:reload) end expect(subject.execute).to eq(0) end end context "with an argument" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.define "app" config.vm.define "db" end VF end let(:argv) { ["app"] } it "should reload a vm" do expect(machine).to receive(:action) do |name, opts| expect(name).to eq(:reload) end expect(subject.execute).to eq(0) end end context "with the force flag" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.define "app" config.vm.define "db" end VF end let(:argv) { ["--force"] } it "should reload a vm" do expect(machine).to receive(:action) do |name, opts| expect(opts).to include(force_halt: true) expect(name).to eq(:reload) end expect(subject.execute).to eq(0) end end end ================================================ FILE: test/unit/plugins/commands/snapshot/command/delete_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/snapshot/command/delete") describe VagrantPlugins::CommandSnapshot::Command::Delete 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(machine.provider).to receive(:capability).with(:snapshot_list). and_return([]) allow(machine.provider).to receive(:capability?).with(:snapshot_list). and_return(true) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end describe "execute" do context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with an unsupported provider" do let(:argv) { ["test"] } before do allow(machine.provider).to receive(:capability?).with(:snapshot_list). and_return(false) end it "raises an exception" do machine.id = "foo" expect { subject.execute }. to raise_error(Vagrant::Errors::SnapshotNotSupported) end end context "with a snapshot name given" do let(:argv) { ["test"] } it "calls snapshot_delete with a snapshot name" do machine.id = "foo" allow(machine.provider).to receive(:capability).with(:snapshot_list). and_return(["test"]) expect(machine).to receive(:action) do |name, opts| expect(name).to eq(:snapshot_delete) expect(opts[:snapshot_name]).to eq("test") end expect(subject.execute).to eq(0) end it "doesn't delete a snapshot on a non-existent machine" do machine.id = nil expect(subject).to receive(:with_target_vms){} expect(machine).to_not receive(:action) expect(subject.execute).to eq(0) end end context "with a snapshot name that doesn't exist" do let(:argv) { ["nopetest"] } it "fails to take a snapshot and prints a warning to the user" do machine.id = "foo" allow(machine.provider).to receive(:capability).with(:snapshot_list). and_return(["test"]) expect(machine).to_not receive(:action) expect { subject.execute }. to raise_error(Vagrant::Errors::SnapshotNotFound) end end end end ================================================ FILE: test/unit/plugins/commands/snapshot/command/list_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/snapshot/command/list") describe VagrantPlugins::CommandSnapshot::Command::List 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(machine.provider).to receive(:capability?).with(:snapshot_list). and_return(true) allow(machine.provider).to receive(:capability).with(:snapshot_list). and_return([]) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end describe "execute" do context "with an unsupported provider" do let(:argv) { ["foo"] } before do allow(machine.provider).to receive(:capability?).with(:snapshot_list). and_return(false) end it "raises an exception" do machine.id = "foo" expect { subject.execute }. to raise_error(Vagrant::Errors::SnapshotNotSupported) end end context "with a vm given" do let(:argv) { ["foo"] } it "prints a message if the vm does not exist" do machine.id = nil expect(iso_env.ui).to receive(:info).with("==> default: VM not created. Moving on...", anything). and_call_original expect(machine).to_not receive(:action) expect(subject.execute).to eq(0) end it "prints a message if no snapshots have been taken" do machine.id = "foo" expect(iso_env.ui).to receive(:output).and_call_original .with(/No snapshots have been taken yet!/, anything) expect(subject.execute).to eq(0) end it "prints a list of snapshots" do machine.id = "foo" allow(machine.provider).to receive(:capability).with(:snapshot_list). and_return(["foo", "bar", "baz"]) expect(iso_env.ui).to receive(:output).with(/default/, anything).and_call_original expect(iso_env.ui).to receive(:detail).with(/foo/, anything).and_call_original expect(iso_env.ui).to receive(:detail).with(/bar/, anything).and_call_original expect(iso_env.ui).to receive(:detail).with(/baz/, anything) expect(subject.execute).to eq(0) end end end end ================================================ FILE: test/unit/plugins/commands/snapshot/command/pop_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 before do machine.id = "mach" allow(machine.provider).to receive(:capability). with(:snapshot_list).and_return(["push_2_0"]) end 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 it "should disable ignoring sentinel file for provisioning" do expect(machine).to receive(:action) do |name, opts| expect(name).to eq(:snapshot_restore) expect(opts[:provision_ignore_sentinel]).to eq(false) end subject.execute end it "should start the snapshot" do expect(machine).to receive(:action) do |name, opts| expect(name).to eq(:snapshot_restore) expect(opts[:snapshot_start]).to eq(true) end subject.execute end context "when --no-start flag is provided" do let(:argv) { ["--no-start"] } it "should not start the snapshot" do expect(machine).to receive(:action) do |name, opts| expect(name).to eq(:snapshot_restore) expect(opts[:snapshot_start]).to eq(false) end subject.execute end end end end ================================================ FILE: test/unit/plugins/commands/snapshot/command/push_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 ================================================ FILE: test/unit/plugins/commands/snapshot/command/restore_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/snapshot/command/restore") describe VagrantPlugins::CommandSnapshot::Command::Restore 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(:snapshot_name) { "snapshot_name" } 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(machine.provider).to receive(:capability).with(:snapshot_list). and_return([]) allow(machine.provider).to receive(:capability?).with(:snapshot_list). and_return(true) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end describe "execute" do before do allow(machine.provider).to receive(:capability). with(:snapshot_list).and_return([snapshot_name]) end context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with an unsupported provider" do let(:argv) { ["test"] } before do allow(machine.provider).to receive(:capability?).with(:snapshot_list). and_return(false) end it "raises an exception" do machine.id = "foo" expect { subject.execute }. to raise_error(Vagrant::Errors::SnapshotNotSupported) end end context "with a snapshot name given" do let(:argv) { ["test"] } it "calls snapshot_delete with a snapshot name" do machine.id = "foo" allow(machine.provider).to receive(:capability).with(:snapshot_list). and_return(["test"]) expect(machine).to receive(:action) do |name, opts| expect(name).to eq(:snapshot_restore) expect(opts[:snapshot_name]).to eq("test") end expect(subject.execute).to eq(0) end it "doesn't delete a snapshot on a non-existent machine" do machine.id = nil expect(subject).to receive(:with_target_vms){} expect(machine).to_not receive(:action) expect(subject.execute).to eq(0) end end context "with a snapshot name that doesn't exist" do let(:argv) { ["nopetest"] } it "fails to take a snapshot and prints a warning to the user" do machine.id = "foo" allow(machine.provider).to receive(:capability).with(:snapshot_list). and_return(["test"]) expect(machine).to_not receive(:action) expect { subject.execute }. to raise_error(Vagrant::Errors::SnapshotNotFound) end end it "should disable ignoring sentinel file for provisioning" do argv << snapshot_name expect(machine).to receive(:action) do |_, opts| expect(opts[:provision_ignore_sentinel]).to eq(false) end subject.execute end it "should start the snapshot" do argv << snapshot_name expect(machine).to receive(:action) do |_, opts| expect(opts[:snapshot_start]).to eq(true) end subject.execute end context "when --no-start flag is provided" do let(:argv) { [snapshot_name, "--no-start"] } it "should not start the snapshot" do expect(machine).to receive(:action) do |_, opts| expect(opts[:snapshot_start]).to eq(false) end subject.execute end end end end ================================================ FILE: test/unit/plugins/commands/snapshot/command/root_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/snapshot/command/root") describe VagrantPlugins::CommandSnapshot::Command::Root 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(:argv) { [] } subject { described_class.new(argv, iso_env) } describe "execute" do context "--help" do let(:argv) { ["--help"] } it "shows help" do expect(iso_env.ui). to receive(:info).with(/Usage: vagrant snapshot /, anything) expect(subject.execute).to eq(0) end end context "with no subcommand" do let(:argv) { [] } it "shows help and fails" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with invalid subcommand" do let(:argv) { ["invalid"] } it "shows help and fails" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end end end ================================================ FILE: test/unit/plugins/commands/snapshot/command/save_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/snapshot/command/save") describe VagrantPlugins::CommandSnapshot::Command::Save 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(machine.provider).to receive(:capability).with(:snapshot_list). and_return([]) allow(machine.provider).to receive(:capability?).with(:snapshot_list). and_return(true) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end describe "execute" do context "with no arguments" do it "shows help" do expect { subject.execute }. to raise_error(Vagrant::Errors::CLIInvalidUsage) end end context "with an unsupported provider" do let(:argv) { ["test"] } before do allow(machine.provider).to receive(:capability?).with(:snapshot_list). and_return(false) end it "raises an exception" do machine.id = "foo" expect { subject.execute }. to raise_error(Vagrant::Errors::SnapshotNotSupported) end end context "with a snapshot name given" do let(:argv) { ["test"] } it "calls snapshot_save with a 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 eq("test") end expect(subject.execute).to eq(0) end it "doesn't snapshot a non-existent machine" do machine.id = nil expect(subject).to receive(:with_target_vms){} expect(machine).to_not receive(:action) expect(subject.execute).to eq(0) end end context "with a snapshot guest and name given" do let(:argv) { ["foo", "backup"] } it "calls snapshot_save with a 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 eq("backup") 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 context "with a duplicate snapshot name given and no force flag" do let(:argv) { ["test"] } it "fails to take a snapshot and prints a warning to the user" do machine.id = "fool" allow(machine.provider).to receive(:capability).with(:snapshot_list). and_return(["test"]) expect(machine).to_not receive(:action) expect { subject.execute }. to raise_error(Vagrant::Errors::SnapshotConflictFailed) end end context "with a duplicate snapshot name given and a force flag" do let(:argv) { ["test", "--force"] } it "deletes the existing snapshot and takes a new one" do machine.id = "foo" allow(machine.provider).to receive(:capability).with(:snapshot_list). and_return(["test"]) expect(machine).to receive(:action).with(:snapshot_delete, snapshot_name: "test") expect(machine).to receive(:action).with(:snapshot_save, snapshot_name: "test") expect(subject.execute).to eq(0) end end end end ================================================ FILE: test/unit/plugins/commands/ssh_config/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/ssh_config/command") describe VagrantPlugins::CommandSSHConfig::Command do include_context "unit" include_context "virtualbox" 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) { [] } let(:ssh_info) {{ host: "testhost.vagrant.dev", port: 1234, username: "testuser", keys_only: true, verify_host_key: false, private_key_path: ["/home/vagrant/.private/keys.key"], forward_agent: false, forward_x11: false }} subject { described_class.new(argv, iso_env) } before do allow(machine).to receive(:ssh_info).and_return(ssh_info) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end describe "execute" do it "prints out the ssh config for the given machine" do output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to eq(<<-SSHCONFIG) Host #{machine.name} HostName testhost.vagrant.dev User testuser Port 1234 UserKnownHostsFile /dev/null StrictHostKeyChecking no PasswordAuthentication no IdentityFile /home/vagrant/.private/keys.key IdentitiesOnly yes LogLevel FATAL PubkeyAcceptedKeyTypes +ssh-rsa HostKeyAlgorithms +ssh-rsa SSHCONFIG end it "turns on agent forwarding when it is configured" do allow(machine).to receive(:ssh_info) { ssh_info.merge(forward_agent: true) } output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include("ForwardAgent yes") end it "turns on x11 forwarding when it is configured" do allow(machine).to receive(:ssh_info) { ssh_info.merge(forward_x11: true) } output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include("ForwardX11 yes") end it "handles multiple private key paths" do allow(machine).to receive(:ssh_info) { ssh_info.merge(private_key_path: ["foo", "bar"]) } output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include("IdentityFile foo") expect(output).to include("IdentityFile bar") end it "puts quotes around an identityfile path if it has a space" do allow(machine).to receive(:ssh_info) { ssh_info.merge(private_key_path: ["with a space"]) } output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include('IdentityFile "with a space"') end it "omits IdentitiesOnly when keys_only is false" do allow(machine).to receive(:ssh_info) { ssh_info.merge(keys_only: false) } output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).not_to include('IdentitiesOnly') end it "omits StrictHostKeyChecking and UserKnownHostsFile when verify_host_key is true" do allow(machine).to receive(:ssh_info) { ssh_info.merge(verify_host_key: true) } output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).not_to include('StrictHostKeyChecking ') expect(output).not_to include('UserKnownHostsFile ') end it "formats windows paths if windows" do allow(machine).to receive(:ssh_info) { ssh_info.merge(private_key_path: ["C:\\path\\to\\vagrant\\home.key"]) } allow(Vagrant::Util::Platform).to receive(:format_windows_path).and_return("/home/vagrant/home.key") allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include('IdentityFile /home/vagrant/home.key') end it "handles verify_host_key :never value" do allow(machine).to receive(:ssh_info) { ssh_info.merge(verify_host_key: :never) } output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include('StrictHostKeyChecking ') expect(output).to include('UserKnownHostsFile ') end it "includes custom ssh_config path when provided" do allow(machine).to receive(:ssh_info) { ssh_info.merge(config: "/custom/ssh/config") } output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include("Include /custom/ssh/config") end it "includes enabling ssh-rsa key types and host algorithms" do output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include("PubkeyAcceptedKeyTypes +ssh-rsa") expect(output).to include("HostKeyAlgorithms +ssh-rsa") end it "does not enable ssh-rsa key types and host algorithms when disabled" do allow(machine).to receive(:ssh_info) { ssh_info.merge(disable_deprecated_algorithms: true) } output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).not_to include("PubkeyAcceptedKeyTypes +ssh-rsa") expect(output).not_to include("HostKeyAlgorithms +ssh-rsa") end end end ================================================ FILE: test/unit/plugins/commands/suspend/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/suspend/command") describe VagrantPlugins::CommandSuspend::Command do include_context "unit" let(:entry_klass) { Vagrant::MachineIndex::Entry } let(:argv) { [] } let(:vagrantfile_content){ "" } let(:iso_env) do env = isolated_environment env.vagrantfile(vagrantfile_content) env.create_vagrant_env end subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:machine2) { iso_env.machine(iso_env.machine_names[0], :dummy) } def new_entry(name) entry_klass.new.tap do |e| e.name = name e.vagrantfile_path = "/bar" end end before do allow(iso_env).to receive(:action_runner).and_return(action_runner) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end context "with no argument" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.define "app" config.vm.define "db" end VF end it "should suspend all vms" do allow(subject).to receive(:with_target_vms) { |&block| block.call machine block.call machine2 } expect(machine).to receive(:action) do |name, opts| expect(name).to eq(:suspend) end expect(machine2).to receive(:action) do |name, opts| expect(name).to eq(:suspend) end expect(subject.execute).to eq(0) end end context "with an argument" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.define "app" config.vm.define "db" end VF end let(:argv) { ["app"] } it "should suspend a vm" do expect(machine).to receive(:action) do |name, opts| expect(name).to eq(:suspend) end expect(subject.execute).to eq(0) end end context "with the global all flag" do let(:argv){ ["--all-global"] } it "should suspend all vms globally" do global_env = isolated_environment global_env.vagrantfile("Vagrant.configure(2){|config| config.vm.box = 'dummy'}") global_venv = global_env.create_vagrant_env global_machine = global_venv.machine(global_venv.machine_names[0], :dummy) global_machine.id = "1234" global = new_entry(global_machine.name) global.provider = "dummy" global.vagrantfile_path = global_env.workdir locked = iso_env.machine_index.set(global) iso_env.machine_index.release(locked) allow(subject).to receive(:with_target_vms) { |&block| block.call global_machine } expect(global_machine).to receive(:action) do |name, opts| expect(name).to eq(:suspend) end expect(subject.execute).to eq(0) end context "with an argument is used" do let(:argv){ ["machine", "--all-global"] } it "errors out" do expect{subject.execute}.to raise_error(Vagrant::Errors::CommandSuspendAllArgs) end end end end ================================================ FILE: test/unit/plugins/commands/up/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/up/command") describe VagrantPlugins::CommandUp::Command do include_context "unit" let(:entry_klass) { Vagrant::MachineIndex::Entry } let(:argv) { [] } let(:vagrantfile_content){ "" } let(:iso_env) do env = isolated_environment env.vagrantfile(vagrantfile_content) env.create_vagrant_env end subject { described_class.new(argv, iso_env) } let(:action_runner) { double("action_runner") } def new_entry(name) entry_klass.new.tap do |e| e.name = name e.vagrantfile_path = "/bar" end end before do allow(iso_env).to receive(:action_runner).and_return(action_runner) end context "with no argument" do let(:vagrantfile_content){ "Vagrant.configure(2){|config| config.vm.box = 'dummy'}" } it "should bring up the default box" do batch = double("environment_batch") expect(iso_env).to receive(:batch).and_yield(batch) expect(batch).to receive(:action).with(anything, :up, anything) subject.execute end context "with VAGRANT_DEFAULT_PROVIDER set" do before do if ENV["VAGRANT_DEFAULT_PROVIDER"] @original_default = ENV["VAGRANT_DEFAULT_PROVIDER"] end ENV["VAGRANT_DEFAULT_PROVIDER"] = "unknown" end after do if @original_default ENV["VAGRANT_DEFAULT_PROVIDER"] = @original_default else ENV.delete("VAGRANT_DEFAULT_PROVIDER") end end it "should attempt to use dummy provider" do expect{ subject.execute }.to raise_error(Vagrant::Errors::ProviderNotFound) end context "with --provider set" do let(:argv){ ["--provider", "dummy"] } it "should only use provider explicitly set" do batch = double("environment_batch") expect(iso_env).to receive(:batch).and_yield(batch) expect(batch).to receive(:action).with(anything, :up, anything) subject.execute end end end end context "with a global machine" do let(:argv){ ["1234"] } it "brings up a vm with an id" do global_env = isolated_environment global_env.vagrantfile("Vagrant.configure(2){|config| config.vm.box = 'dummy'}") global_venv = global_env.create_vagrant_env global_machine = global_venv.machine(global_venv.machine_names[0], :dummy) global_machine.id = "1234" global = new_entry(global_machine.name) global.provider = "dummy" global.vagrantfile_path = global_env.workdir locked = iso_env.machine_index.set(global) iso_env.machine_index.release(locked) allow(subject).to receive(:with_target_vms) { |&block| block.call global_machine } batch = double("environment_batch") expect(iso_env).to receive(:batch).and_yield(batch) expect(batch).to receive(:action).with(global_machine, :up, anything) do |machine,action,args| expect(machine).to be_kind_of(Vagrant::Machine) expect(action).to eq(:up) end subject.execute end end context "with an argument" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.define "app" config.vm.define "db" end VF end let(:argv){ ["app"] } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } it "brings up a vm" do batch = double("environment_batch") expect(iso_env).to receive(:batch).and_yield(batch) expect(batch).to receive(:action).with(machine, :up, anything) do |machine,action,args| expect(machine).to be_kind_of(Vagrant::Machine) expect(action).to eq(:up) end subject.execute end context "with an invalid argument" do let(:argv){ ["notweb"] } it "brings up a vm" do expect { subject.execute }.to raise_error(Vagrant::Errors::MachineNotFound) end end end end ================================================ FILE: test/unit/plugins/commands/up/middleware/store_box_metadata_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/up/middleware/store_box_metadata") describe VagrantPlugins::CommandUp::StoreBoxMetadata do include_context "unit" let(:app) { double("app") } let(:machine) { double("machine", box: box) } let(:box) { double("box", name: box_name, version: box_version, provider: box_provider, directory: box_directory ) } let(:box_name) { "BOX_NAME" } let(:box_version) { "1.0.0" } let(:box_provider) { "dummy" } let(:box_directory) { File.join(vagrant_user_data_path, box_directory_relative) } let(:box_directory_relative) { File.join("boxes", "BOX_NAME") } let(:vagrant_user_data_path) { "/vagrant/user/data" } let(:meta_path) { "META_PATH" } let(:env) { {machine: machine} } let(:subject) { described_class.new(app, env) } describe "#call" do context "with no box file" do let(:machine) { double("machine", name: "guest", provider_name: "provider") } let(:env) { {machine: machine} } before do allow(machine).to receive(:box).and_return(nil) expect(app).to receive(:call).with(env) end it "does not write the metadata file" do expect(File).to_not receive(:open) subject.call(env) end end let(:meta_file) { double("meta_file") } before do allow(Vagrant).to receive(:user_data_path).and_return(vagrant_user_data_path) allow(machine).to receive(:data_dir).and_return(meta_path) allow(meta_path).to receive(:join).with("box_meta").and_return(meta_path) allow(File).to receive(:open) expect(app).to receive(:call).with(env) end after { subject.call(env) } it "should open a metadata file" do expect(File).to receive(:open).with(meta_path, anything) end context "contents of metadata file" do before { expect(File).to receive(:open).with(meta_path, anything).and_yield(meta_file) } it "should be JSON data" do expect(meta_file).to receive(:write) do |data| val = JSON.parse(data) expect(val).to be_a(Hash) end end it "should include box name" do expect(meta_file).to receive(:write) do |data| val = JSON.parse(data) expect(val["name"]).to eq(box_name) end end it "should include box version" do expect(meta_file).to receive(:write) do |data| val = JSON.parse(data) expect(val["version"]).to eq(box_version) end end it "should include box provider" do expect(meta_file).to receive(:write) do |data| val = JSON.parse(data) expect(val["provider"]).to eq(box_provider) end end it "should include relative box directory" do expect(meta_file).to receive(:write) do |data| val = JSON.parse(data) expect(val["directory"]).to eq(box_directory_relative) end end end end end ================================================ FILE: test/unit/plugins/commands/upload/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/upload/command") describe VagrantPlugins::CommandUpload::Command do include_context "unit" include_context "virtualbox" 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", capability_host_chain: guest_chain) } let(:host) { double("host", capability_host_chain: host_chain) } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("communicator") } let(:host_chain){ [[]] } let(:guest_chain){ [[]] } let(:argv) { [] } let(:config) { double("config") } subject { described_class.new(argv, iso_env) } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:config).and_return(config) allow(subject).to receive(:with_target_vms) end it "should raise invalid usage error by default" do expect { subject.execute }.to raise_error(Vagrant::Errors::CLIInvalidUsage) end context "when three arguments are provided" do let(:argv) { ["source", "destination", "guest"] } before { allow(File).to receive(:file?).and_return(true) } it "should use third argument as name of guest" do expect(subject).to receive(:with_target_vms).with("guest", any_args) subject.execute end it "should use first argument as source and second as destination" do allow(subject).to receive(:with_target_vms) { |&block| block.call machine } expect(communicator).to receive(:upload).with("source", "destination") subject.execute end end context "when two arguments are provided" do let(:argv) { ["source", "ambiguous"] } let(:active_machines) { [] } before do allow(File).to receive(:file?).and_return(true) allow(iso_env).to receive(:active_machines).and_return(active_machines) end it "should use the the second argument as destination when not a machine name" do allow(subject).to receive(:with_target_vms) { |&block| block.call machine } expect(communicator).to receive(:upload).with("source", "ambiguous") subject.execute end context "when active machine matches second argument" do let(:active_machines) { [["ambiguous"]] } it "should use second argument as guest name and generate destination" do allow(subject).to receive(:with_target_vms).with("ambiguous", any_args) { |&block| block.call machine } expect(communicator).to receive(:upload).with("source", "source") subject.execute end end end context "when single argument is provided" do let(:argv) { ["item"] } before do allow(File).to receive(:file?) allow(File).to receive(:directory?) end it "should check if source is a file" do expect(File).to receive(:file?).with("item").and_return(true) subject.execute end it "should check if source is a directory" do expect(File).to receive(:directory?).with("item").and_return(true) subject.execute end it "should raise error if source is not a directory or file" do expect { subject.execute }.to raise_error(Vagrant::Errors::UploadSourceMissing) end context "when source path ends with double quote" do let(:argv) { [".\\item\""] } it "should remove trailing quote" do expect(File).to receive(:file?).with(".\\item").and_return(true) subject.execute end end context "when source path ends with single quote" do let(:argv) { ['.\item\''] } it "should remove trailing quote" do expect(File).to receive(:file?).with(".\\item").and_return(true) subject.execute end end context "when source is a directory" do before do allow(File).to receive(:file?).with("item").and_return(false) allow(File).to receive(:directory?).with("item").and_return(true) allow(communicator).to receive(:upload) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end it "should upload the directory" do expect(communicator).to receive(:upload).with(/item/, anything) subject.execute end it "should append separator and dot to source path for upload" do expect(communicator).to receive(:upload).with("item/.", anything) subject.execute end end context "when source is a file" do before do allow(File).to receive(:file?).with("item").and_return(true) allow(communicator).to receive(:upload) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } allow(machine).to receive(:guest).and_return(guest) allow(machine).to receive(:env).and_return(double("env", host: host)) allow(guest).to receive(:capability?).and_return(true) allow(guest).to receive(:capability) end it "should upload the file" do expect(communicator).to receive(:upload).with("item", anything) subject.execute end it "should name destination after the source" do expect(communicator).to receive(:upload).with("item", "item") subject.execute end context "when temporary option is set" do before { argv << "-t" } it "should get temporary path for destination from guest" do expect(guest).to receive(:capability).with(:create_tmp_path, any_args).and_return("TMP_PATH") expect(communicator).to receive(:upload).with("item", "TMP_PATH") subject.execute end end context "when compress option is set" do before do argv << "-c" allow(guest).to receive(:capability).with(:create_tmp_path, any_args).and_return("TMP") allow(subject).to receive(:compress_source_zip).and_return("COMPRESS_SOURCE") allow(subject).to receive(:compress_source_tgz).and_return("COMPRESS_SOURCE") allow(FileUtils).to receive(:rm).with("COMPRESS_SOURCE") end it "should check for guest decompression" do expect(guest).to receive(:capability?).with(:decompress_tgz).and_return(true) subject.execute end it "should compress the source" do expect(subject).to receive(:compress_source_tgz).with("item").and_return("COMPRESS_SOURCE") subject.execute end it "should cleanup compressed source" do expect(FileUtils).to receive(:rm).with("COMPRESS_SOURCE") subject.execute end it "should upload the compressed source" do expect(communicator).to receive(:upload).with("COMPRESS_SOURCE", anything) subject.execute end it "should upload compressed source to temporary location" do expect(communicator).to receive(:upload).with("COMPRESS_SOURCE", "TMP") subject.execute end it "should have guest decompress file" do expect(guest).to receive(:capability).with(:decompress_tgz, "TMP", any_args) subject.execute end it "should provide destination for guest decompression of file" do expect(guest).to receive(:capability).with(:decompress_tgz, "TMP", "item", any_args) subject.execute end it "should provide the destination type for guest decompression" do expect(guest).to receive(:capability).with(:decompress_tgz, "TMP", "item", hash_including(type: :file)) subject.execute end context "with compression type set to zip" do before { argv << "-C" << "zip" } it "should test guest for decompression capability" do expect(guest).to receive(:capability?).with(:decompress_zip).and_return(true) subject.execute end it "should compress source using zip" do expect(subject).to receive(:compress_source_zip) subject.execute end it "should have guest decompress file using zip" do expect(guest).to receive(:capability).with(:decompress_zip, any_args) subject.execute end end end end end end ================================================ FILE: test/unit/plugins/commands/validate/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require_relative "../../../../../plugins/commands/validate/command" describe VagrantPlugins::CommandValidate::Command do include_context "unit" include_context "command plugin helpers" let(:vagrantfile_content){ "" } let(:iso_env) do env = isolated_environment env.vagrantfile(vagrantfile_content) env.create_vagrant_env end let(:action_runner) { double("action_runner") } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:argv) { [] } before(:all) do I18n.load_path << Vagrant.source_root.join("plugins/commands/port/locales/en.yml") I18n.reload! end subject { described_class.new(argv, iso_env) } describe "#execute" do context "validating configs" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.box = "hashicorp/precise64" config.vm.synced_folder ".", "/vagrant", disabled: true end VF end it "validates correct Vagrantfile" do expect(machine).to receive(:action_raw) do |name, action, env| expect(name).to eq(:config_validate) expect(action).to eq(Vagrant::Action::Builtin::ConfigValidate) expect(env).to eq({}) end expect(iso_env.ui).to receive(:info).with(any_args) { |message, _| expect(message).to include("Vagrantfile validated successfully.") } expect(subject.execute).to eq(0) end end context "invalid configs" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.bix = "hashicorp/precise64" config.vm.synced_folder ".", "/vagrant", disabled: true end VF end it "validates the configuration" do expect { subject.execute }.to raise_error(Vagrant::Errors::ConfigInvalid) { |err| expect(err.message).to include("The following settings shouldn't exist: bix") } end end context "valid configs for multiple vms" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.box = "hashicorp/precise64" config.vm.synced_folder ".", "/vagrant", disabled: true config.vm.define "test" do |vm| vm.vm.provider :virtualbox end config.vm.define "machine" do |vm| vm.vm.provider :virtualbox end end VF end it "validates correct Vagrantfile of all vms" do expect(machine).to receive(:action_raw) do |name, action, env| expect(name).to eq(:config_validate) expect(action).to eq(Vagrant::Action::Builtin::ConfigValidate) expect(env).to eq({}) end expect(iso_env.ui).to receive(:info).with(any_args) { |message, _| expect(message).to include("Vagrantfile validated successfully.") } expect(subject.execute).to eq(0) end end context "an invalid config for some vms" do let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.box = "hashicorp/precise64" config.vm.synced_folder ".", "/vagrant", disabled: true config.vm.define "test" do |vm| vm.vm.provider :virtualbox end config.vm.define "machine" do |vm| vm.vm.not_provider :virtualbox end end VF end it "validates the configuration of all vms" do expect(machine).to receive(:action_raw) do |name, action, env| expect(name).to eq(:config_validate) expect(action).to eq(Vagrant::Action::Builtin::ConfigValidate) expect(env).to eq({}) end expect { subject.execute }.to raise_error(Vagrant::Errors::ConfigInvalid) { |err| expect(err.message).to include("The following settings shouldn't exist: not_provider") } end end context "with the ignore provider flag" do let(:argv) { ["--ignore-provider"]} let(:vagrantfile_content) do <<-VF Vagrant.configure("2") do |config| config.vm.box = "hashicorp/precise64" config.vm.synced_folder ".", "/vagrant", disabled: true config.vm.define "test" do |vm| vm.vm.hostname = "test" vm.vm.provider :virtualbox do |v| v.not_a_real_option = true end end end VF end it "ignores provider specific configurations with the flag" do allow(subject).to receive(:mockup_providers!).and_return("") allow(FileUtils).to receive(:remove_entry).and_return(true) expect(iso_env.ui).to receive(:info).with(any_args) { |message, _| expect(message).to include("Vagrantfile validated successfully.") } expect(machine).to receive(:action_raw) do |name, action, env| expect(name).to eq(:config_validate) expect(action).to eq(Vagrant::Action::Builtin::ConfigValidate) expect(env).to eq({:ignore_provider=>true}) end expect(subject.execute).to eq(0) end end context "no vagrantfile" do let(:vagrantfile_content){ "" } let(:env) { isolated_environment.create_vagrant_env } subject { described_class.new(argv, env) } it "throws an exception if there's no Vagrantfile" do expect { subject.execute }.to raise_error(Vagrant::Errors::NoEnvironmentError) end end end end ================================================ FILE: test/unit/plugins/commands/winrm/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/winrm/command") describe VagrantPlugins::CommandWinRM::Command do include_context "unit" include_context "virtualbox" 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(:communicator) { double("communicator") } let(:argv) { [] } let(:config) { double("config", vm: double("vm-config", communicator: communicator_name)) } let(:communicator_name) { :winrm } subject { described_class.new(argv, iso_env) } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:config).and_return(config) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } allow(communicator).to receive(:execute) end it "should exit successfully when no command is provided" do expect(subject.execute).to eq(0) end context "with command provided" do let(:argv){ ["-c", "dir"] } it "should execute the command via the communicator" do expect(communicator).to receive(:execute).with("dir", any_args) subject.execute end it "should execute with default shell" do expect(communicator).to receive(:execute).with(anything, hash_including(shell: :powershell)) subject.execute end it "should execute without elevated privileges" do expect(communicator).to receive(:execute).with(anything, hash_including(elevated: false)) subject.execute end context "with elevated option set" do let(:argv) { ["-c", "dir", "-e"] } it "should execute with elevated privileges" do expect(communicator).to receive(:execute).with(anything, hash_including(elevated: true)) subject.execute end end context "with shell option set" do let(:argv) { ["-c", "dir", "-s", "cmd"] } it "should execute with custom shell" do expect(communicator).to receive(:execute).with(anything, hash_including(shell: :cmd)) subject.execute end end end context "with multiple command provided" do let(:argv) { ["-c", "dir", "-c", "dir2"] } it "should execute multiple commands via the communicator" do expect(communicator).to receive(:execute).with("dir", any_args) expect(communicator).to receive(:execute).with("dir2", any_args) subject.execute end end context "with invalid communicator configured" do let(:communicator_name) { :ssh } it "should raise an error" do expect { subject.execute }.to raise_error(Vagrant::Errors::WinRMInvalidCommunicator) end end end ================================================ FILE: test/unit/plugins/commands/winrm_config/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/commands/winrm_config/command") require Vagrant.source_root.join("plugins/communicators/winrm/helper") describe VagrantPlugins::CommandWinRMConfig::Command do include_context "unit" include_context "virtualbox" 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) { [] } let(:winrm_info) {{ host: "testhost.vagrant.dev", port: 1234 }} let(:config) { double("config", winrm: double("winrm-config", username: "vagrant", password: "vagrant"), rdp: rdp_config, vm: double("vm-config", communicator: :winrm) ) } let(:rdp_config) { double("rdp-config", port: 9876) } subject { described_class.new(argv, iso_env) } before do allow(machine).to receive(:config).and_return(config) allow(VagrantPlugins::CommunicatorWinRM::Helper).to receive(:winrm_info).and_return(winrm_info) allow(subject).to receive(:with_target_vms) { |&block| block.call machine } end describe "execute" do it "prints out the winrm config for the given machine" do output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to eq(<<-WINRMCONFIG) Host #{machine.name} HostName testhost.vagrant.dev User vagrant Password vagrant Port 1234 RDPHostName testhost.vagrant.dev RDPPort 9876 RDPUser vagrant RDPPassword vagrant WINRMCONFIG end context "with host option set" do let(:argv) { ["--host", "my-host"]} it "should use custom host name in config output" do output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to eq(<<-WINRMCONFIG) Host my-host HostName testhost.vagrant.dev User vagrant Password vagrant Port 1234 RDPHostName testhost.vagrant.dev RDPPort 9876 RDPUser vagrant RDPPassword vagrant WINRMCONFIG end end context "when no RDP port is configured" do let(:rdp_config) { double("rdp-config", port: nil) } it "should not include any RDP configuration information" do output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).not_to include("RDP") end end context "when provider has rdp_info capability" do let(:rdp_info) { {host: "provider-host", port: 9999, username: "pvagrant", password: "pvagrant"} } before do allow(machine.provider).to receive(:capability?).with(:rdp_info).and_return(true) allow(machine.provider).to receive(:capability).with(:rdp_info).and_return(rdp_info) end it "should use provider RDP information" do output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include("RDPPort 9999") expect(output).to include("RDPHostName provider-host") expect(output).to include("RDPUser pvagrant") expect(output).to include("RDPPassword pvagrant") end context "when provider rdp_info does not include host" do before { rdp_info[:host] = nil } it "should use winrm host" do output = "" allow(subject).to receive(:safe_puts) do |data| output += data if data end subject.execute expect(output).to include("RDPHostName testhost.vagrant.dev") end end end end end ================================================ FILE: test/unit/plugins/communicators/none/communicator_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/communicators/none/communicator") describe VagrantPlugins::CommunicatorNone::Communicator do include_context "unit" let(:machine) { double(:machine) } subject { described_class.new(machine) } context "#ready?" do it "should be true" do expect(subject.ready?).to be end end context "#execute" do it "should be successful regardless of command" do expect(subject.execute("/bin/false")).to eq(0) end end context "#sudo" do it "should be successful regardless of command" do expect(subject.execute("/bin/false")).to eq(0) end end context "test" do it "should be true" do expect(subject.test("/bin/false")).to be end end end ================================================ FILE: test/unit/plugins/communicators/ssh/communicator_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/communicators/ssh/communicator") describe VagrantPlugins::CommunicatorSSH::Communicator do include_context "unit" let(:export_command_template){ 'export %ENV_KEY%="%ENV_VALUE%"' } # SSH configuration information mock let(:ssh) do double("ssh", key_type: :auto, timeout: 1, host: nil, port: 5986, guest_port: 5986, pty: false, keep_alive: false, insert_key: insert_ssh_key, export_command_template: export_command_template, shell: 'bash -l' ) end # Do not insert public key by default let(:insert_ssh_key){ false } # Configuration mock let(:config) { double("config", ssh: ssh) } # Provider mock let(:provider) { double("provider") } let(:ui) { Vagrant::UI::Silent.new } # Machine mock built with previously defined let(:machine) do double("machine", config: config, provider: provider, ui: ui, env: env ) end let(:env){ double("env", host: host) } let(:host){ double("host") } # SSH information of the machine let(:machine_ssh_info){ {host: '10.1.2.3', port: 22} } # Subject instance to test let(:communicator){ @communicator ||= described_class.new(machine) } # Underlying net-ssh connection mock let(:connection) { double("connection") } # Base net-ssh connection channel mock let(:channel) { double("channel") } # net-ssh connection channel mock for running commands let(:command_channel) { double("command_channel") } # Default exit data for commands run let(:exit_data) { double("exit_data", read_long: 0) } # Core shell command used when starting command connection let(:core_shell_cmd) { "bash -l" } # Marker used for flagging start of output let(:command_garbage_marker) { communicator.class.const_get(:CMD_GARBAGE_MARKER) } # Start marker output when PTY is enabled let(:pty_delim_start) { communicator.class.const_get(:PTY_DELIM_START) } # End marker output when PTY is enabled let(:pty_delim_end) { communicator.class.const_get(:PTY_DELIM_END) } # Command output returned on stdout let(:command_stdout_data) { '' } # Command output returned on stderr let(:command_stderr_data) { '' } # Mock for net-ssh scp let(:scp) { double("scp") } # Value returned from remote ssh supported key check let(:sudo_supported_key_list) { "pubkeyacceptedalgorithms ssh-rsa" } # Setup for commands using the net-ssh connection. This can be reused where needed # by providing to `before` connection_setup = proc do allow(connection).to receive(:closed?).and_return false allow(connection).to receive(:open_channel). and_yield(channel).and_return(channel) allow(connection).to receive(:close) allow(channel).to receive(:wait).and_return true allow(channel).to receive(:close) allow(command_channel).to receive(:send_data) allow(command_channel).to receive(:eof!) allow(command_channel).to receive(:on_data). and_yield(nil, command_stdout_data) allow(command_channel).to receive(:on_extended_data). and_yield(nil, nil, command_stderr_data) allow(machine).to receive(:ssh_info).and_return(machine_ssh_info) allow(channel).to receive(:exec).with(core_shell_cmd). and_yield(command_channel, '').and_return channel allow(command_channel).to receive(:on_request).with('exit-status'). and_yield(nil, exit_data) # Return mocked net-ssh connection during setup allow(communicator).to receive(:retryable).and_return(connection) # Stub in a response for supported key types check allow(communicator).to receive(:sudo).with("sshd -T | grep key", any_args). and_yield(:stdout, sudo_supported_key_list).and_return(0) end before do allow(host).to receive(:capability?).and_return(false) end describe "#wait_for_ready" do before(&connection_setup) context "with no static config (default scenario)" do context "when ssh_info requires a multiple tries before it is ready" do before do expect(machine).to receive(:ssh_info). and_return(nil).ordered expect(machine).to receive(:ssh_info). and_return(host: '10.1.2.3', port: 22).ordered end it "retries ssh_info until ready" do # retries are every 0.5 so buffer the timeout just a hair over expect(communicator.wait_for_ready(0.6)).to eq(true) end end context "when printing message to the user" do before do allow(machine).to receive(:ssh_info). and_return(host: '10.1.2.3', port: 22) allow(communicator).to receive(:connect) allow(communicator).to receive(:ready?).and_return(true) allow(ui).to receive(:detail).and_call_original end it "should print message" do expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout) expect(ui).to receive(:detail).with(/timeout/).and_call_original communicator.wait_for_ready(0.5) end it "should not print the same message twice" do expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout) expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout) expect(ui).to receive(:detail).with(/timeout/).and_call_original expect(ui).not_to receive(:detail).with(/timeout/) communicator.wait_for_ready(0.5) end it "should print different messages" do expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout) expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHDisconnected) expect(ui).to receive(:detail).with(/timeout/).and_call_original expect(ui).to receive(:detail).with(/disconnect/).and_call_original communicator.wait_for_ready(0.5) end it "should not print different messages twice" do expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout) expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHDisconnected) expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHConnectionTimeout) expect(communicator).to receive(:connect).and_raise(Vagrant::Errors::SSHDisconnected) expect(ui).to receive(:detail).with(/timeout/).and_call_original expect(ui).to receive(:detail).with(/disconnect/).and_call_original expect(ui).not_to receive(:detail).with(/timeout/) expect(ui).not_to receive(:detail).with(/disconnect/) communicator.wait_for_ready(0.5) end end end end describe "#reset!" do let(:connection) { double("connection") } before do allow(communicator).to receive(:wait_for_ready) allow(connection).to receive(:close) communicator.send(:instance_variable_set, :@connection, connection) end it "should close existing connection" do expect(connection).to receive(:close) communicator.reset! end it "should call wait_for_ready to re-enable the connection" do expect(communicator).to receive(:wait_for_ready) communicator.reset! end end describe "#ready?" do before(&connection_setup) it "returns true if shell test is successful" do expect(communicator.ready?).to be(true) end context "with an invalid shell test" do before do expect(exit_data).to receive(:read_long).and_return 1 end it "returns raises SSHInvalidShell error" do expect{ communicator.ready? }.to raise_error Vagrant::Errors::SSHInvalidShell end end context "with insert key enabled" do before do allow(machine).to receive(:guest).and_return(guest) allow(guest).to receive(:capability?).with(:insert_public_key). and_return(has_insert_cap) allow(guest).to receive(:capability?).with(:remove_public_key). and_return(has_remove_cap) allow(communicator).to receive(:insecure_key?).with("KEY_PATH").and_return(true) end let(:insert_ssh_key){ true } let(:has_insert_cap){ false } let(:has_remove_cap){ false } let(:machine_ssh_info){ {host: '10.1.2.3', port: 22, private_key_path: ["KEY_PATH"]} } let(:guest){ double("guest") } context "without guest insert_ssh_key or remove_ssh_key capabilities" do it "should raise an error" do expect{ communicator.ready? }.to raise_error(Vagrant::Errors::SSHInsertKeyUnsupported) end end context "without guest insert_ssh_key capability" do let(:has_remove_cap){ true } it "should raise an error" do expect{ communicator.ready? }.to raise_error(Vagrant::Errors::SSHInsertKeyUnsupported) end end context "without guest remove_ssh_key capability" do let(:has_insert_cap){ true } it "should raise an error" do expect{ communicator.ready? }.to raise_error(Vagrant::Errors::SSHInsertKeyUnsupported) end end context "with guest insert_ssh_key capability and remove_ssh_key capability" do let(:has_insert_cap){ true } let(:has_remove_cap){ true } let(:new_public_key){ :new_public_key } let(:new_private_key){ :new_private_key } let(:openssh){ :openssh } let(:private_key_file){ double("private_key_file") } let(:path_joiner){ double("path_joiner") } let(:algorithms) { double(:algorithms) } let(:transport) { double(:transport, algorithms: algorithms) } before do allow(Vagrant::Util::Keypair).to receive(:create). and_return([new_public_key, new_private_key, openssh]) allow(private_key_file).to receive(:open).and_yield(private_key_file) allow(private_key_file).to receive(:write) allow(private_key_file).to receive(:chmod) allow(guest).to receive(:capability) allow(File).to receive(:chmod) allow(machine).to receive(:data_dir).and_return(path_joiner) allow(path_joiner).to receive(:join).and_return(private_key_file) allow(guest).to receive(:capability).with(:insert_public_key) allow(guest).to receive(:capability).with(:remove_public_key) allow(connection).to receive(:transport).and_return(transport) allow(communicator).to receive(:supported_key_types).and_raise(described_class.const_get(:ServerDataError)) end it "should create a new key pair" do expect(Vagrant::Util::Keypair).to receive(:create). and_return([new_public_key, new_private_key, openssh]) communicator.ready? end it "should call the insert_public_key guest capability" do expect(guest).to receive(:capability).with(:insert_public_key, openssh) communicator.ready? end it "should write the new private key" do expect(private_key_file).to receive(:write).with(new_private_key) communicator.ready? end it "should call the set_ssh_key_permissions host capability" do expect(host).to receive(:capability?).with(:set_ssh_key_permissions).and_return(true) expect(host).to receive(:capability).with(:set_ssh_key_permissions, private_key_file) communicator.ready? end it "should remove the default public key" do expect(guest).to receive(:capability).with(:remove_public_key, any_args) communicator.ready? end context "with server algorithm support data" do before do allow(communicator).to receive(:supported_key_types).and_return(valid_key_types) end context "when rsa is the only match" do let(:valid_key_types) { ["ssh-ecdsa", "ssh-rsa"] } it "should use rsa type" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :rsa).and_call_original communicator.ready? end end context "when ed25519 and rsa are both available" do let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa"] } it "should use ed25519 type" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :ed25519).and_call_original communicator.ready? end end context "when ed25519 is the only match" do let(:valid_key_types) { ["ssh-ecdsa", "ssh-ed25519"] } it "should use ed25519 type" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :ed25519).and_call_original communicator.ready? end end context "with key_type set as :auto in configuration" do let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa"] } before { allow(ssh).to receive(:key_type).and_return(:auto) } it "should use the preferred ed25519 key type" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :ed25519).and_call_original communicator.ready? end context "when no supported key type is detected" do let(:valid_key_types) { ["fake-type", "other-fake-type"] } it "should raise an error" do expect { communicator.ready? }.to raise_error(Vagrant::Errors::SSHKeyTypeNotSupportedByServer) end end end context "with key_type set as :ecdsa521 in configuration" do let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp256"] } before { allow(ssh).to receive(:key_type).and_return(:ecdsa521) } it "should use the requested key type" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :ecdsa521).and_call_original communicator.ready? end context "when requested key type is not supported" do let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256"] } it "should raise an error" do expect { communicator.ready? }.to raise_error(Vagrant::Errors::SSHKeyTypeNotSupportedByServer) end end end end context "when an error is encountered getting server data" do before do expect(communicator).to receive(:supported_key_types).and_raise(StandardError) end it "should default to rsa key" do expect(Vagrant::Util::Keypair).to receive(:create). with(type: :rsa).and_call_original communicator.ready? end end end end end describe "#execute" do before(&connection_setup) it "runs valid command and returns successful status code" do expect(command_channel).to receive(:send_data).with(/ls \/\n/) expect(communicator.execute("ls /")).to eq(0) end it "prepends UUID output to command for garbage removal" do expect(command_channel).to receive(:send_data). with("printf '#{command_garbage_marker}'\n(>&2 printf '#{command_garbage_marker}')\nls /\n") expect(communicator.execute("ls /")).to eq(0) end context "with command returning an error" do let(:exit_data) { double("exit_data", read_long: 1) } it "raises error when exit-code is non-zero" do expect(command_channel).to receive(:send_data).with(/ls \/\n/) expect{ communicator.execute("ls /") }.to raise_error(Vagrant::Errors::VagrantError) end it "returns exit-code when exit-code is non-zero and error check is disabled" do expect(command_channel).to receive(:send_data).with(/ls \/\n/) expect(communicator.execute("ls /", error_check: false)).to eq(1) end end context "with no exit code received" do let(:exit_data) { double("exit_data", read_long: nil) } it "raises error when exit code is nil" do expect(command_channel).to receive(:send_data).with(/make\n/) expect{ communicator.execute("make") }.to raise_error(Vagrant::Errors::SSHNoExitStatus) end end context "with garbage content prepended to command output" do let(:command_stdout_data) do "Line of garbage\nMore garbage\n#{command_garbage_marker}bin\ntmp\n" end let(:command_stderr_data) { "some data" } it "removes any garbage output prepended to command output" do stdout = '' expect( communicator.execute("ls /") do |type, data| if type == :stdout stdout << data end end ).to eq(0) expect(stdout).to eq("bin\ntmp\n") end it "should not receive any stderr data" do stderr = '' communicator.execute("ls /") do |type, data| if type == :stderr stderr << data end end expect(stderr).to be_empty end end context "with no command output" do let(:command_stdout_data) do "#{command_garbage_marker}" end let(:command_stderr_data) { "some data" } it "does not send empty stdout data string" do empty = true expect( communicator.execute("ls /") do |type, data| if type == :stdout && data.empty? empty = false end end ).to eq(0) expect(empty).to be(true) end it "should not receive any stderr data" do stderr = '' communicator.execute("ls /") do |type, data| if type == :stderr stderr << data end end expect(stderr).to be_empty end end context "with garbage content prepended to command stderr output" do let(:command_stderr_data) do "Line of garbage\nMore garbage\n#{command_garbage_marker}bin\ntmp\n" end let(:command_stdout_data) { "some data" } it "removes any garbage output prepended to command stderr output" do stderr = '' expect( communicator.execute("ls /") do |type, data| if type == :stderr stderr << data end end ).to eq(0) expect(stderr).to eq("bin\ntmp\n") end it "should not receive any stdout data" do stdout = '' communicator.execute("ls /") do |type, data| if type == :stdout stdout << data end end expect(stdout).to be_empty end end context "with no command output on stderr" do let(:command_stderr_data) do "#{command_garbage_marker}" end let(:command_std_data) { "some data" } it "does not send empty stderr data string" do empty = true expect( communicator.execute("ls /") do |type, data| if type == :stderr && data.empty? empty = false end end ).to eq(0) expect(empty).to be(true) end it "should not receive any stdout data" do stdout = '' communicator.execute("ls /") do |type, data| if type == :stdout stdout << data end end expect(stdout).to be_empty end end context "with pty enabled" do before do expect(ssh).to receive(:pty).and_return true expect(channel).to receive(:request_pty).and_yield(command_channel, true) expect(command_channel).to receive(:send_data). with(/#{Regexp.escape(pty_delim_start)}/) end let(:command_stdout_data) do "#{pty_delim_start}bin\ntmp\n#{pty_delim_end}" end it "requests pty for connection" do expect(communicator.execute("ls")).to eq(0) end context "with sudo enabled" do let(:core_shell_cmd){ 'sudo bash -l' } before do expect(ssh).to receive(:sudo_command).and_return 'sudo %c' end it "wraps command in elevated shell when sudo is true" do expect(communicator.execute("ls", sudo: true)).to eq(0) end end end context "with sudo enabled" do let(:core_shell_cmd){ 'sudo bash -l' } before do expect(ssh).to receive(:sudo_command).and_return 'sudo %c' end it "wraps command in elevated shell when sudo is true" do expect(communicator.execute("ls", sudo: true)).to eq(0) end end end describe "#test" do before(&connection_setup) context "with exit code as zero" do it "returns true" do expect(communicator.test("ls")).to be(true) end end context "with exit code as non-zero" do before do expect(exit_data).to receive(:read_long).and_return 1 end it "returns false" do expect(communicator.test("/bin/false")).to be(false) end end end describe "#upload" do before do expect(communicator).to receive(:scp_connect).and_yield(scp) allow(communicator).to receive(:create_remote_directory) end context "directory uploads" do let(:test_dir) { @dir } let(:test_file) { File.join(test_dir, "test-file") } let(:dir_name) { File.basename(test_dir) } let(:file_name) { File.basename(test_file) } before do @dir = Dir.mktmpdir("vagrant-test") FileUtils.touch(test_file) end after { FileUtils.rm_rf(test_dir) } it "uploads directory when directory path provided" do expect(scp).to receive(:upload!).with(instance_of(File), File.join("", "destination", dir_name, file_name)) communicator.upload(test_dir, "/destination") end it "uploads contents of directory when dot suffix provided on directory" do expect(scp).to receive(:upload!).with(instance_of(File), File.join("", "destination", file_name)) communicator.upload(File.join(test_dir, "."), "/destination") end it "creates directories before upload" do expect(communicator).to receive(:create_remote_directory).with( /#{Regexp.escape(File.join("", "destination", dir_name))}/) allow(scp).to receive(:upload!) communicator.upload(test_dir, "/destination") end end it "uploads a file if local path is a file" do file = Tempfile.new('vagrant-test') begin expect(scp).to receive(:upload!).with(instance_of(File), '/destination/file') communicator.upload(file.path, '/destination/file') ensure file.delete end end it "uploads file to directory if destination ends with file separator" do file = Tempfile.new('vagrant-test') begin expect(scp).to receive(:upload!).with(instance_of(File), "/destination/dir/#{File.basename(file.path)}") expect(communicator).to receive(:create_remote_directory).with("/destination/dir") communicator.upload(file.path, "/destination/dir/") ensure file.delete end end it "creates remote directory path to destination on upload" do file = Tempfile.new('vagrant-test') begin expect(scp).to receive(:upload!).with(instance_of(File), "/destination/dir/file.txt") expect(communicator).to receive(:create_remote_directory).with("/destination/dir") communicator.upload(file.path, "/destination/dir/file.txt") ensure file.delete end end it "creates remote directory given an empty directory" do file = Dir.mktmpdir begin expect(communicator).to receive(:create_remote_directory).with("/destination/dir/#{ File.basename(file)}/") communicator.upload(file, "/destination/dir") ensure FileUtils.rm_rf(file) end end it "raises custom error on permission errors" do file = Tempfile.new('vagrant-test') begin expect(scp).to receive(:upload!).with(instance_of(File), '/destination/file'). and_raise("Permission denied") expect{ communicator.upload(file.path, '/destination/file') }.to( raise_error(Vagrant::Errors::SCPPermissionDenied) ) ensure file.delete end end it "does not raise custom error on non-permission errors" do file = Tempfile.new('vagrant-test') begin expect(scp).to receive(:upload!).with(instance_of(File), '/destination/file'). and_raise("Some other error") expect{ communicator.upload(file.path, '/destination/file') }.to raise_error(RuntimeError) ensure file.delete end end end describe "#download" do before do expect(communicator).to receive(:scp_connect).and_yield(scp) end it "calls scp to download file" do expect(scp).to receive(:download!).with('/path/from', '/path/to') communicator.download('/path/from', '/path/to') end end describe "#connect" do it "cannot be called directly" do expect{ communicator.connect }.to raise_error(NoMethodError) end context "with default configuration" do before do expect(machine).to receive(:ssh_info).and_return( host: nil, port: nil, private_key_path: nil, username: nil, password: nil, keys_only: true, verify_host_key: false ) end it "has keys_only enabled" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( keys_only: true ) ).and_return(true) communicator.send(:connect) end it "has verify_host_key disabled" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( verify_host_key: false ) ).and_return(true) communicator.send(:connect) end it "does not include any private key paths" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_excluding( keys: anything ) ).and_return(true) communicator.send(:connect) end it "includes `none` and `hostbased` auth methods" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( auth_methods: ["none", "hostbased"] ) ).and_return(true) communicator.send(:connect) end context "keep ssh connection alive" do let(:ssh) do double("ssh", timeout: 1, host: nil, port: 5986, guest_port: 5986, pty: false, keep_alive: true, insert_key: insert_ssh_key, export_command_template: export_command_template, shell: 'bash -l' ) end it "sets keepalive settings" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( keepalive: true, keepalive_interval: 5 ) ).and_return(true) communicator.send(:connect) end end end context "with keys_only disabled and verify_host_key enabled" do before do expect(machine).to receive(:ssh_info).and_return( host: nil, port: nil, private_key_path: nil, username: nil, password: nil, keys_only: false, verify_host_key: true ) end it "has keys_only enabled" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( keys_only: false ) ).and_return(true) communicator.send(:connect) end it "has verify_host_key disabled" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( verify_host_key: true ) ).and_return(true) communicator.send(:connect) end end context "with host and port configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, private_key_path: nil, username: nil, password: nil, keys_only: true, verify_host_key: false ) end it "specifies configured host" do expect(Net::SSH).to receive(:start).with("127.0.0.1", anything, anything) communicator.send(:connect) end it "has port defined" do expect(Net::SSH).to receive(:start).with("127.0.0.1", anything, hash_including(port: 2222)) communicator.send(:connect) end end context "with private_key_path configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, private_key_path: ['/priv/key/path'], username: nil, password: nil, keys_only: true, verify_host_key: false ) end it "includes private key paths" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( keys: ["/priv/key/path"] ) ).and_return(true) communicator.send(:connect) end it "includes `publickey` auth method" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( auth_methods: ["none", "hostbased", "publickey"] ) ).and_return(true) communicator.send(:connect) end end context "with username and password configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, private_key_path: nil, username: 'vagrant', password: 'vagrant', keys_only: true, verify_host_key: false ) end it "has username defined" do expect(Net::SSH).to receive(:start).with(anything, 'vagrant', anything).and_return(true) communicator.send(:connect) end it "has password defined" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( password: 'vagrant' ) ).and_return(true) communicator.send(:connect) end it "includes `password` auth method" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( auth_methods: ["none", "hostbased", "password"] ) ).and_return(true) communicator.send(:connect) end end context "with password and private_key_path configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, private_key_path: ['/priv/key/path'], username: 'vagrant', password: 'vagrant', keys_only: true, verify_host_key: false ) end it "has password defined" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( password: 'vagrant' ) ).and_return(true) communicator.send(:connect) end it "includes private key paths" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( keys: ["/priv/key/path"] ) ).and_return(true) communicator.send(:connect) end it "includes `publickey` and `password` auth methods" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( auth_methods: ["none", "hostbased", "publickey", "password"] ) ).and_return(true) communicator.send(:connect) end end context "with config configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, config: './ssh_config', keys_only: true, verify_host_key: false ) end it "has config defined" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( config: './ssh_config' ) ).and_return(true) communicator.send(:connect) end end context "with remote_user configured" do let(:remote_user) { double("remote_user") } before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, remote_user: remote_user ) end it "has remote_user defined" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( remote_user: remote_user ) ).and_return(true) communicator.send(:connect) end end context "with connect_timeout configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, connect_timeout: 30 ) end it "has connect_timeout defined" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( timeout: 30 ) ).and_return(true) communicator.send(:connect) end end context "with connect_retries configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, connect_retries: 3 ) end it "should retry the connect three times" do expect(Net::SSH).to receive(:start).and_raise(Errno::EACCES).twice expect(Net::SSH).to receive(:start) communicator.send(:connect) end it "should override from passed options" do expect(Net::SSH).to receive(:start).and_raise(Errno::EACCES).thrice expect(Net::SSH).to receive(:start) communicator.send(:connect, retries: 4) end end context "with connect_retry_delay configured" do let(:retries) { 3 } let(:sleep_delay) { 5 } before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, connect_retries: retries, connect_retry_delay: sleep_delay ) end it "should sleep twice while retrying" do expect(Net::SSH).to receive(:start).and_raise(Errno::EACCES).twice expect(Net::SSH).to receive(:start) expect(communicator).to receive(:sleep).with(sleep_delay).twice communicator.send(:connect) end it "should overrride from passed options" do expect(Net::SSH).to receive(:start).and_raise(Errno::EACCES).twice expect(Net::SSH).to receive(:start) expect(communicator).to receive(:sleep).with(20).twice communicator.send(:connect, retry_delay: 20) end context "when no retries are configured" do let(:retries) { 0 } it "should not sleep" do expect(Net::SSH).to receive(:start).and_raise(Errno::EACCES) expect(communicator).not_to receive(:sleep) expect { communicator.send(:connect) }.to raise_error(Vagrant::Errors::SSHConnectEACCES) end end end end describe "#insecure_key?" do let(:key_data) { "" } let(:key_file) { if !@key_file f = Tempfile.new f.write(key_data) f.close @key_file = f.path end @key_file } after { File.delete(key_file) } context "when using rsa private key" do let(:key_data) { File.read(Vagrant.source_root.join("keys", "vagrant.key.rsa")) } it "should match as insecure key" do expect(communicator.send(:insecure_key?, key_file)).to be_truthy end end context "when using ed25519 private key" do let(:key_data) { File.read(Vagrant.source_root.join("keys", "vagrant.key.ed25519")) } it "should match as insecure key" do expect(communicator.send(:insecure_key?, key_file)).to be_truthy end end context "when using unknown private key" do let(:key_data) { "invalid data" } it "should not match as insecure key" do expect(communicator.send(:insecure_key?, key_file)).to be_falsey end end end describe "#generate_environment_export" do it "should generate bourne shell compatible export" do expect(communicator.send(:generate_environment_export, "TEST", "value")).to eq("export TEST=\"value\"\n") end context "with custom template defined" do let(:export_command_template){ "setenv %ENV_KEY% %ENV_VALUE%" } it "should generate custom export based on template" do expect(communicator.send(:generate_environment_export, "TEST", "value")).to eq("setenv TEST value\n") end end end describe "#supported_key_types" do let(:sudo_result) { 0 } let(:sudo_data) { "" } let(:server_data_error) { VagrantPlugins::CommunicatorSSH::Communicator::ServerDataError } let(:transport) { double("transport", algorithms: algorithms) } let(:algorithms) { double("algorithms") } before do allow(communicator).to receive(:ready?).and_return(true) expect(communicator).to receive(:sudo). with("sshd -T | grep key", any_args). and_yield(:stdout, sudo_data). and_return(sudo_result) # The @connection value is checked to determine if supported key types # can be checked. To facilitate this, set it to a non-nil value communicator.instance_variable_set(:@connection, connection) allow(connection).to receive(:transport).and_return(transport) end it "should raise an error when no data is returned" do expect { communicator.send(:supported_key_types) }.to raise_error(server_data_error) end context "when sudo command is unsuccessful" do let(:sudo_result) { 1 } it "should inspect the net-ssh connection" do expect(algorithms).to receive(:instance_variable_get). with(:@server_data).and_return({}) communicator.send(:supported_key_types) end end context "when data includes pubkeyacceptedalgorithms" do let(:sudo_data) do "pubkeyauthentication yes gssapikeyexchange no gssapistorecredentialsonrekey no trustedusercakeys none revokedkeys none authorizedkeyscommand none authorizedkeyscommanduser none hostkeyagent none hostbasedacceptedkeytypes ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa hostkeyalgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa pubkeyacceptedalgorithms rsa-sha2-512,rsa-sha2-256,ssh-rsa authorizedkeysfile .ssh/authorized_keys hostkey /etc/ssh/ssh_host_rsa_key rekeylimit 0 0" end it "should return expected values" do expect(communicator.send(:supported_key_types)).to eq(["rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"]) end end context "when data includes pubkeyacceptedkeytypes" do let(:sudo_data) do "pubkeyauthentication yes gssapikeyexchange no gssapistorecredentialsonrekey no trustedusercakeys none revokedkeys none authorizedkeyscommand none authorizedkeyscommanduser none hostkeyagent none hostbasedacceptedkeytypes ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa hostkeyalgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa pubkeyacceptedkeytypes rsa-sha2-512,rsa-sha2-256,ssh-rsa authorizedkeysfile .ssh/authorized_keys hostkey /etc/ssh/ssh_host_rsa_key rekeylimit 0 0" end it "should return expected values" do expect(communicator.send(:supported_key_types)). to eq(["rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"]) end end context "when data does not include pubkeyacceptedalgorithms or pubkeyacceptedkeytypes" do let(:sudo_data) do "pubkeyauthentication yes gssapikeyexchange no gssapistorecredentialsonrekey no trustedusercakeys none revokedkeys none authorizedkeyscommand none authorizedkeyscommanduser none hostkeyagent none hostbasedacceptedkeytypes ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa hostkeyalgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa authorizedkeysfile .ssh/authorized_keys hostkey /etc/ssh/ssh_host_rsa_key rekeylimit 0 0" end it "should use hostkeyalgorithms" do expect(communicator.send(:supported_key_types)). to eq(["ssh-ed25519", "rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"]) end end context "when data does not include defined config options" do let(:sudo_data) do "pubkeyauthentication yes gssapikeyexchange no gssapistorecredentialsonrekey no trustedusercakeys none revokedkeys none authorizedkeyscommand none authorizedkeyscommanduser none hostkeyagent none authorizedkeysfile .ssh/authorized_keys hostkey /etc/ssh/ssh_host_rsa_key rekeylimit 0 0" end it "should raise error" do expect { communicator.send(:supported_key_types) }. to raise_error(server_data_error) end end end end ================================================ FILE: test/unit/plugins/communicators/winrm/command_filter_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/communicators/winrm/command_filter") describe VagrantPlugins::CommunicatorWinRM::CommandFilter, unit: true do describe '.command_filters' do it 'initializes all command filters in command filters directory' do expect(subject.command_filters()).not_to be_empty end end describe '.filter' do it 'filters out uname commands' do expect(subject.filter('uname -s stuff')).to eq('') end it 'filters out grep commands' do expect(subject.filter("grep 'Fedora release [12][67890]' /etc/redhat-release")).to eq("") end it 'filters out which commands' do expect(subject.filter('which ruby')).to include( '[Array](Get-Command "ruby" -errorAction SilentlyContinue)') end it 'filters out test -d commands' do expect(subject.filter('test -d /tmp/dir')).to include( "$p = \"/tmp/dir\"") expect(subject.filter('test -d /tmp/dir')).to include( "if ((Test-Path $p) -and (get-item $p).PSIsContainer) {") end it 'filters out test -f commands' do expect(subject.filter('test -f /tmp/file.txt')).to include( "$p = \"/tmp/file.txt\"") expect(subject.filter('test -f /tmp/file.txt')).to include( "if ((Test-Path $p) -and (!(get-item $p).PSIsContainer)) {") end it 'filters out test -x commands' do expect(subject.filter('test -x /tmp/file.txt')).to include( "$p = \"/tmp/file.txt\"") expect(subject.filter('test -x /tmp/file.txt')).to include( "if ((Test-Path $p) -and (!(get-item $p).PSIsContainer)) {") end it 'filters out other test commands' do expect(subject.filter('test -L /tmp/file.txt')).to include( "$p = \"/tmp/file.txt\"") expect(subject.filter('test -L /tmp/file.txt')).to include( "if (Test-Path $p) {") end it 'filters out rm recurse commands' do expect(subject.filter('rm -Rf /some/dir')).to eq( "if (Test-Path \"/some/dir\") {Remove-Item \"/some/dir\" -force -recurse}") expect(subject.filter('rm -fr /some/dir')).to eq( "if (Test-Path \"/some/dir\") {Remove-Item \"/some/dir\" -force -recurse}") expect(subject.filter('rm -r /some/dir')).to eq( "if (Test-Path \"/some/dir\") {Remove-Item \"/some/dir\" -force -recurse}") expect(subject.filter('rm -r "/some/dir"')).to eq( "if (Test-Path \"/some/dir\") {Remove-Item \"/some/dir\" -force -recurse}") end it 'filters out rm commands' do expect(subject.filter('rm /some/dir')).to eq( "if (Test-Path \"/some/dir\") {Remove-Item \"/some/dir\" -force}") expect(subject.filter('rm -f /some/dir')).to eq( "if (Test-Path \"/some/dir\") {Remove-Item \"/some/dir\" -force}") expect(subject.filter('rm -f "/some/dir"')).to eq( "if (Test-Path \"/some/dir\") {Remove-Item \"/some/dir\" -force}") end it 'filters out mkdir commands' do expect(subject.filter('mkdir /some/dir')).to eq( "mkdir \"/some/dir\" -force") expect(subject.filter('mkdir -p /some/dir')).to eq( "mkdir \"/some/dir\" -force") expect(subject.filter('mkdir "/some/dir"')).to eq( "mkdir \"/some/dir\" -force") end it 'filters out chown commands' do expect(subject.filter("chown -R root '/tmp/dir'")).to eq('') end it 'filters out chmod commands' do expect(subject.filter("chmod 0600 ~/.ssh/authorized_keys")).to eq('') end it 'filters out certain cat commands' do expect(subject.filter("cat /etc/release | grep -i OmniOS")).to eq('') end it 'should not filter out other cat commands' do expect(subject.filter("cat /tmp/somefile")).to eq('cat /tmp/somefile') end end end ================================================ FILE: test/unit/plugins/communicators/winrm/communicator_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/communicators/winrm/communicator") describe VagrantPlugins::CommunicatorWinRM::Communicator do include_context "unit" let(:winrm) { double("winrm", timeout: 1, host: nil, port: 5986, guest_port: 5986) } let(:config) { double("config", winrm: winrm) } let(:provider) { double("provider") } let(:ui) { Vagrant::UI::Silent.new } let(:machine) { double("machine", config: config, provider: provider, ui: ui) } let(:shell) { double("shell") } let(:good_output) { WinRM::Output.new.tap { |out| out.exitcode = 0 } } let(:bad_output) { WinRM::Output.new.tap { |out| out.exitcode = 1 } } subject do described_class.new(machine).tap do |comm| allow(comm).to receive(:create_shell).and_return(shell) end end 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 ".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(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(:cmd).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 ".reset!" do it "should create a new shell" do expect(subject).to receive(:shell).with(true) subject.reset! end end describe ".ready?" do it "returns true if hostname command executes without error" do expect(shell).to receive(:cmd).with("hostname").and_return({ exitcode: 0 }) expect(subject.ready?).to be(true) end it "returns false if hostname command fails with a transient error" do expect(shell).to receive(:cmd).with("hostname").and_raise(VagrantPlugins::CommunicatorWinRM::Errors::TransientError) expect(subject.ready?).to be(false) end it "returns false if hostname command fails with a WinRMNotReady error" do expect(shell).to receive(:cmd).with("hostname").and_raise(VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady) expect(subject.ready?).to be(false) end it "raises an error if hostname command fails with an unknown error" do expect(shell).to receive(:cmd).with("hostname").and_raise(Vagrant::Errors::VagrantError) expect { subject.ready? }.to raise_error(Vagrant::Errors::VagrantError) end it "raises timeout error when hostname command takes longer then winrm timeout" do expect(shell).to receive(:cmd).with("hostname") do sleep 2 # winrm.timeout = 1 end expect { subject.ready? }.to raise_error(Timeout::Error) end end describe ".execute" do it "defaults to running in powershell" do expect(shell).to receive(:powershell).with(kind_of(String), kind_of(Hash)).and_return(good_output) expect(subject.execute("dir")).to eq(0) end it "use elevated shell script when elevated is true" do expect(shell).to receive(:elevated).and_return(good_output) expect(subject.execute("dir", { elevated: true })).to eq(0) end it "can use cmd shell" do expect(shell).to receive(:cmd).with(kind_of(String), kind_of(Hash)).and_return(good_output) expect(subject.execute("dir", { shell: :cmd })).to eq(0) end it "raises error when error_check is true and exit code is non-zero" do expect(shell).to receive(:powershell).with(kind_of(String), kind_of(Hash)).and_return(bad_output) expect { subject.execute("dir") }.to raise_error( VagrantPlugins::CommunicatorWinRM::Errors::WinRMBadExitStatus) end it "does not raise error when error_check is false and exit code is non-zero" do expect(shell).to receive(:powershell).with(kind_of(String), kind_of(Hash)).and_return(bad_output) expect(subject.execute("dir", { error_check: false })).to eq(1) end end describe ".test" do it "returns true when exit code is zero" do expect(shell).to receive(:powershell).with(kind_of(String)).and_return(good_output) expect(subject.test("test -d c:/windows")).to be(true) end it "returns false when exit code is non-zero" do expect(shell).to receive(:powershell).with(kind_of(String)).and_return(bad_output) expect(subject.test("test -d /tmp/foobar")).to be(false) end it "returns false when stderr contains output" do output = WinRM::Output.new output.exitcode = 1 output << { stderr: 'this is an error' } expect(shell).to receive(:powershell).with(kind_of(String)).and_return(output) expect(subject.test("[-x stuff] && foo")).to be(false) end it "returns false when command is testing for linux OS" do expect(subject.test("uname -s | grep Debian")).to be(false) end end describe ".upload" do it "calls upload on shell" do expect(shell).to receive(:upload).with("from", "to") subject.upload("from", "to") end end describe ".download" do it "calls download on shell" do expect(shell).to receive(:download).with("from", "to") subject.download("from", "to") end end end ================================================ FILE: test/unit/plugins/communicators/winrm/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/communicators/winrm/config") describe VagrantPlugins::CommunicatorWinRM::Config do let(:machine) { double("machine") } subject { described_class.new } it "is valid by default" do subject.finalize! result = subject.validate(machine) expect(result["WinRM"]).to be_empty end end ================================================ FILE: test/unit/plugins/communicators/winrm/helper_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/communicators/winrm/helper") describe VagrantPlugins::CommunicatorWinRM::Helper do include_context "unit" let(:iso_env) do # We have to create a Vagrantfile so there is a root path test_iso_env.vagrantfile("") test_iso_env.create_vagrant_env end let(:test_iso_env) { isolated_environment } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } subject { described_class } describe ".winrm_address" do before do machine.config.winrm.host = nil end it "returns the configured host if set" do machine.config.winrm.host = "foo" expect(subject.winrm_address(machine)).to eq("foo") end it "returns the SSH info host if available" do allow(machine).to receive(:ssh_info).and_return({ host: "bar" }) expect(subject.winrm_address(machine)).to eq("bar") end it "raise an exception if it can't detect a host" do allow(machine).to receive(:ssh_info).and_return(nil) expect { subject.winrm_address(machine) }. to raise_error(VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady) end it "raise an exception if it detects an empty host ip" do allow(machine).to receive(:ssh_info).and_return({ host: "" }) expect { subject.winrm_address(machine) }. to raise_error(VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady) end it "raise a WinRMNotReady exception if it detects an unset host ip" do allow(machine).to receive(:ssh_info).and_return({ host: nil }) expect { subject.winrm_address(machine) }. to raise_error(VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady) end it "raise an exception if it detects an APIPA" do allow(machine).to receive(:ssh_info).and_return({ host: "169.254.123.123" }) expect { subject.winrm_address(machine) }. to raise_error(VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady) end end describe ".winrm_info" do before do allow(machine.provider).to receive(:capability?) .with(:winrm_info).and_return(false) allow(subject).to receive(:winrm_address).and_return(nil) allow(subject).to receive(:winrm_port).and_return(nil) end it "returns default info if no capability" do allow(subject).to receive(:winrm_address).and_return("bar") allow(subject).to receive(:winrm_port).and_return(45) result = subject.winrm_info(machine) expect(result[:host]).to eq("bar") expect(result[:port]).to eq(45) end it "raises an exception if capability returns nil" do allow(machine.provider).to receive(:capability?) .with(:winrm_info).and_return(true) allow(machine.provider).to receive(:capability) .with(:winrm_info).and_return(nil) expect { subject.winrm_info(machine) }. to raise_error(VagrantPlugins::CommunicatorWinRM::Errors::WinRMNotReady) end it "returns the proper information if set" do allow(machine.provider).to receive(:capability?) .with(:winrm_info).and_return(true) allow(machine.provider).to receive(:capability).with(:winrm_info).and_return({ host: "foo", port: 12, }) result = subject.winrm_info(machine) expect(result[:host]).to eq("foo") expect(result[:port]).to eq(12) end it "defaults information if capability doesn't set it" do allow(machine.provider).to receive(:capability?) .with(:winrm_info).and_return(true) allow(machine.provider).to receive(:capability).with(:winrm_info).and_return({}) allow(subject).to receive(:winrm_address).and_return("bar") allow(subject).to receive(:winrm_port).and_return(45) result = subject.winrm_info(machine) expect(result[:host]).to eq("bar") expect(result[:port]).to eq(45) end end describe ".winrm_port" do it "returns the configured port if no guest port set" do machine.config.winrm.port = 22 machine.config.winrm.guest_port = nil expect(subject.winrm_port(machine)).to eq(22) end it "returns the configured guest port if non local" do machine.config.winrm.port = 22 machine.config.winrm.guest_port = 2222 machine.config.vm.network "forwarded_port", host: 1234, guest: 2222 expect(subject.winrm_port(machine, false)).to eq(2222) end it "returns a forwarded port that matches the guest port" do machine.config.winrm.port = 22 machine.config.winrm.guest_port = 2222 machine.config.vm.network "forwarded_port", host: 1234, guest: 2222 expect(subject.winrm_port(machine)).to eq(1234) end it "uses the provider capability if it exists" do machine.config.winrm.port = 22 machine.config.winrm.guest_port = 2222 machine.config.vm.network "forwarded_port", host: 1234, guest: 2222 allow(machine.provider).to receive(:capability?).with(:forwarded_ports).and_return(true) allow(machine.provider).to receive(:capability).with(:forwarded_ports).and_return({ 1234 => 4567, 2456 => 2222, }) expect(subject.winrm_port(machine)).to eq(2456) end it "does not error when the provider capability returns nil result" do machine.config.winrm.port = 22 machine.config.winrm.guest_port = 2222 machine.config.vm.network "forwarded_port", host: 1234, guest: 2222 allow(machine.provider).to receive(:capability).with(:forwarded_ports).and_return(nil) expect(subject.winrm_port(machine)).to eq(1234) end end describe ".winrm_info_invalid?" do it "returns true if it can't detect a host" do allow(machine).to receive(:ssh_info).and_return(nil) expect(subject).to be_winrm_info_invalid(machine.ssh_info) end it "returns true if it detects an empty host ip" do allow(machine).to receive(:ssh_info).and_return({ host: "" }) expect(subject).to be_winrm_info_invalid(machine.ssh_info) end it "returns true if it detects an unset host ip" do allow(machine).to receive(:ssh_info).and_return({ host: nil }) expect(subject).to be_winrm_info_invalid(machine.ssh_info) end it "returns true if it detects an APIPA" do allow(machine).to receive(:ssh_info).and_return({ host: "169.254.123.123" }) expect(subject).to be_winrm_info_invalid(machine.ssh_info) end it "returns false if the IP is valid" do allow(machine).to receive(:ssh_info).and_return({ host: "192.168.123.123" }) expect(subject).not_to be_winrm_info_invalid(machine.ssh_info) end end end ================================================ FILE: test/unit/plugins/communicators/winrm/plugin_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/communicators/winrm/plugin") describe VagrantPlugins::CommunicatorWinRM::Plugin do describe "#init!" do let(:manager_instance) { double("manager_instance", installed_plugins: installed_plugins) } let(:installed_plugins) { {} } before do allow(I18n).to receive(:load_path).and_return("") allow(I18n).to receive(:reload!) allow(described_class).to receive(:require) allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager_instance) end after do described_class.init! described_class.reset! end it "should not output any warning" do expect($stderr).not_to receive(:puts).with(/WARNING/) end context "when vagrant-winrm plugin is installed" do let(:installed_plugins) { {"vagrant-winrm" => "PLUGIN_INFO"} } it "should output a warning" do expect($stderr).to receive(:puts).with(/WARNING/) end context "with VAGRANT_IGNORE_WINRM_PLUGIN set" do before { allow(ENV).to receive(:[]).with("VAGRANT_IGNORE_WINRM_PLUGIN").and_return("1") } it "should not output any warning" do expect($stderr).not_to receive(:puts).with(/WARNING/) end end end end end ================================================ FILE: test/unit/plugins/communicators/winrm/shell_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/communicators/winrm/shell") require Vagrant.source_root.join("plugins/communicators/winrm/config") describe VagrantPlugins::CommunicatorWinRM::WinRMShell do include_context "unit" let(:connection) { double("winrm_connection") } let(:shell) { double("winrm_shell") } let(:port) { config.transport == :ssl ? 5986 : 5985 } let(:config) { VagrantPlugins::CommunicatorWinRM::Config.new.tap do |c| c.username = 'username' c.password = 'password' c.max_tries = 3 c.retry_delay = 0 c.basic_auth_only = false c.retry_delay = 1 c.max_tries = 2 c.finalize! end } let(:output) { WinRM::Output.new.tap { |out| out.exitcode = 0 } } before { allow(connection).to receive(:shell).and_yield(shell) } subject do described_class.new('localhost', port, config).tap do |comm| allow(comm).to receive(:new_connection).and_return(connection) end end describe "#upload" do let(:fm) { double("file_manager") } before do allow(WinRM::FS::FileManager).to receive(:new).with(connection) .and_return(fm) end it "should call file_manager.upload for each passed in path" do from = ["/path", "/path/folder", "/path/folder/file.py"] to = "/destination" size = 80 allow(fm).to receive(:upload).and_return(size) expect(fm).to receive(:upload).exactly(from.size).times expect(subject.upload(from, to)).to eq(size*from.size) end it "should call file_manager.upload once for a single path" do from = "/path/folder/file.py" to = "/destination" size = 80 allow(fm).to receive(:upload).and_return(size) expect(fm).to receive(:upload).exactly(1).times expect(subject.upload(from, to)).to eq(size) end context "when source is a directory" do let(:source) { "path/sourcedir" } before do allow(File).to receive(:directory?).with(/#{Regexp.escape(source)}/).and_return(true) end it "should add source directory name to destination" do expect(fm).to receive(:upload) do |from, to| expect(to).to include("sourcedir") end subject.upload(source, "/dest") end it "should not add source directory name to destination when source ends with '.'" do source << "/." expect(fm).to receive(:upload) do |from, to| expect(to).to eq("/dest") end subject.upload(source, "/dest") end end end describe ".powershell" do it "should call winrm powershell" do expect(shell).to receive(:run).with("dir").and_return(output) expect(subject.powershell("dir").exitcode).to eq(0) end it "should raise an execution error when an exception occurs" do expect(shell).to receive(:run).with("dir").and_raise( StandardError.new("Oh no! a 500 SOAP error!")) expect { subject.powershell("dir") }.to raise_error( VagrantPlugins::CommunicatorWinRM::Errors::ExecutionError) end end describe ".elevated" do let(:eusername) { double("elevatedusername") } let(:username) { double("username") } let(:failed_output) { WinRM::Output.new.tap { |out| out.exitcode = described_class.const_get(:INVALID_USERID_EXITCODE) out << {stderr: "(10,8):UserId:"} out << {stderr: "At line:72 char:1"} } } before do allow(subject).to receive(:elevated_username).and_return(eusername) allow(shell).to receive(:username).and_return(username) allow(shell).to receive(:username=) end it "should call winrm elevated" do expect(shell).to receive(:run).with("dir").and_return(output) expect(shell).to receive(:interactive_logon=).with(false) expect(subject.elevated("dir").exitcode).to eq(0) end it "should set interactive_logon when interactive is true" do expect(shell).to receive(:run).with("dir").and_return(output) expect(shell).to receive(:interactive_logon=).with(true) expect(subject.elevated("dir", { interactive: true }).exitcode).to eq(0) end it "should raise an execution error when an exception occurs" do expect(shell).to receive(:run).with("dir").and_raise( StandardError.new("Oh no! a 500 SOAP error!")) expect { subject.powershell("dir") }.to raise_error( VagrantPlugins::CommunicatorWinRM::Errors::ExecutionError) end it "should use elevated username and retry on username failure" do expect(subject).to receive(:elevated_username).and_return(eusername) expect(shell).to receive(:run).with("dir").and_return(failed_output) expect(shell).to receive(:run).with("dir").and_return(output) expect(shell).to receive(:interactive_logon=).with(false) expect(subject.elevated("dir").exitcode).to eq(0) end it "should not retry on username failure if elevated username is the same" do expect(subject).to receive(:elevated_username).and_return(username) expect(shell).to receive(:run).with("dir").and_return(failed_output) expect(shell).to receive(:interactive_logon=).with(false) expect(subject.elevated("dir").exitcode).to eq(failed_output.exitcode) end end describe ".cmd" do it "should call winrm cmd" do expect(connection).to receive(:shell).with(:cmd, { }) expect(shell).to receive(:run).with("dir").and_return(output) expect(subject.cmd("dir").exitcode).to eq(0) end context "when codepage is given" do let(:config) { VagrantPlugins::CommunicatorWinRM::Config.new.tap do |c| c.codepage = 800 c.finalize! end } it "creates shell with the given codepage when set" do expect(connection).to receive(:shell).with(:cmd, { codepage: 800 }) expect(shell).to receive(:run).with("dir").and_return(output) expect(subject.cmd("dir").exitcode).to eq(0) end end it "should catch timeout errors" do expect(connection).to receive(:shell).with(:cmd, { }) expect(shell).to receive(:run).with("hostname").and_raise(IO::TimeoutError) expect { subject.cmd("hostname") }.to raise_error(VagrantPlugins::CommunicatorWinRM::Errors::ConnectionTimeout) end end describe ".wql" do it "should call winrm wql" do expect(connection).to receive(:run_wql).with("select * from Win32_OperatingSystem") subject.wql("select * from Win32_OperatingSystem") end it "should retry when a WinRMAuthorizationError is received" do expect(connection).to receive(:run_wql).with("select * from Win32_OperatingSystem").exactly(2).times.and_raise( # Note: The initialize for WinRMAuthorizationError may require a status_code as # the second argument in a future WinRM release. Currently it doesn't track the # status code. WinRM::WinRMAuthorizationError.new("Oh no!! Unauthorized") ) expect { subject.wql("select * from Win32_OperatingSystem") }.to raise_error( VagrantPlugins::CommunicatorWinRM::Errors::AuthenticationFailed) end end describe ".endpoint" do context 'when transport is :ssl' do let(:config) { VagrantPlugins::CommunicatorWinRM::Config.new.tap do |c| c.transport = :ssl c.finalize! end } it "should create winrm endpoint address using https" do expect(subject.send(:endpoint)).to eq("https://localhost:5986/wsman") end end context "when transport is :negotiate" do it "should create winrm endpoint address using http" do expect(subject.send(:endpoint)).to eq("http://localhost:5985/wsman") end end context "when transport is :plaintext" do let(:config) { VagrantPlugins::CommunicatorWinRM::Config.new.tap do |c| c.transport = :plaintext c.finalize! end } it "should create winrm endpoint address using http" do expect(subject.send(:endpoint)).to eq("http://localhost:5985/wsman") end end end describe ".endpoint_options" do it "should create endpoint options" do expect(subject.send(:endpoint_options)).to eq( { endpoint: "http://localhost:5985/wsman", operation_timeout: 1800, user: "username", password: "password", host: "localhost", port: 5985, basic_auth_only: false, no_ssl_peer_verification: false, retry_delay: 1, retry_limit: 2, transport: :negotiate }) end end describe "#elevated_username" do let(:username) { "username" } before do allow(subject).to receive(:username).and_return(username) allow(subject).to receive(:powershell) end it "should return username" do expect(subject.send(:elevated_username)).to eq(username) end it "should attempt to get computer name" do expect(subject).to receive(:powershell).with(/computername/) subject.send(:elevated_username) end it "should prepend computer name when available" do expect(subject).to receive(:powershell).with(/computername/).and_yield(:stdout, "COMPUTERNAME") expect(subject.send(:elevated_username)).to eq("COMPUTERNAME\\#{username}") end it "should compute elevated username every time" do expect(subject).to receive(:powershell).twice.with(/computername/).and_yield(:stdout, "COMPUTERNAME") expect(subject.send(:elevated_username)).to eq("COMPUTERNAME\\#{username}") expect(subject.send(:elevated_username)).to eq("COMPUTERNAME\\#{username}") end context "when username includes computer/domain name" do let(:username) { "machine\\username" } it "should not attempt to get computer name" do expect(subject).not_to receive(:powershell) expect(subject.send(:elevated_username)).to eq(username) end end end end ================================================ FILE: test/unit/plugins/communicators/winssh/communicator_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/communicators/winssh/communicator") require Vagrant.source_root.join("plugins/communicators/winssh/config") describe VagrantPlugins::CommunicatorWinSSH::Communicator do include_context "unit" let(:export_command_template){ 'export %ENV_KEY%="%ENV_VALUE%"' } let(:ssh) do double("ssh", timeout: 1, host: nil, port: 5986, guest_port: 5986, keep_alive: false ) end let(:shell) { "cmd" } # SSH configuration information mock let(:winssh) do double("winssh", insert_key: false, export_command_template: export_command_template, shell: shell, upload_directory: "C:\\Windows\\Temp" ) end # Configuration mock let(:config) { double("config", winssh: winssh, ssh: ssh) } # Provider mock let(:provider) { double("provider") } let(:ui) { Vagrant::UI::Silent.new } # SSH info mock let(:ssh_info) { double("ssh_info") } # Machine mock built with previously defined let(:machine) do double("machine", config: config, provider: provider, ui: ui, ssh_info: ssh_info ) end # Subject instance to test let(:communicator){ @communicator ||= described_class.new(machine) } # Underlying net-ssh connection mock let(:connection) { double("connection", open_channel: nil) } # Base net-ssh connection channel mock let(:channel) { double("channel") } # net-ssh connection channel mock for running commands let(:command_channel) { double("command_channel") } # Default exit data for commands run let(:exit_data) { double("exit_data", read_long: 0) } # Marker used for flagging start of output let(:command_garbage_marker) { communicator.class.const_get(:CMD_GARBAGE_MARKER) } # Start marker output when PTY is enabled let(:pty_delim_start) { communicator.class.const_get(:PTY_DELIM_START) } # End marker output when PTY is enabled let(:pty_delim_end) { communicator.class.const_get(:PTY_DELIM_END) } # Command output returned on stdout let(:command_stdout_data) { '' } # Command output returned on stderr let(:command_stderr_data) { '' } # Mock for net-ssh sftp let(:sftp) { double("sftp") } # Prevent connection patching by default in tests let(:winssh_patch) { true } # Setup for commands using the net-ssh connection. This can be reused where needed # by providing to `before` connection_setup = proc do connection.instance_variable_set(:@winssh_patched, winssh_patch) allow(connection).to receive(:logger) allow(connection).to receive(:closed?).and_return(false) allow(connection).to receive(:open_channel). and_yield(channel).and_return(channel) allow(channel).to receive(:wait).and_return(true) allow(channel).to receive(:close) allow(command_channel).to receive(:send_data) allow(command_channel).to receive(:eof!) allow(command_channel).to receive(:on_data). and_yield(nil, command_stdout_data) allow(command_channel).to receive(:on_extended_data). and_yield(nil, nil, command_stderr_data) allow(machine).to receive(:ssh_info).and_return(host: '10.1.2.3', port: 22) allow(channel).to receive(:[]=).with(any_args).and_return(true) allow(channel).to receive(:on_close) allow(channel).to receive(:on_data) allow(channel).to receive(:on_extended_data) allow(channel).to receive(:on_request) allow(channel).to receive(:on_process) allow(channel).to receive(:exec).with(anything). and_yield(command_channel, '').and_return(channel) allow(command_channel).to receive(:on_request).with('exit-status'). and_yield(nil, exit_data) # Return mocked net-ssh connection during setup allow(communicator).to receive(:retryable).and_return(connection) allow(sftp).to receive(:upload!) allow(communicator).to receive(:sftp_connect).and_return(true) allow(communicator).to receive(:execute).and_call_original allow(communicator).to receive(:execute). with(described_class.const_get(:READY_COMMAND), error_check: false). and_return(0) end describe "#wait_for_ready" do before(&connection_setup) context "with no static config (default scenario)" do context "when ssh_info requires a multiple tries before it is ready" do before do expect(machine).to receive(:ssh_info). and_return(nil).ordered expect(machine).to receive(:ssh_info). and_return(host: '10.1.2.3', port: 22).ordered end it "retries ssh_info until ready" do # retries are every 0.5 so buffer the timeout just a hair over expect(communicator.wait_for_ready(0.6)).to eq(true) end end end end describe "#ready?" do before(&connection_setup) it "returns true if shell test is successful" do expect(communicator.ready?).to be_truthy end context "with an invalid shell test" do before do allow(communicator).to receive(:execute). with(described_class.const_get(:READY_COMMAND), error_check: false). and_return(1) end it "returns raises SSHInvalidShell error" do expect{ communicator.ready? }.to raise_error(Vagrant::Errors::SSHInvalidShell) end end end describe "#execute" do before(&connection_setup) it "runs valid command and returns successful status code" do expect(communicator.execute("command-to-run", error_check: false)).to eq(0) end it "prepends UUID output to command for garbage removal" do expect(channel).to receive(:exec). with(/Write-Output #{command_garbage_marker};\[Console\]::Error.WriteLine\('#{command_garbage_marker}'\).*/) expect(communicator.execute("command-to-run")).to eq(0) end context "with command returning an error" do let(:exit_data) { double("exit_data", read_long: 1) } it "raises error when exit-code is non-zero" do expect{ communicator.execute("command-to-run") }.to raise_error(Vagrant::Errors::VagrantError) end it "returns exit-code when exit-code is non-zero and error check is disabled" do expect(communicator.execute("command-to-run", error_check: false)).to eq(1) end end context "with garbage content prepended to command output" do let(:command_stdout_data) do "Line of garbage\nMore garbage\n#{command_garbage_marker}Dir1\nDir2\n" end it "removes any garbage output prepended to command output" do stdout = '' expect( communicator.execute("command-to-run") do |type, data| if type == :stdout stdout << data end end ).to eq(0) expect(stdout).to eq("Dir1\nDir2\n") end end context "with garbage content prepended to command stderr output" do let(:command_stderr_data) do "Line of garbage\nMore garbage\n#{command_garbage_marker}Dir1\nDir2\n" end it "removes any garbage output prepended to command stderr output" do stderr = '' expect( communicator.execute("command-to-run") do |type, data| if type == :stderr stderr << data end end ).to eq(0) expect(stderr).to eq("Dir1\nDir2\n") end end end describe "#test" do before(&connection_setup) context "with exit code as zero" do it "returns true" do expect(communicator.test("dir")).to be_truthy end end context "with exit code as non-zero" do before do expect(exit_data).to receive(:read_long).and_return(1) end it "returns false" do expect(communicator.test("false.exe")).to be_falsey end end end describe "#upload" do before do allow(sftp).to receive(:upload) expect(communicator).to receive(:sftp_connect).and_yield(sftp) end it "uploads a directory if local path is a directory" do Dir.mktmpdir('vagrant-test') do |dir| FileUtils.touch(File.join(dir, "test-file")) expect(sftp).to receive(:mkdir).with(/destination/).exactly(2).times expect(sftp).to receive(:upload!).with(an_instance_of(File), /test-file/) communicator.upload(dir, 'C:\destination') end end it "uploads a file if local path is a file" do file = Tempfile.new('vagrant-test') begin expect(sftp).to receive(:mkdir).with(/destination/) expect(sftp).to receive(:upload!).with(instance_of(File), 'C:/destination/file') expect(Vagrant::Util::Platform).to receive(:unix_windows_path).with('C:\destination\file'). and_call_original communicator.upload(file.path, 'C:\destination\file') ensure file.delete end end it "does not raise custom error on non-permission errors" do file = Tempfile.new('vagrant-test') begin expect(sftp).to receive(:mkdir).with(/destination/) expect(sftp).to receive(:upload!).with(instance_of(File), 'C:/destination/file'). and_raise("Some other error") expect{ communicator.upload(file.path, 'C:\destination\file') }.to raise_error(RuntimeError) ensure file.delete end end end describe "#download" do before do expect(communicator).to receive(:sftp_connect).and_yield(sftp) end it "calls sftp to download file" do expect(sftp).to receive(:download!).with('/path/from', 'C:\path\to') communicator.download('/path/from', 'C:\path\to') end end describe "#connect" do it "cannot be called directly" do expect{ communicator.connect }.to raise_error(NoMethodError) end context "with default configuration" do before do expect(machine).to receive(:ssh_info).and_return( host: nil, port: nil, private_key_path: nil, username: nil, password: nil, keys_only: true, verify_host_key: false ) end it "has keys_only enabled" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( keys_only: true ) ).and_return(connection) communicator.send(:connect) end it "has verify_host_key disabled" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( verify_host_key: false ) ).and_return(connection) communicator.send(:connect) end it "does not include any private key paths" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_excluding( keys: anything ) ).and_return(connection) communicator.send(:connect) end it "includes `none` and `hostbased` auth methods" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( auth_methods: ["none", "hostbased"] ) ).and_return(connection) communicator.send(:connect) end end context "with keys_only disabled and verify_host_key enabled" do before do expect(machine).to receive(:ssh_info).and_return( host: nil, port: nil, private_key_path: nil, username: nil, password: nil, keys_only: false, verify_host_key: true ) end it "has keys_only enabled" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( keys_only: false ) ).and_return(connection) communicator.send(:connect) end it "has verify_host_key disabled" do expect(Net::SSH).to receive(:start).with( nil, nil, hash_including( verify_host_key: true ) ).and_return(connection) communicator.send(:connect) end end context "with host and port configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, private_key_path: nil, username: nil, password: nil, keys_only: true, verify_host_key: false ) end it "specifies configured host" do expect(Net::SSH).to receive(:start).with("127.0.0.1", anything, anything). and_return(connection) communicator.send(:connect) end it "has port defined" do expect(Net::SSH).to receive(:start).with("127.0.0.1", anything, hash_including(port: 2222)). and_return(connection) communicator.send(:connect) end end context "with private_key_path configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, private_key_path: ['/priv/key/path'], username: nil, password: nil, keys_only: true, verify_host_key: false ) end it "includes private key paths" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( keys: ["/priv/key/path"] ) ).and_return(connection) communicator.send(:connect) end it "includes `publickey` auth method" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( auth_methods: ["none", "hostbased", "publickey"] ) ).and_return(connection) communicator.send(:connect) end end context "with username and password configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, private_key_path: nil, username: 'vagrant', password: 'vagrant', keys_only: true, verify_host_key: false ) end it "has username defined" do expect(Net::SSH).to receive(:start).with(anything, 'vagrant', anything). and_return(connection) communicator.send(:connect) end it "has password defined" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( password: 'vagrant' ) ).and_return(connection) communicator.send(:connect) end it "includes `password` auth method" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( auth_methods: ["none", "hostbased", "password"] ) ).and_return(connection) communicator.send(:connect) end end context "with password and private_key_path configured" do before do expect(machine).to receive(:ssh_info).and_return( host: '127.0.0.1', port: 2222, private_key_path: ['/priv/key/path'], username: 'vagrant', password: 'vagrant', keys_only: true, verify_host_key: false ) end it "has password defined" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( password: 'vagrant' ) ).and_return(connection) communicator.send(:connect) end it "includes private key paths" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( keys: ["/priv/key/path"] ) ).and_return(connection) communicator.send(:connect) end it "includes `publickey` and `password` auth methods" do expect(Net::SSH).to receive(:start).with( anything, anything, hash_including( auth_methods: ["none", "hostbased", "publickey", "password"] ) ).and_return(connection) communicator.send(:connect) end end context "when not patched for winssh" do let(:winssh_patch) { false } before(&connection_setup) it "should patch the connection instance on first request" do expect(connection).to receive(:define_singleton_method) communicator.send(:connect) end it "should force powershell on exec" do expect(channel).to receive(:exec).with(/powershell/).and_return(channel) communicator.execute("test", error_check: false) end end end describe "#generate_environment_export" do let(:winssh) do @c ||= VagrantPlugins::CommunicatorWinSSH::Config.new @c.finalize! @c end it "should generate bourne shell compatible export" do expect(communicator.send(:generate_environment_export, "TEST", "value")).to eq("$env:TEST=\"value\"\n") end context "with custom template defined" do let(:winssh) do @c ||= VagrantPlugins::CommunicatorWinSSH::Config.new @c.export_command_template = "setenv %ENV_KEY% %ENV_VALUE%" @c.finalize! @c end it "should generate custom export based on template" do expect(communicator.send(:generate_environment_export, "TEST", "value")).to eq("setenv TEST value\n") end end end end ================================================ FILE: test/unit/plugins/guests/alma/cap/flavor_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestAlma::Cap::Flavor" do let(:caps) do VagrantPlugins::GuestAlma::Plugin .components .guest_capabilities[:alma] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".flavor" do let(:cap) { caps.get(:flavor) } { "" => :alma, "8.2" => :alma_8, "9" => :alma_9, "invalid" => :alma }.each do |str, expected| it "returns #{expected} for #{str}" do comm.stub_command("source /etc/os-release && printf $VERSION_ID", stdout: str) expect(cap.flavor(machine)).to be(expected) end end end end ================================================ FILE: test/unit/plugins/guests/alpine/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe 'VagrantPlugins::GuestAlpine::Cap::ChangeHostname' do let(:described_class) do VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:change_host_name) end let(:machine) { double('machine') } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:old_hostname) { 'oldhostname.olddomain.tld' } let(:networks) {[ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ]} before do allow(machine).to receive(:communicate).and_return(communicator) communicator.stub_command('hostname -f', stdout: old_hostname) allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end after do communicator.verify_expectations! end describe '.change_host_name' do it 'updates /etc/hostname on the machine' do communicator.expect_command("echo 'newhostname' > /etc/hostname") described_class.change_host_name(machine, 'newhostname.newdomain.tld') end it 'only tries to update /etc/hosts when the provided hostname is not different' do described_class.change_host_name(machine, 'oldhostname.olddomain.tld') expect(communicator.received_commands[0]).to eq('hostname -f') expect(communicator.received_commands.length).to eq(2) end it 'refreshes the hostname service with the hostname command' do communicator.expect_command('hostname -F /etc/hostname') described_class.change_host_name(machine, 'newhostname.newdomain.tld') end it 'renews dhcp on the system with the new hostname' do communicator.expect_command('ifdown -a; ifup -a; ifup eth0') described_class.change_host_name(machine, 'newhostname.newdomain.tld') end describe 'flipping out the old hostname in /etc/hosts' do context "minimal network config" do it "sets the hostname" do described_class.change_host_name(machine, 'newhostname.newdomain.tld') add_to_loopback_cmd = communicator.received_commands.find { |c| c =~ /127.0.\$\{i\}.1/ } expect(add_to_loopback_cmd).to_not eq(nil) end end context "multiple networks configured with hostname" do let(:networks) {[ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :hostname=>true, :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ]} it "sets the hostname" do described_class.change_host_name(machine, 'newhostname.newdomain.tld') add_to_loopback_cmd = communicator.received_commands.find { |c| c =~ /sed -i '\/newhostname.newdomain.tld\/d'/ } expect(add_to_loopback_cmd).to_not eq(nil) end end end end end ================================================ FILE: test/unit/plugins/guests/alpine/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe 'VagrantPlugins::GuestAlpine::Cap::ConfigureNetworks' do let(:described_class) do VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:configure_networks) end let(:machine) { double('machine') } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(communicator) end after do communicator.verify_expectations! end it 'should configure networks' do networks = [ { type: :static, ip: '192.168.10.10', netmask: '255.255.255.0', interface: 0, name: 'eth0' }, { type: :dhcp, interface: 1, name: 'eth1' } ] expect(communicator).to receive(:sudo).with("sed -e '/^#VAGRANT-BEGIN/,$ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces.pre") expect(communicator).to receive(:sudo).with("sed -ne '/^#VAGRANT-END/,$ p' /etc/network/interfaces | tail -n +2 > /tmp/vagrant-network-interfaces.post") expect(communicator).to receive(:sudo).with(/\/sbin\/ifdown eth0/) expect(communicator).to receive(:sudo).with('/sbin/ip addr flush dev eth0 2> /dev/null') expect(communicator).to receive(:sudo).with(/\/sbin\/ifdown eth1/) expect(communicator).to receive(:sudo).with('/sbin/ip addr flush dev eth1 2> /dev/null') expect(communicator).to receive(:sudo).with('cat /tmp/vagrant-network-interfaces.pre /tmp/vagrant-network-entry /tmp/vagrant-network-interfaces.post > /etc/network/interfaces') expect(communicator).to receive(:sudo).with('rm -f /tmp/vagrant-network-interfaces.pre /tmp/vagrant-network-entry /tmp/vagrant-network-interfaces.post') expect(communicator).to receive(:sudo).with('/sbin/ifup eth0') expect(communicator).to receive(:sudo).with('/sbin/ifup eth1') allow_message_expectations_on_nil described_class.configure_networks(machine, networks) end context "dhcp assigned default route" do let(:networks) { [{type: :dhcp, use_dhcp_assigned_default_route: is_enabled}] } let(:is_enabled) { false } let(:tempfile) { double(:tempfile, binmode: true, close: true, path: "/dev/null") } before do allow(Tempfile).to receive(:new).and_return(tempfile) end context "when not enabled" do it "should not configure default route" do expect(tempfile).not_to receive(:write).with(/post-up route del default dev eth0/) described_class.configure_networks(machine, networks) end end context "when enabled" do let(:is_enabled) { true } it "should configure default route" do expect(tempfile).to receive(:write).with(/post-up route del default dev eth0/) described_class.configure_networks(machine, networks) end end end end ================================================ FILE: test/unit/plugins/guests/alpine/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe 'VagrantPlugins::GuestAlpine::Cap::Halt' do let(:described_class) do VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:halt) end let(:machine) { double('machine') } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(communicator) end after do communicator.verify_expectations! end it 'should halt guest' do expect(communicator).to receive(:sudo).with('poweroff') allow_message_expectations_on_nil described_class.halt(machine) end end ================================================ FILE: test/unit/plugins/guests/alpine/cap/nfs_client_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe 'VagrantPlugins::GuestAlpine::Cap::NFSClient' do let(:described_class) do VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:nfs_client_install) end let(:machine) { double('machine') } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(communicator) end after do communicator.verify_expectations! end it 'should install nfs client' do described_class.nfs_client_install(machine) expect(communicator.received_commands[0]).to match(/apk update/) expect(communicator.received_commands[1]).to match(/apk add --upgrade nfs-utils/) expect(communicator.received_commands[2]).to match(/rc-update add rpc.statd/) expect(communicator.received_commands[3]).to match(/rc-service rpc.statd start/) end end ================================================ FILE: test/unit/plugins/guests/alpine/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe 'VagrantPlugins::GuestAlpine::Cap::RSync' do let(:machine) { double('machine') } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(communicator) end after do communicator.verify_expectations! end let(:described_class) do VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:rsync_install) end it 'should install rsync with --update-cache flag' do # communicator.should_receive(:sudo).with('apk add rsync') expect(communicator).to receive(:sudo).with('apk add --update-cache rsync') allow_message_expectations_on_nil described_class.rsync_install(machine) end let(:described_class) do VagrantPlugins::GuestAlpine::Plugin.components.guest_capabilities[:alpine].get(:rsync_installed) end it 'should verify rsync installed' do # communicator.should_receive(:test).with('test -f /usr/bin/rsync') expect(communicator).to receive(:test).with('test -f /usr/bin/rsync') allow_message_expectations_on_nil described_class.rsync_installed(machine) end end ================================================ FILE: test/unit/plugins/guests/alpine/plugin_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe VagrantPlugins::GuestAlpine::Plugin do let(:manager) { double("manager") } before do allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(manager) end context "when vagrant-alpine plugin is not installed" do before do allow(manager).to receive(:installed_plugins).and_return({}) end it "should not display a warning" do expect($stderr).to_not receive(:puts) VagrantPlugins::GuestAlpine::Plugin.check_community_plugin end end context "when vagrant-alpine plugin is installed" do before do allow(manager).to receive(:installed_plugins).and_return({ "vagrant-alpine" => {} }) end it "should display a warning" do expect($stderr).to receive(:puts).with(/vagrant plugin uninstall vagrant-alpine/) VagrantPlugins::GuestAlpine::Plugin.check_community_plugin end end end ================================================ FILE: test/unit/plugins/guests/alt/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestALT::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestALT::Plugin .components .guest_capabilities[:alt] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostnamectl set-hostname --static '#{name}'/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/hostnamectl set-hostname --static '#{name}'/) end end end end ================================================ FILE: test/unit/plugins/guests/alt/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestALT::Cap::ConfigureNetworks" do let(:caps) do VagrantPlugins::GuestALT::Plugin .components .guest_capabilities[:alt] end let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config", vm: vm) } let(:guest) { double("guest") } let(:machine) { double("machine", guest: guest, config: config) } let(:networks){ [[:public_network, network_1], [:private_network, network_2]] } let(:vm){ double("vm", networks: networks) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } before do allow(guest).to receive(:capability) .with(:flavor) .and_return(:alt) allow(guest).to receive(:capability) .with(:network_scripts_dir) .and_return("/etc/net") allow(guest).to receive(:capability) .with(:network_interfaces) .and_return(["eth1", "eth2"]) end let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end context "with NetworkManager installed" do let(:net1_nm_controlled) { true } let(:net2_nm_controlled) { true } let(:networks){ [ [:public_network, network_1.merge(nm_controlled: net1_nm_controlled)], [:private_network, network_2.merge(nm_controlled: net2_nm_controlled)] ] } before do allow(cap).to receive(:nmcli?).and_return true end context "with devices managed by NetworkManager" do before do allow(cap).to receive(:nm_controlled?).and_return true end context "with nm_controlled option omitted" do let(:networks){ [ [:public_network, network_1], [:private_network, network_2] ] } it "downs networks via nmcli, creates ifaces and restart NetworksManager" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/nmcli.*disconnect/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/NetworkManager/) expect(comm.received_commands[0]).to_not match(/ifdown|ifup/) end end context "with nm_controlled option set to true" do it "downs networks via nmcli, creates ifaces and restart NetworksManager" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/nmcli.*disconnect/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/NetworkManager/) expect(comm.received_commands[0]).to_not match(/(ifdown|ifup)/) end end context "with nm_controlled option set to false" do let(:net1_nm_controlled) { false } let(:net2_nm_controlled) { false } it "downs networks manually, creates ifaces, starts networks manually and restart NetworksManager" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to match(/NetworkManager/) expect(comm.received_commands[0]).to_not match(/nmcli/) end end context "with nm_controlled option set to false on first device" do let(:net1_nm_controlled) { false } let(:net2_nm_controlled) { true } it "downs networks, creates ifaces and starts the networks with one managed manually and one NetworkManager controlled" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/nmcli.*disconnect/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to match(/NetworkManager/) end end end context "with devices not managed by NetworkManager" do before do allow(cap).to receive(:nm_controlled?).and_return false end context "with nm_controlled option omitted" do let(:networks){ [ [:public_network, network_1], [:private_network, network_2] ] } it "creates and starts the networks manually" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to match(/NetworkManager/) expect(comm.received_commands[0]).to_not match(/nmcli/) end end context "with nm_controlled option set to true" do let(:net1_nm_controlled) { true } let(:net2_nm_controlled) { true } it "creates and starts the networks via nmcli" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/NetworkManager/) expect(comm.received_commands[0]).to_not match(/ifup/) end end context "with nm_controlled option set to false" do let(:net1_nm_controlled) { false } let(:net2_nm_controlled) { false } it "creates and starts the networks via ifup " do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to match(/NetworkManager/) expect(comm.received_commands[0]).to_not match(/nmcli/) end end context "with nm_controlled option set to false on first device" do let(:net1_nm_controlled) { false } let(:net2_nm_controlled) { true } it "creates and starts the networks with one managed manually and one NetworkManager controlled" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to match(/NetworkManager/) expect(comm.received_commands[0]).to_not match(/nmcli.*disconnect/) end end end end context "without NetworkManager installed" do before do allow(cap).to receive(:nmcli?).and_return false end context "with nm_controlled option omitted" do it "creates and starts the networks manually" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to_not match(/nmcli/) expect(comm.received_commands[0]).to_not match(/NetworkManager/) end end context "with nm_controlled option omitted" do let(:networks){ [[{nm_controlled: false}], [{nm_controlled: false}]] } it "creates and starts the networks manually" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/mkdir.*\/etc\/net\/ifaces/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to_not match(/nmcli/) expect(comm.received_commands[0]).to_not match(/NetworkManager/) end end context "with nm_controlled option set" do let(:networks){ [ [:public_network, network_1.merge(nm_controlled: true)], [:private_network, network_2.merge(nm_controlled: true)] ] } it "raises an error" do expect{ cap.configure_networks(machine, [network_1, network_2]) }.to raise_error(Vagrant::Errors::NetworkManagerNotInstalled) end end end end end ================================================ FILE: test/unit/plugins/guests/alt/cap/flavor_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestALT::Cap::Flavor" do let(:caps) do VagrantPlugins::GuestALT::Plugin .components .guest_capabilities[:alt] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".flavor" do let(:cap) { caps.get(:flavor) } context "without /etc/os-release file" do { "ALT 8.1 Server" => :alt_8, "ALT Education 8.1" => :alt_8, "ALT Workstation 8.1" => :alt_8, "ALT Workstation K 8.1 (Centaurea Ruthenica)" => :alt_8, "ALT Linux p8 (Hypericum)" => :alt_8, "ALT Sisyphus (unstable) (sisyphus)" => :alt, "ALT Linux Sisyphus (unstable)" => :alt, "ALT Linux 6.0.1 Spt (separator)" => :alt, "ALT Linux 7.0.5 School Master" => :alt, "ALT starter kit (Hypericum)" => :alt, "ALT" => :alt, "Simply" => :alt, }.each do |str, expected| it "returns #{expected} for #{str} in /etc/altlinux-release" do comm.stub_command("test -f /etc/os-release", exit_code: 1) comm.stub_command("cat /etc/altlinux-release", stdout: str) expect(cap.flavor(machine)).to be(expected) end end end context "with /etc/os-release file" do { [ "NAME=\"Sisyphus\"", "VERSION_ID=20161130" ] => :alt, [ "NAME=\"ALT Education\"", "VERSION_ID=8.1" ] => :alt_8, [ "NAME=\"ALT Server\"", "VERSION_ID=8.1" ] => :alt_8, [ "NAME=\"ALT SPServer\"", "VERSION_ID=8.0" ] => :alt_8, [ "NAME=\"starter kit\"", "VERSION_ID=p8" ] => :alt_8, [ "NAME=\"ALT Linux\"", "VERSION_ID=8.0.0" ] => :alt_8, [ "NAME=\"Simply Linux\"", "VERSION_ID=7.95.0" ] => :alt_8, [ "NAME=\"ALT Linux\"", "VERSION_ID=7.0.5" ] => :alt_7, [ "NAME=\"School Junior\"", "VERSION_ID=7.0.5" ] => :alt_7, }.each do |strs, expected| it "returns #{expected} for #{strs[0]} and #{strs[1]} in /etc/os-release" do comm.stub_command("test -f /etc/os-release", exit_code: 0) comm.stub_command("grep NAME /etc/os-release", stdout: strs[0]) comm.stub_command("grep VERSION_ID /etc/os-release", stdout: strs[1]) expect(cap.flavor(machine)).to be(expected) end end end end end ================================================ FILE: test/unit/plugins/guests/alt/cap/network_scripts_dir_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestALT::Cap::NetworkScriptsDir" do let(:caps) do VagrantPlugins::GuestALT::Plugin .components .guest_capabilities[:alt] end let(:machine) { double("machine") } describe ".network_scripts_dir" do let(:cap) { caps.get(:network_scripts_dir) } let(:name) { "banana-rama.example.com" } it "is /etc/net" do expect(cap.network_scripts_dir(machine)).to eq("/etc/net") end end end ================================================ FILE: test/unit/plugins/guests/alt/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestALT::Cap:RSync" do let(:caps) do VagrantPlugins::GuestALT::Plugin .components .guest_capabilities[:alt] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_install" do let(:cap) { caps.get(:rsync_install) } it "installs rsync" do cap.rsync_install(machine) expect(comm.received_commands[0]).to match(/install rsync/) end end end ================================================ FILE: test/unit/plugins/guests/amazon/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestAmazon::Cap::ConfigureNetworks" do let(:caps) do VagrantPlugins::GuestAmazon::Plugin .components .guest_capabilities[:amazon] end let(:machine) { double("machine", communicate: communicator) } let(:communicator) { double("communicator") } let(:networks) { double("networks") } describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } before do allow(cap).to receive(:systemd_networkd?). with(communicator).and_return(is_networkd) end context "when guest is using networkd" do let(:is_networkd) { true } it "should call the debian capability" do expect(VagrantPlugins::GuestDebian::Cap::ConfigureNetworks). to receive(:configure_networks).with(machine, networks) cap.configure_networks(machine, networks) end end context "when guest is not using networkd" do let(:is_networkd) { false } it "should call the redhat capability" do expect(VagrantPlugins::GuestRedHat::Cap::ConfigureNetworks). to receive(:configure_networks).with(machine, networks) cap.configure_networks(machine, networks) end end end end ================================================ FILE: test/unit/plugins/guests/amazon/cap/flavor_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestAmazon::Cap::Flavor" do let(:caps) do VagrantPlugins::GuestAmazon::Plugin .components .guest_capabilities[:amazon] end let(:machine) { double("machine") } describe ".flavor" do let(:cap) { caps.get(:flavor) } it "returns rhel" do expect(cap.flavor(machine)).to be(:rhel) end end end ================================================ FILE: test/unit/plugins/guests/arch/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestArch::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestArch::Plugin .components .guest_capabilities[:arch] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostnamectl set-hostname '#{basename}'/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/hostnamectl set-hostname/) end end end end ================================================ FILE: test/unit/plugins/guests/arch/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestArch::Cap::ConfigureNetworks" do let(:caps) do VagrantPlugins::GuestArch::Plugin .components .guest_capabilities[:arch] end let(:guest) { double("guest") } let(:machine) { double("machine", guest: guest) } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } before do allow(guest).to receive(:capability).with(:network_interfaces) .and_return(["eth1", "eth2"]) allow(cap).to receive(:systemd_networkd?).and_return(true) allow(cap).to receive(:systemd_network_manager?).and_return(false) end let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end it "creates and starts the networks" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/chmod 0644 '(.+)'/) expect(comm.received_commands[0]).to match(/mv (.+) '\/etc\/systemd\/network\/eth1.network'/) expect(comm.received_commands[0]).to match(/networkctl reload/) expect(comm.received_commands[0]).to match(/chmod 0644 '(.+)'/) expect(comm.received_commands[0]).to match(/mv (.+) '\/etc\/systemd\/network\/eth2.network'/) expect(comm.received_commands[0]).to match(/networkctl reload/) end it "should not extraneous && joiners" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).not_to match(/^\s*&&\s*$/) end context "network is not contolled by systemd" do before do allow(cap).to receive(:systemd_networkd?).and_return(false) end it "creates and starts the networks" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/mv (.+) '\/etc\/netctl\/eth1'/) expect(comm.received_commands[0]).to match(/ip link set 'eth1' down/) expect(comm.received_commands[0]).to match(/netctl restart 'eth1'/) expect(comm.received_commands[0]).to match(/netctl enable 'eth1'/) expect(comm.received_commands[0]).to match(/mv (.+) '\/etc\/netctl\/eth2'/) expect(comm.received_commands[0]).to match(/ip link set 'eth2' down/) expect(comm.received_commands[0]).to match(/netctl restart 'eth2'/) expect(comm.received_commands[0]).to match(/netctl enable 'eth2'/) end end context "network is controlled by NetworkManager via systemd" do before do expect(cap).to receive(:systemd_network_manager?).and_return(true) end it "should configure for network manager" do expect(cap).to receive(:configure_network_manager).with(machine, [network_1]) cap.configure_networks(machine, [network_1]) end end end end ================================================ FILE: test/unit/plugins/guests/arch/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestArch::Cap:RSync" do let(:described_class) do VagrantPlugins::GuestArch::Plugin .components .guest_capabilities[:arch] .get(:rsync_install) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_install" do it "installs rsync=" do described_class.rsync_install(machine) expect(comm.received_commands[0]).to match(/pacman -Sy --noconfirm/) expect(comm.received_commands[0]).to match(/pacman -S --noconfirm rsync/) end end end ================================================ FILE: test/unit/plugins/guests/arch/cap/smb_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestArch::Cap::SMB" do let(:described_class) do VagrantPlugins::GuestArch::Plugin .components .guest_capabilities[:arch] .get(:smb_install) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".smb_install" do it "installs smb when /usr/bin/mount.cifs does not exist" do comm.stub_command("test -f /usr/bin/mount.cifs", exit_code: 1) described_class.smb_install(machine) expect(comm.received_commands[1]).to match(/pacman -Sy --noconfirm/) expect(comm.received_commands[1]).to match(/pacman -S --noconfirm smbclient cifs-utils/) end it "does not install smb when /usr/bin/mount.cifs exists" do comm.stub_command("test -f /usr/bin/mount.cifs", exit_code: 0) described_class.smb_install(machine) expect(comm.received_commands.join("")).to_not match(/-S/) end end end ================================================ FILE: test/unit/plugins/guests/atomic/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestAtomic::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestAtomic::Plugin .components .guest_capabilities[:atomic] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostnamectl set-hostname '#{basename}'/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/hostnamectl set-hostname/) end end end end ================================================ FILE: test/unit/plugins/guests/atomic/cap/docker_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestAtomic::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestAtomic::Plugin .components .guest_capabilities[:atomic] .get(:docker_daemon_running) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".docker_daemon_running" do it "checks /run/docker/sock" do described_class.docker_daemon_running(machine) expect(comm.received_commands[0]).to eq("test -S /run/docker.sock") end end end ================================================ FILE: test/unit/plugins/guests/bsd/cap/file_system_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestBSD::Cap::FileSystem" do let(:caps) do VagrantPlugins::GuestBSD::Plugin .components .guest_capabilities[:bsd] end let(:machine) { double("machine", communicate: comm) } let(:comm) { double("comm") } before { allow(comm).to receive(:execute) } describe ".create_tmp_path" do let(:cap) { caps.get(:create_tmp_path) } let(:opts) { {} } it "should generate path on guest" do expect(comm).to receive(:execute).with(/mktemp/) cap.create_tmp_path(machine, opts) end it "should capture path generated on guest" do expect(comm).to receive(:execute).with(/mktemp/).and_yield(:stdout, "TMP_PATH") expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH") end it "should strip newlines on path" do expect(comm).to receive(:execute).with(/mktemp/).and_yield(:stdout, "TMP_PATH\n") expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH") end context "when type is a directory" do before { opts[:type] = :directory } it "should create guest path as a directory" do expect(comm).to receive(:execute).with(/-d/) cap.create_tmp_path(machine, opts) end end end describe ".decompress_tgz" do let(:cap) { caps.get(:decompress_tgz) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_tgz(machine, comp, dest, opts) } it "should create temporary directory for extraction" do expect(cap).to receive(:create_tmp_path) end it "should extract file with tar" do expect(comm).to receive(:execute).with(/tar/) end it "should extract file to temporary directory" do expect(comm).to receive(:execute).with(/TMP_DIR/) end it "should remove compressed file from guest" do expect(comm).to receive(:execute).with(/rm .*#{comp}/) end it "should remove extraction directory from guest" do expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) end it "should create parent directories for destination" do expect(comm).to receive(:execute).with(/mkdir -p .*to'/) end context "when type is directory" do before { opts[:type] = :directory } it "should create destination directory" do expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) end end end describe ".decompress_zip" do let(:cap) { caps.get(:decompress_zip) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_zip(machine, comp, dest, opts) } it "should create temporary directory for extraction" do expect(cap).to receive(:create_tmp_path) end it "should extract file with zip" do expect(comm).to receive(:execute).with(/zip/) end it "should extract file to temporary directory" do expect(comm).to receive(:execute).with(/TMP_DIR/) end it "should remove compressed file from guest" do expect(comm).to receive(:execute).with(/rm .*#{comp}/) end it "should remove extraction directory from guest" do expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) end it "should create parent directories for destination" do expect(comm).to receive(:execute).with(/mkdir -p .*to'/) end context "when type is directory" do before { opts[:type] = :directory } it "should create destination directory" do expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) end end end end ================================================ FILE: test/unit/plugins/guests/bsd/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestBSD::Cap::Halt" do let(:caps) do VagrantPlugins::GuestBSD::Plugin .components .guest_capabilities[:bsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:shutdown_command) { "/sbin/shutdown -p now" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".halt" do let(:cap) { caps.get(:halt) } it "runs the shutdown command" do comm.expect_command(shutdown_command) cap.halt(machine) end it "ignores an IOError" do comm.stub_command(shutdown_command, raise: IOError) expect { cap.halt(machine) }.to_not raise_error end it "ignores a Vagrant::Errors::SSHDisconnected" do comm.stub_command(shutdown_command, raise: Vagrant::Errors::SSHDisconnected) expect { cap.halt(machine) }.to_not raise_error end end end ================================================ FILE: test/unit/plugins/guests/bsd/cap/insert_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestBSD::Cap::InsertPublicKey" do let(:caps) do VagrantPlugins::GuestBSD::Plugin .components .guest_capabilities[:bsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".insert_public_key" do let(:cap) { caps.get(:insert_public_key) } it "inserts the public key" do cap.insert_public_key(machine, "ssh-rsa ...") expect(comm.received_commands[0]).to match(/mkdir -p ~\/.ssh/) expect(comm.received_commands[0]).to match(/chmod 0700 ~\/.ssh/) expect(comm.received_commands[0]).to match(/cat '\/tmp\/vagrant-(.+)' >> ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/chmod 0600 ~\/.ssh\/authorized_keys/) end end end ================================================ FILE: test/unit/plugins/guests/bsd/cap/mount_virtual_box_shared_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestBSD::Cap::MountVirtualBoxSharedFolder" do let(:caps) do VagrantPlugins::GuestBSD::Plugin .components .guest_capabilities[:bsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:mount_owner){ "vagrant" } let(:mount_group){ "vagrant" } let(:mount_uid){ "1000" } let(:mount_gid){ "1000" } let(:mount_name){ "vagrant" } let(:mount_guest_path){ "/vagrant" } let(:folder_options) do { owner: mount_owner, group: mount_group, hostpath: "/host/directory/path" } end let(:cap){ caps.get(:mount_virtualbox_shared_folder) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".mount_virtualbox_shared_folder" do it "raises an error as unsupported" do expect {cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) }. to raise_error(Vagrant::Errors::VirtualBoxMountNotSupportedBSD) end end end ================================================ FILE: test/unit/plugins/guests/bsd/cap/nfs_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestBSD::Cap::NFS" do let(:caps) do VagrantPlugins::GuestBSD::Plugin .components .guest_capabilities[:bsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".mount_nfs_folder" do let(:cap) { caps.get(:mount_nfs_folder) } let(:ip) { "1.2.3.4" } it "mounts the folder" do folders = { "/vagrant-nfs" => { guestpath: "/guest", hostpath: "/host", } } cap.mount_nfs_folder(machine, ip, folders) expect(comm.received_commands[0]).to match(/mkdir -p \/guest/) expect(comm.received_commands[1]).to match(/mount -t nfs/) expect(comm.received_commands[1]).to match(/1.2.3.4:\/host \/guest/) end it "mounts with options" do folders = { "/vagrant-nfs" => { guestpath: "/guest", hostpath: "/host", nfs_version: 2, nfs_udp: true, mount_options: ["banana"] } } cap.mount_nfs_folder(machine, ip, folders) expect(comm.received_commands[1]).to match(/mount -t nfs -o 'nfsv2,mntudp,banana'/) end it "escapes host and guest paths" do folders = { "/vagrant-nfs" => { guestpath: "/guest with spaces", hostpath: "/host's", } } cap.mount_nfs_folder(machine, ip, folders) expect(comm.received_commands[1]).to match(/host\\\'s/) expect(comm.received_commands[1]).to match(/guest\\\ with\\\ spaces/) end end end ================================================ FILE: test/unit/plugins/guests/bsd/cap/remove_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestBSD::Cap::RemovePublicKey" do let(:caps) do VagrantPlugins::GuestBSD::Plugin .components .guest_capabilities[:bsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".remove_public_key" do let(:cap) { caps.get(:remove_public_key) } it "removes the public key" do cap.remove_public_key(machine, "ssh-rsa ...") expect(comm.received_commands[0]).to match(/grep -v -x -f '\/tmp\/vagrant-(.+)' ~\/\.ssh\/authorized_keys > ~\/.ssh\/authorized_keys\.tmp/) expect(comm.received_commands[0]).to match(/mv ~\/.ssh\/authorized_keys\.tmp ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/chmod 0600 ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/rm -f '\/tmp\/vagrant-(.+)'/) end end end ================================================ FILE: test/unit/plugins/guests/centos/cap/flavor_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestCentos::Cap::Flavor" do let(:caps) do VagrantPlugins::GuestCentos::Plugin .components .guest_capabilities[:centos] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".flavor" do let(:cap) { caps.get(:flavor) } # /etc/os-release was added in EL7+ context "without /etc/os-release file" do { "" => :centos }.each do |str, expected| it "returns #{expected} for #{str}" do comm.stub_command("test -f /etc/os-release", exit_code: 1) expect(cap.flavor(machine)).to be(expected) end end end context "with /etc/os-release file" do { "7" => :centos_7, "8" => :centos_8, "9.0" => :centos_9, "9.1" => :centos_9, "" => :centos, "banana" => :centos, }.each do |str, expected| it "returns #{expected} for #{str}" do comm.stub_command("test -f /etc/os-release", exit_code: 0) comm.stub_command("source /etc/os-release && printf $VERSION_ID", stdout: str) expect(cap.flavor(machine)).to be(expected) end end end end end ================================================ FILE: test/unit/plugins/guests/coreos/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestCoreOS::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestCoreOS::Plugin .components .guest_capabilities[:coreos] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do let(:name) { "banana-rama.example.com" } let(:has_cloudinit) { false } before do allow(described_class).to receive(:systemd_unit_file?). with(anything, /cloudinit/).and_return(has_cloudinit) end context "with systemd cloud-init" do let(:has_cloudinit) { true } it "should upload cloudinit configuration file" do expect(comm).to receive(:upload) described_class.change_host_name(machine, name) end it "should set hostname in configuration file" do expect(comm).to receive(:upload) do |src, dst| contents = File.read(src) expect(contents).to include(name) end described_class.change_host_name(machine, name) end it "should start the cloudinit service" do expect(comm).to receive(:sudo).with(/systemctl start system-cloudinit/) described_class.change_host_name(machine, name) end end context "without systemd cloud-init" do it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) comm.expect_command("hostname 'banana-rama'") described_class.change_host_name(machine, name) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm.received_commands.size).to eq(1) end end end end ================================================ FILE: test/unit/plugins/guests/coreos/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestCoreOS::Cap::ConfigureNetworks" do let(:described_class) do VagrantPlugins::GuestCoreOS::Plugin .components .guest_capabilities[:coreos] .get(:configure_networks) end let(:machine) { double("machine", config: config, guest: guest) } let(:guest) { double("guest") } let(:config) { double("config", vm: vm) } let(:vm) { double("vm") } let(:comm) { double("comm") } let(:env) do double("env", machine: machine, active_machines: [machine]) end before do allow(machine).to receive(:communicate).and_return(comm) allow(machine).to receive(:env).and_return(env) end describe ".configure_networks" do context "with network manager" do let(:network_1) do { interface: 1, type: "static", ip: "10.0.0.3", netmask: "255.255.255.0", mac_address: "00:00:00:00:00:00", gateway: "10.0.0.2", } end let(:network_2) do { interface: 2, type: "static", ip: "192.168.3.3", netmask: "255.255.0.0", } end let(:nm_list) do [ "Wired connection 1:UUID_for_eth1:ethernet:eth1\n", "Wired connection 2:UUID_for_eth2:ethernet:eth2\n" ] end let(:interfaces) { ["eth0", "eth1", "eth2"] } let(:networks) do [ network_1, network_2, ] end let(:tempfile) do double("tempfile", close: nil, delete: nil, path: temp_path, ).tap do |f| allow(f).to receive(:puts) end end let(:temp_path) { "/dev/null" } before do allow(guest).to receive(:capability). with(:network_interfaces). and_return(interfaces) allow(comm).to receive(:upload) allow(comm).to receive(:sudo) allow(comm).to receive(:execute) allow(Tempfile).to receive(:new).and_return(tempfile) expect(comm).to receive(:execute). with("nmcli -t c show") { |&block| nm_list.each { |line| block.call(:stdout, line) } } allow(comm).to receive(:test). with("command -v cloud-init"). and_return(false) end it "should test for cloud-init" do expect(comm).to receive(:test). with("command -v cloud-init"). and_return(false) described_class.configure_networks(machine, networks) end it "should remove any previous vagrant configuration" do expect(comm).to receive(:sudo). with(/rm .*vagrant-.*conf/, error_check: false) described_class.configure_networks(machine, networks) end it "should get MAC address from guest if not provided" do expect(comm).to receive(:execute). with(/cat .*eth2\/address/) described_class.configure_networks(machine, networks) end it "should not get MAC address from guest when provided" do expect(comm).not_to receive(:execute). with(/cat .*eth1\/address/) described_class.configure_networks(machine, networks) end it "should provide a default gateway when one is not provided" do expect(tempfile).to receive(:puts). with("gateway=192.168.0.1") described_class.configure_networks(machine, networks) end it "should use gateway value when provided" do expect(tempfile).to receive(:puts). with("gateway=10.0.0.2") described_class.configure_networks(machine, networks) end it "should disconnect device in network manager" do expect(comm).to receive(:sudo). with("nmcli d disconnect 'eth1'", error_check: false) expect(comm).to receive(:sudo). with("nmcli d disconnect 'eth2'", error_check: false) described_class.configure_networks(machine, networks) end it "should delete connection from network manager" do expect(comm).to receive(:sudo). with("nmcli c delete 'UUID_for_eth1'", error_check: false) expect(comm).to receive(:sudo). with("nmcli c delete 'UUID_for_eth2'", error_check: false) described_class.configure_networks(machine, networks) end it "should upload configuration files" do expect(comm).to receive(:upload).twice described_class.configure_networks(machine, networks) end it "should change file ownership to root" do expect(comm).to receive(:sudo). with(/chown root:root .*/) described_class.configure_networks(machine, networks) end it "should modify file permissions to remove read access" do expect(comm).to receive(:sudo). with(/chmod 0600 .*/) described_class.configure_networks(machine, networks) end it "should delete local temporary files" do expect(tempfile).to receive(:delete) described_class.configure_networks(machine, networks) end it "should load the configuration files into network manager" do expect(comm).to receive(:sudo). with(/nmcli c load .*conf/).twice described_class.configure_networks(machine, networks) end it "should connect the devices in network manager" do expect(comm).to receive(:sudo). with("nmcli d connect 'eth1'") expect(comm).to receive(:sudo). with("nmcli d connect 'eth2'") described_class.configure_networks(machine, networks) end end context "with cloud-init" do let(:interfaces) { ["eth0", "eth1", "lo"] } let(:network_1) do { interface: 0, type: "dhcp", } end let(:netconfig_1) do [:public_interface, {}] end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end let(:netconfig_2) do [:public_network, {ip: "33.33.33.10", netmask: 16}] end let(:network_3) do { interface: 2, type: "static", ip: "192.168.120.22", netmask: "255.255.255.0", gateway: "192.168.120.1" } end let(:netconfig_3) do [:private_network, {ip: "192.168.120.22", netmask: 24}] end let(:networks) { [network_1, network_2, network_3] } let(:network_configs) { [netconfig_1, netconfig_2, netconfig_3] } let(:vm) { double("vm") } let(:default_env_ip) { described_class.const_get(:DEFAULT_ENVIRONMENT_IP) } before do allow(guest).to receive(:capability).with(:network_interfaces). and_return(interfaces) allow(vm).to receive(:networks).and_return(network_configs) allow(comm).to receive(:upload) allow(comm).to receive(:sudo) allow(comm).to receive(:test). with("command -v cloud-init"). and_return(true) end it "should upload network configuration file" do expect(comm).to receive(:upload) described_class.configure_networks(machine, networks) end it "should configure public ipv4 address" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) expect(content).to include("COREOS_PUBLIC_IPV4=#{netconfig_2.last[:ip]}") end described_class.configure_networks(machine, networks) end it "should configure the private ipv4 address" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) expect(content).to include("COREOS_PRIVATE_IPV4=#{netconfig_3.last[:ip]}") end described_class.configure_networks(machine, networks) end it "should configure network interfaces" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) interfaces.each { |i| expect(content).to include("Name=#{i}") } end described_class.configure_networks(machine, networks) end it "should configure DHCP interface" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) expect(content).to include("DHCP=yes") end described_class.configure_networks(machine, networks) end it "should configure static IP addresses" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) network_configs.map(&:last).find_all { |c| c[:ip] }.each { |c| expect(content).to include("Address=#{c[:ip]}") } end described_class.configure_networks(machine, networks) end context "when no public network is defined" do let(:networks) { [network_1, network_3] } let(:network_configs) { [netconfig_1, netconfig_3] } it "should set public IP to the default environment IP" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) expect(content).to include("COREOS_PUBLIC_IPV4=#{default_env_ip}") end described_class.configure_networks(machine, networks) end it "should set the private IP to the private network" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) expect(content).to include("COREOS_PRIVATE_IPV4=#{netconfig_3.last[:ip]}") end described_class.configure_networks(machine, networks) end end context "when no private network is defined" do let(:networks) { [network_1, network_2] } let(:network_configs) { [netconfig_1, netconfig_2] } it "should set public IP to the public network" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) expect(content).to include("COREOS_PUBLIC_IPV4=#{netconfig_2.last[:ip]}") end described_class.configure_networks(machine, networks) end it "should set the private IP to the public IP" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) expect(content).to include("COREOS_PRIVATE_IPV4=#{netconfig_2.last[:ip]}") end described_class.configure_networks(machine, networks) end end context "when no public or private network is defined" do let(:networks) { [network_1] } let(:network_configs) { [netconfig_1] } it "should set public IP to the default environment IP" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) expect(content).to include("COREOS_PUBLIC_IPV4=#{default_env_ip}") end described_class.configure_networks(machine, networks) end it "should set the private IP to the default environment IP" do expect(comm).to receive(:upload) do |src, dst| content = File.read(src) expect(content).to include("COREOS_PRIVATE_IPV4=#{default_env_ip}") end described_class.configure_networks(machine, networks) end end end end end ================================================ FILE: test/unit/plugins/guests/coreos/cap/docker_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestCoreOS::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestCoreOS::Plugin .components .guest_capabilities[:coreos] .get(:docker_daemon_running) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".docker_daemon_running" do it "checks /run/docker/sock" do described_class.docker_daemon_running(machine) expect(comm.received_commands[0]).to eq("test -S /run/docker.sock") end end end ================================================ FILE: test/unit/plugins/guests/darwin/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDarwin::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestDarwin::Plugin .components .guest_capabilities[:darwin] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } let(:networks) {} before do allow(machine).to receive(:communicate).and_return(comm) allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[1]).to match(/scutil --set ComputerName '#{name}'/) expect(comm.received_commands[1]).to match(/scutil --set HostName '#{name}'/) expect(comm.received_commands[1]).to match(/scutil --set LocalHostName '#{basename}'/) expect(comm.received_commands[1]).to match(/hostname 'banana-rama.example.com'/) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/scutil --set ComputerName '#{name}/) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) end end context "multiple networks configured with hostname" do it "adds a new entry only for the hostname" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :hostname=>true, :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(described_class).to receive(:replace_host) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) described_class.change_host_name(machine, name) end it "appends an entry to the loopback interface" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(described_class).to_not receive(:replace_host) expect(described_class).to receive(:add_hostname_to_loopback_interface).once described_class.change_host_name(machine, name) end end end end ================================================ FILE: test/unit/plugins/guests/darwin/cap/choose_addressable_ip_addr_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDarwin::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestDarwin::Plugin .components .guest_capabilities[:darwin] .get(:choose_addressable_ip_addr) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".choose_addressable_ip_addr" do let(:possible) { ["1.2.3.4", "5.6.7.8"] } it "retrieves the value" do comm.stub_command("ping -c1 -t1 5.6.7.8", exit_code: 0) result = described_class.choose_addressable_ip_addr(machine, possible) expect(result).to eq("5.6.7.8") end it "returns nil if no ips are found" do result = described_class.choose_addressable_ip_addr(machine, []) expect(result).to be(nil) end end end ================================================ FILE: test/unit/plugins/guests/darwin/cap/darwin_version_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDarwin::Cap::DarwinVersion" do let(:caps) do VagrantPlugins::GuestDarwin::Plugin .components .guest_capabilities[:darwin] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".darwin_version" do let(:cap) { caps.get(:darwin_version) } { "kern.osrelease: 19.6.0" => "19.6.0", "kern.osrelease: 20.1.10" => "20.1.10", }.each do |str, expected| it "returns #{expected} for #{str}" do comm.stub_command("sysctl kern.osrelease", stdout: str) expect(cap.darwin_version(machine)).to eq(expected) end end end describe ".darwin_major_version" do let(:cap) { caps.get(:darwin_major_version) } { "kern.osrelease: 19.6.0" => 19, "kern.osrelease: 20.1.10" => 20, "" => nil }.each do |str, expected| it "returns #{expected} for #{str}" do comm.stub_command("sysctl kern.osrelease", stdout: str) expect(cap.darwin_major_version(machine)).to eq(expected) end end end end ================================================ FILE: test/unit/plugins/guests/darwin/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDarwin::Cap::Halt" do let(:caps) do VagrantPlugins::GuestDarwin::Plugin .components .guest_capabilities[:darwin] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".halt" do let(:cap) { caps.get(:halt) } it "runs the shutdown command" do comm.expect_command("/sbin/shutdown -h now") cap.halt(machine) end it "ignores an IOError" do comm.stub_command("/sbin/shutdown -h now", raise: IOError) expect { cap.halt(machine) }.to_not raise_error end it "ignores a Vagrant::Errors::SSHDisconnected" do comm.stub_command("/sbin/shutdown -h now", raise: Vagrant::Errors::SSHDisconnected) expect { cap.halt(machine) }.to_not raise_error end end end ================================================ FILE: test/unit/plugins/guests/darwin/cap/mount_vmware_shared_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDarwin::Cap::MountVmwareSharedFolder" do let(:described_class) do VagrantPlugins::GuestDarwin::Plugin .components .guest_capabilities[:darwin] .get(:mount_vmware_shared_folder) end let(:machine) { double("machine", communicate: communicator, id: "MACHINE_ID", guest: guest) } let(:guest) {double("guest")} let(:communicator) { double("communicator") } before do allow(communicator).to receive(:test) allow(communicator).to receive(:sudo) allow(VagrantPlugins::GuestDarwin::Plugin).to receive(:action_hook) end describe ".mount_vmware_shared_folder" do let(:name) { "-vagrant" } let(:guestpath) { "/vagrant" } let(:options) { {} } before do allow(described_class).to receive(:system_firmlink?) described_class.reset! end after { described_class.mount_vmware_shared_folder(machine, name, guestpath, options) } context "with APFS root container" do before do expect(communicator).to receive(:test).with("test -d /System/Volumes/Data").and_return(true) end it "should check for existing entry" do expect(communicator).to receive(:test).with(/synthetic\.conf/) end context "with guest path within existing directory" do let(:guestpath) { "/Users/vagrant/workspace" } it "should test if guest path is a symlink" do expect(communicator).to receive(:test).with(/test -L/) end it "should remove guest path if it is a symlink" do expect(communicator).to receive(:test).with(/test -L/).and_return(true) expect(communicator).to receive(:sudo).with(/rm -f/) end it "should not test if guest path is a directory if guest path is symlink" do expect(communicator).to receive(:test).with(/test -L/).and_return(true) expect(communicator).not_to receive(:test).with(/test -d/) end it "should test if guest path is directory if not a symlink" do expect(communicator).to receive(:test).with(/test -d/) end it "should remove guest path if it is a directory" do expect(communicator).to receive(:test).with(/test -d/).and_return(true) expect(communicator).to receive(:sudo).with(/rm -Rf/) end it "should create the symlink to the vmware folder" do expect(communicator).to receive(:sudo).with(/ln -s/) end it "should create the symlink within the writable APFS container" do expect(communicator).to receive(:sudo).with(%r{ln -s .+/System/Volumes/Data.+}) end { 19 => "-B", 20 => "-t", 21 => "-t", nil => "-B" }.each do |version, expected_flag| it "should re-bootstrap root dir for darwin version #{version}" do expect(communicator).to receive(:sudo).with(/apfs.util #{expected_flag}/, any_args) expect(guest).to receive(:capability).with("darwin_major_version").and_return(version) described_class.mount_vmware_shared_folder(machine, name, guestpath, options) described_class.apfs_firmlinks_delayed[machine.id].call end end context "when firmlink is provided by the system" do before { expect(described_class).to receive(:system_firmlink?).and_return(true) } it "should not register an action hook" do expect(VagrantPlugins::GuestDarwin::Plugin).not_to receive(:action_hook).with(:apfs_firmlinks, :after_synced_folders) end end end end context "with non-APFS root container" do before do expect(communicator).to receive(:test).with("test -d /System/Volumes/Data").and_return(false) end it "should test if guest path is a symlink" do expect(communicator).to receive(:test).with(/test -L/) end it "should remove guest path if it is a symlink" do expect(communicator).to receive(:test).with(/test -L/).and_return(true) expect(communicator).to receive(:sudo).with(/rm -f/) end it "should not test if guest path is a directory if guest path is symlink" do expect(communicator).to receive(:test).with(/test -L/).and_return(true) expect(communicator).not_to receive(:test).with(/test -d/) end it "should test if guest path is directory if not a symlink" do expect(communicator).to receive(:test).with(/test -d/) end it "should remove guest path if it is a directory" do expect(communicator).to receive(:test).with(/test -d/).and_return(true) expect(communicator).to receive(:sudo).with(/rm -Rf/) end it "should create the symlink to the vmware folder" do expect(communicator).to receive(:sudo).with(/ln -s/) end it "should not register an action hook" do expect(VagrantPlugins::GuestDarwin::Plugin).not_to receive(:action_hook).with(:apfs_firmlinks, :after_synced_folders) end end end describe ".system_firmlink?" do before { described_class.reset! } context "when file does not exist" do before { allow(File).to receive(:exist?).with("/usr/share/firmlinks").and_return(false) } it "should always return false" do expect(described_class.system_firmlink?("test")).to be_falsey end end context "when file does exist" do let(:content) { ["/Users\tUsers", "/usr/local\tusr/local"] } before do expect(File).to receive(:exist?).with("/usr/share/firmlinks").and_return(true) expect(File).to receive(:readlines).with("/usr/share/firmlinks").and_return(content) end it "should return true when firmlink exists" do expect(described_class.system_firmlink?("/Users")).to be_truthy end it "should return true when firmlink is not prefixed with /" do expect(described_class.system_firmlink?("Users")).to be_truthy end it "should return false when firmlink does not exist" do expect(described_class.system_firmlink?("/testing")).to be_falsey end end end describe ".write_apfs_firmlinks" do let(:env) { nil } let(:action) { double("action", call: nil) } let(:machine) { double("machine", id: machine_id) } let(:machine_id) { double("machine_id") } let(:delayed) { {} } context "when env is nil" do it "should be a no-op" do expect(described_class).not_to receive(:apfs_firmlinks_delayed) described_class.write_apfs_firmlinks(env) end end context "when env is empty hash" do let(:env) { {} } it "should be a no-op" do expect(described_class).not_to receive(:apfs_firmlinks_delayed) described_class.write_apfs_firmlinks(env) end end context "when machine is defined within env" do let(:env) { {machine: machine} } it "should request the stored delayed actions" do expect(described_class).to receive(:apfs_firmlinks_delayed).and_return(delayed) described_class.write_apfs_firmlinks(env) end end context "when delayed action is stored for machine" do let(:env) { {machine: machine} } before { described_class.apfs_firmlinks_delayed[machine_id] = action } after { described_class.apfs_firmlinks_delayed.clear } it "should call the delayed action" do expect(action).to receive(:call) described_class.write_apfs_firmlinks(env) end it "should remove the action after calling" do expect(action).to receive(:call) described_class.write_apfs_firmlinks(env) expect(delayed).to be_empty end end end end ================================================ FILE: test/unit/plugins/guests/darwin/cap/shell_expand_guest_path_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDarwin::Cap::ShellExpandGuestPath" do let(:caps) do VagrantPlugins::GuestDarwin::Plugin .components .guest_capabilities[:darwin] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end describe "#shell_expand_guest_path" do let(:cap) { caps.get(:shell_expand_guest_path) } it "expands the path" do path = "/home/vagrant/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, "/home/vagrant/folder") cap.shell_expand_guest_path(machine, path) end it "raises an exception if no path was detected" do path = "/home/vagrant/folder" expect { cap.shell_expand_guest_path(machine, path) }. to raise_error(Vagrant::Errors::ShellExpandFailed) end it "returns a path with a space in it" do path = "/home/vagrant folder/folder" path_with_spaces = "/home/vagrant\\ folder/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, path_with_spaces) expect(machine.communicate).to receive(:execute).with("printf #{path_with_spaces}") cap.shell_expand_guest_path(machine, path) end end end ================================================ FILE: test/unit/plugins/guests/debian/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDebian::Cap::ChangeHostName" do let(:caps) do VagrantPlugins::GuestDebian::Plugin .components .guest_capabilities[:debian] end let(:machine) { double("machine", name: "guestname") } let(:logger) { double("logger", debug: true) } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do let(:cap) { caps.get(:change_host_name) } let(:name) { 'banana-rama.example.com' } let(:systemd) { true } let(:hostnamectl) { true } let(:networkd) { true } let(:network_manager) { false } let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(cap).to receive(:systemd?).and_return(systemd) allow(cap).to receive(:hostnamectl?).and_return(hostnamectl) allow(cap).to receive(:systemd_networkd?).and_return(networkd) allow(cap).to receive(:systemd_controlled?).with(anything, /NetworkManager/).and_return(network_manager) allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) allow(cap).to receive(:add_hostname_to_loopback_interface) allow(cap).to receive(:replace_host) end context "minimal network config" do it "sets the hostname if not set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) cap.change_host_name(machine, name) expect(comm.received_commands[1]).to match(/echo 'banana-rama' > \/etc\/hostname/) end it "sets the hostname if not set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) cap.change_host_name(machine, name) expect(comm.received_commands[1]).to_not match(/echo 'banana-rama' > \/etc\/hostname/) end end context "multiple networks configured with hostname" do it "adds a new entry only for the hostname" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :hostname=>true, :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(cap).to receive(:replace_host) expect(cap).to_not receive(:add_hostname_to_loopback_interface) cap.change_host_name(machine, name) end it "appends an entry to the loopback interface" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(cap).to_not receive(:replace_host) expect(cap).to receive(:add_hostname_to_loopback_interface).once cap.change_host_name(machine, name) end end context "when hostnamectl is in use" do let(:hostnamectl) { true } it "sets hostname with hostnamectl" do cap.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostnamectl/) end end context "when hostnamectl is not in use" do let(:hostnamectl) { false } it "sets hostname with hostname command" do cap.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostname -F/) end end context "restarts the network" do context "when networkd is in use" do let(:networkd) { true } it "restarts networkd with systemctl" do cap.change_host_name(machine, name) expect(comm.received_commands[3]).to match(/systemctl restart systemd-networkd/) end end context "when NetworkManager is in use" do let(:networkd) { false } let(:network_manager) { true } it "restarts NetworkManager with systemctl" do cap.change_host_name(machine, name) expect(comm.received_commands[3]).to match(/systemctl restart NetworkManager/) end end context "when networkd and NetworkManager are not in use" do let(:networkd) { false } let(:network_manager) { false } let(:systemd) { true } it "restarts the network using systemctl" do expect(cap).to receive(:restart_each_interface). with(machine, anything) cap.change_host_name(machine, name) end it "restarts networking with networking init script" do expect(cap).to receive(:restart_each_interface). with(machine, anything) cap.change_host_name(machine, name) end end context "when systemd is not in use" do let(:systemd) { false } it "restarts the network using service" do expect(cap).to receive(:restart_each_interface). with(machine, anything) cap.change_host_name(machine, name) end it "restarts the network using ifdown/ifup" do expect(cap).to receive(:restart_each_interface). with(machine, anything) cap.change_host_name(machine, name) end end end it "does not set the hostname if unset" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) expect(cap).to_not receive(:add_hostname_to_loopback_interface) expect(cap).to_not receive(:replace_host) cap.change_host_name(machine, name) end end describe ".restart_each_interface" do let(:cap) { caps.get(:change_host_name) } let(:systemd) { true } let(:interfaces) { ["eth0", "eth1", "eth2"] } before do allow(cap).to receive(:systemd?).and_return(systemd) allow(VagrantPlugins::GuestLinux::Cap::NetworkInterfaces).to receive(:network_interfaces). and_return(interfaces) end context "with nettools" do let(:systemd) { false } it "restarts every interface" do cap.send(:restart_each_interface, machine, logger) expect(comm.received_commands[0]).to match(/ifdown eth0;ifup eth0/) expect(comm.received_commands[1]).to match(/ifdown eth1;ifup eth1/) expect(comm.received_commands[2]).to match(/ifdown eth2;ifup eth2/) end end context "with systemctl" do it "restarts every interface" do cap.send(:restart_each_interface, machine, logger) expect(comm.received_commands[0]).to match(/systemctl stop ifup@eth0.service;systemctl start ifup@eth0.service/) expect(comm.received_commands[1]).to match(/systemctl stop ifup@eth1.service;systemctl start ifup@eth1.service/) expect(comm.received_commands[2]).to match(/systemctl stop ifup@eth2.service;systemctl start ifup@eth2.service/) end end end end ================================================ FILE: test/unit/plugins/guests/debian/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDebian::Cap::ConfigureNetworks" do let(:caps) do VagrantPlugins::GuestDebian::Plugin .components .guest_capabilities[:debian] end let(:guest) { double("guest") } let(:machine) { double("machine", guest: guest) } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe "#build_interface_entries" do let(:network_0) do { interface: 0, type: "dhcp", } end let(:network_1) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end end describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } before do allow(guest).to receive(:capability).with(:network_interfaces) .and_return(["eth1", "eth2"]) end let(:network_0) do { interface: 0, type: "dhcp", } end let(:network_1) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end before do allow(comm).to receive(:test).with("nmcli -t d show eth1").and_return(false) allow(comm).to receive(:test).with("nmcli -t d show eth2").and_return(false) allow(comm).to receive(:test).with("ps -o comm= 1 | grep systemd", {sudo: true}).and_return(false) allow(comm).to receive(:test).with("systemctl -q is-active systemd-networkd.service", anything).and_return(false) allow(comm).to receive(:test).with("command -v netplan").and_return(false) end it "creates and starts the networks using net-tools" do cap.configure_networks(machine, [network_0, network_1]) expect(comm.received_commands[0]).to match("/sbin/ifdown 'eth1' || true") expect(comm.received_commands[0]).to match("/sbin/ip addr flush dev 'eth1'") expect(comm.received_commands[0]).to match("/sbin/ifdown 'eth2' || true") expect(comm.received_commands[0]).to match("/sbin/ip addr flush dev 'eth2'") expect(comm.received_commands[1]).to match("/sbin/ifup 'eth1'") expect(comm.received_commands[1]).to match("/sbin/ifup 'eth2'") end context "with systemd" do before do expect(comm).to receive(:test).with("ps -o comm= 1 | grep systemd", {sudo: true}).and_return(true) allow(comm).to receive(:test).with("command -v netplan").and_return(false) end it "creates and starts the networks using net-tools" do cap.configure_networks(machine, [network_0, network_1]) expect(comm.received_commands[0]).to match("/sbin/ifdown 'eth1' || true") expect(comm.received_commands[0]).to match("/sbin/ip addr flush dev 'eth1'") expect(comm.received_commands[0]).to match("/sbin/ifdown 'eth2' || true") expect(comm.received_commands[0]).to match("/sbin/ip addr flush dev 'eth2'") expect(comm.received_commands[1]).to match("/sbin/ifup 'eth1'") expect(comm.received_commands[1]).to match("/sbin/ifup 'eth2'") end context "with systemd-networkd" do let(:net_conf_dhcp) { "[Match]\nName=eth1\n[Network]\nDHCP=yes" } let(:net_conf_static) { "[Match]\nName=eth2\n[Network]\nDHCP=no\nAddress=33.33.33.10/16\nGateway=33.33.0.1" } before do expect(comm).to receive(:test).with("systemctl -q is-active systemd-networkd.service", anything).and_return(true) end it "creates and starts the networks using systemd-networkd" do cap.configure_networks(machine, [network_0, network_1]) expect(comm.received_commands[0]).to match("mv -f '/tmp/vagrant-network-entry.*' '/etc/systemd/network/.*network'") expect(comm.received_commands[0]).to match("chown") expect(comm.received_commands[0]).to match("chmod") expect(comm.received_commands[2]).to match("systemctl restart") end it "properly configures DHCP and static IPs if defined" do expect(cap).to receive(:upload_tmp_file).with(comm, net_conf_dhcp) expect(cap).to receive(:upload_tmp_file).with(comm, net_conf_static) cap.configure_networks(machine, [network_0, network_1]) expect(comm.received_commands[0]).to match("mkdir -p /etc/systemd/network") expect(comm.received_commands[0]).to match("mv -f '' '/etc/systemd/network/50-vagrant-eth1.network'") expect(comm.received_commands[0]).to match("chown root:root '/etc/systemd/network/50-vagrant-eth1.network'") expect(comm.received_commands[0]).to match("chmod 0644 '/etc/systemd/network/50-vagrant-eth1.network'") expect(comm.received_commands[2]).to match("systemctl restart") end end context "with netplan" do before do expect(comm).to receive(:test).with("command -v netplan").and_return(true) end let(:nm_yml) { "---\nnetwork:\n version: 2\n renderer: NetworkManager\n ethernets:\n eth1:\n dhcp4: true\n eth2:\n addresses:\n - 33.33.33.10/16\n gateway4: 33.33.0.1\n" } let(:networkd_yml) { "---\nnetwork:\n version: 2\n renderer: networkd\n ethernets:\n eth1:\n dhcp4: true\n eth2:\n addresses:\n - 33.33.33.10/16\n gateway4: 33.33.0.1\n" } it "uses NetworkManager if detected on device" do allow(cap).to receive(:systemd_networkd?).and_return(false) allow(cap).to receive(:nmcli?).and_return(true) allow(cap).to receive(:nm_controlled?).and_return(true) allow(comm).to receive(:test).with("nmcli -t d show eth1").and_return(true) allow(comm).to receive(:test).with("nmcli -t d show eth2").and_return(true) expect(cap).to receive(:upload_tmp_file).with(comm, nm_yml) .and_return("/tmp/vagrant-network-entry.1234") cap.configure_networks(machine, [network_0, network_1]) expect(comm.received_commands[0]).to match("mv -f '/tmp/vagrant-network-entry.*' '/etc/netplan/.*.yaml'") expect(comm.received_commands[0]).to match("chown") expect(comm.received_commands[0]).to match("chmod") expect(comm.received_commands[0]).to match("netplan apply") end it "raises and error if NetworkManager is detected on device but nmcli is not installed" do allow(cap).to receive(:systemd_networkd?).and_return(true) allow(cap).to receive(:nmcli?).and_return(false) allow(cap).to receive(:nm_controlled?).and_return(true) allow(comm).to receive(:test).with("nmcli -t d show eth1").and_return(true) allow(comm).to receive(:test).with("nmcli -t d show eth2").and_return(true) expect { cap.configure_networks(machine, [network_0, network_1]) }.to raise_error(Vagrant::Errors::NetworkManagerNotInstalled) end it "creates and starts the networks for systemd with netplan" do allow(cap).to receive(:systemd_networkd?).and_return(true) expect(cap).to receive(:upload_tmp_file).with(comm, networkd_yml) .and_return("/tmp/vagrant-network-entry.1234") cap.configure_networks(machine, [network_0, network_1]) expect(comm.received_commands[0]).to match("mv -f '/tmp/vagrant-network-entry.*' '/etc/netplan/.*.yaml'") expect(comm.received_commands[0]).to match("chown") expect(comm.received_commands[0]).to match("chmod") expect(comm.received_commands[0]).to match("netplan apply") end end end end end ================================================ FILE: test/unit/plugins/guests/debian/cap/nfs_client_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDebian::Cap::NFSClient" do let(:described_class) do VagrantPlugins::GuestDebian::Plugin .components .guest_capabilities[:debian] .get(:nfs_client_install) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".nfs_client_install" do it "installs nfs client utilities" do described_class.nfs_client_install(machine) expect(comm.received_commands[0]).to match(/apt-get -yqq update/) expect(comm.received_commands[0]).to match(/apt-get -yqq install nfs-common portmap/) end end end ================================================ FILE: test/unit/plugins/guests/debian/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDebian::Cap:RSync" do let(:described_class) do VagrantPlugins::GuestDebian::Plugin .components .guest_capabilities[:debian] .get(:rsync_install) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_install" do it "installs rsync" do described_class.rsync_install(machine) expect(comm.received_commands[0]).to match(/apt-get -yqq update/) expect(comm.received_commands[0]).to match(/apt-get -yqq install rsync/) end end end ================================================ FILE: test/unit/plugins/guests/debian/cap/smb_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestDebian::Cap::SMB" do let(:described_class) do VagrantPlugins::GuestDebian::Plugin .components .guest_capabilities[:debian] .get(:smb_install) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".smb_install" do it "installs smb when /sbin/mount.cifs does not exist" do comm.stub_command("test -f /sbin/mount.cifs", exit_code: 1) described_class.smb_install(machine) expect(comm.received_commands[1]).to match(/apt-get -yqq update/) expect(comm.received_commands[1]).to match(/apt-get -yqq install cifs-utils/) end it "does not install smb when /sbin/mount.cifs exists" do comm.stub_command("test -f /sbin/mount.cifs", exit_code: 0) described_class.smb_install(machine) expect(comm.received_commands.join("")).to_not match(/update/) end end end ================================================ FILE: test/unit/plugins/guests/esxi/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestEsxi::Cap::Halt" do let(:caps) do VagrantPlugins::GuestEsxi::Plugin .components .guest_capabilities[:esxi] end let(:shutdown_command){ "/bin/halt -d 0" } let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".halt" do let(:cap) { caps.get(:halt) } it "runs the shutdown command" do comm.expect_command(shutdown_command) cap.halt(machine) end it "ignores an IOError" do comm.stub_command(shutdown_command, raise: IOError) expect { cap.halt(machine) }.to_not raise_error end it "ignores a Vagrant::Errors::SSHDisconnected" do comm.stub_command(shutdown_command, raise: Vagrant::Errors::SSHDisconnected) expect { cap.halt(machine) }.to_not raise_error end end end ================================================ FILE: test/unit/plugins/guests/esxi/cap/public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestEsxi::Cap::PublicKey" do let(:caps) do VagrantPlugins::GuestEsxi::Plugin .components .guest_capabilities[:esxi] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".insert_public_key" do let(:cap) { caps.get(:insert_public_key) } it "inserts the public key" do cap.insert_public_key(machine, "ssh-rsa ...") expect(comm.received_commands[0]).to match(/SSH_DIR=".*"/) expect(comm.received_commands[0]).to match(/mkdir -p "\${SSH_DIR}"/) expect(comm.received_commands[0]).to match(/chmod 0700 "\${SSH_DIR}"/) expect(comm.received_commands[0]).to match(/cat '\/tmp\/vagrant-(.+)' >> "\${SSH_DIR}\/authorized_keys"/) expect(comm.received_commands[0]).to match(/chmod 0600 "\${SSH_DIR}\/authorized_keys"/) expect(comm.received_commands[0]).to match(/rm -f '\/tmp\/vagrant-(.+)'/) end end describe ".remove_public_key" do let(:cap) { caps.get(:remove_public_key) } it "removes the public key" do cap.remove_public_key(machine, "ssh-rsa ...") expect(comm.received_commands[0]).to match(/SSH_DIR=".*"/) expect(comm.received_commands[0]).to match(/grep -v -x -f '\/tmp\/vagrant-(.+)' "\${SSH_DIR}\/authorized_keys" > "\${SSH_DIR}\/authorized_keys\.tmp"/) expect(comm.received_commands[0]).to match(/mv "\${SSH_DIR}\/authorized_keys\.tmp" "\${SSH_DIR}\/authorized_keys"/) expect(comm.received_commands[0]).to match(/chmod 0600 "\${SSH_DIR}\/authorized_keys"/) expect(comm.received_commands[0]).to match(/rm -f '\/tmp\/vagrant-(.+)'/) end end end ================================================ FILE: test/unit/plugins/guests/freebsd/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestFreeBSD::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestFreeBSD::Plugin .components .guest_capabilities[:freebsd] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } let(:networks) {} before do allow(machine).to receive(:communicate).and_return(comm) allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[1]).to match(/hostname '#{name}'/) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/hostname '#{name}'/) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) end end context "multiple networks configured with hostname" do it "adds a new entry only for the hostname" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :hostname=>true, :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(described_class).to receive(:replace_host) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) described_class.change_host_name(machine, name) end it "appends an entry to the loopback interface" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(described_class).to_not receive(:replace_host) expect(described_class).to receive(:add_hostname_to_loopback_interface).once described_class.change_host_name(machine, name) end end end end ================================================ FILE: test/unit/plugins/guests/freebsd/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestFreeBSD::Cap::ConfigureNetworks" do let(:described_class) do VagrantPlugins::GuestFreeBSD::Plugin .components .guest_capabilities[:freebsd] .get(:configure_networks) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) comm.stub_command("ifconfig -l ether", stdout: "em1 em2") end after do comm.verify_expectations! end describe ".configure_networks" do let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end it "creates and starts the networks" do described_class.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[1]).to match(/dhclient 'em1'/) expect(comm.received_commands[1]).to match(/\/etc\/rc.d\/netif restart 'em1'/) expect(comm.received_commands[1]).to_not match(/dhclient 'em2'/) expect(comm.received_commands[1]).to match(/\/etc\/rc.d\/netif restart 'em2'/) end end end ================================================ FILE: test/unit/plugins/guests/freebsd/cap/mount_virtual_box_shared_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestFreeBSD::Cap::MountVirtualBoxSharedFolder" do let(:caps) do VagrantPlugins::GuestFreeBSD::Plugin .components .guest_capabilities[:freebsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:mount_owner){ "vagrant" } let(:mount_group){ "vagrant" } let(:mount_uid){ "1000" } let(:mount_gid){ "1000" } let(:mount_name){ "vagrant" } let(:mount_guest_path){ "/vagrant" } let(:folder_options) do { owner: mount_owner, group: mount_group, hostpath: "/host/directory/path" } end let(:cap){ caps.get(:mount_virtualbox_shared_folder) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".mount_virtualbox_shared_folder" do before do allow(comm).to receive(:sudo).with(any_args) allow(comm).to receive(:execute).with(any_args) end it "generates the expected default mount command" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end it "automatically chown's the mounted directory on guest" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) expect(comm).to receive(:sudo).with("chown #{mount_uid}:#{mount_gid} #{mount_guest_path}") cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end context "with owner user ID explicitly defined" do before do expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") end context "with user ID provided as Integer" do let(:mount_owner){ 2000 } it "generates the expected mount command using mount_owner directly" do expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_owner},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) expect(comm).to receive(:sudo).with("chown #{mount_owner}:#{mount_gid} #{mount_guest_path}") cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end context "with user ID provided as String" do let(:mount_owner){ "2000" } it "generates the expected mount command using mount_owner directly" do expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_owner},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) expect(comm).to receive(:sudo).with("chown #{mount_owner}:#{mount_gid} #{mount_guest_path}") cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end end context "with owner group ID explicitly defined" do before do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) end context "with owner group ID provided as Integer" do let(:mount_group){ 2000 } it "generates the expected mount command using mount_group directly" do expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{mount_group} #{mount_name} #{mount_guest_path}", anything) expect(comm).to receive(:sudo).with("chown #{mount_uid}:#{mount_group} #{mount_guest_path}") cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end context "with owner group ID provided as String" do let(:mount_group){ "2000" } it "generates the expected mount command using mount_group directly" do expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{mount_group} #{mount_name} #{mount_guest_path}", anything) expect(comm).to receive(:sudo).with("chown #{mount_uid}:#{mount_group} #{mount_guest_path}") cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end end context "with non-existent default owner group" do it "fetches the effective group ID of the user" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_raise(Vagrant::Errors::VirtualBoxMountFailed, {command: '', output: ''}) expect(comm).to receive(:execute).with("id -g #{mount_owner}", anything).and_yield(:stdout, "1").and_return(0) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end context "with non-existent owner group" do it "raises an error" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_raise(Vagrant::Errors::VirtualBoxMountFailed, {command: '', output: ''}) expect do cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end.to raise_error Vagrant::Errors::VirtualBoxMountFailed end end context "with read-only option defined" do it "does not chown mounted guest directory" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") expect(comm).to receive(:sudo).with("mount -t vboxvfs -o ro,uid=#{mount_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) expect(comm).not_to receive(:sudo).with("chown #{mount_uid}:#{mount_gid} #{mount_guest_path}") cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["ro"])) end end context "with upstart init" do it "emits mount event" do expect(comm).to receive(:sudo).with(/initctl emit/) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end context "with custom mount options" do let(:ui){ Vagrant::UI::Silent.new } before do allow(machine).to receive(:ui).and_return(ui) end context "with uid defined" do let(:options_uid){ '1234' } it "should only include uid defined within mount options" do expect(comm).not_to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{options_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["uid=#{options_uid}"])) end end context "with gid defined" do let(:options_gid){ '1234' } it "should only include gid defined within mount options" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).not_to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{mount_uid},gid=#{options_gid} #{mount_name} #{mount_guest_path}", anything) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["gid=#{options_gid}"])) end end context "with uid and gid defined" do let(:options_gid){ '1234' } let(:options_uid){ '1234' } it "should only include uid and gid defined within mount options" do expect(comm).not_to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).not_to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{options_gid}:") expect(comm).to receive(:sudo).with("mount -t vboxvfs -o uid=#{options_uid},gid=#{options_gid} #{mount_name} #{mount_guest_path}", anything) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["gid=#{options_gid}", "uid=#{options_uid}"])) end end end context "with guest builtin vboxvfs module" do let(:vbox_stderr){ <<-EOF mount.vboxvfs cannot be used with mainline vboxvfs; instead use: mount -cit vboxvfs NAME MOUNTPOINT EOF } it "should perform guest mount using builtin module" do expect(comm).to receive(:sudo).with(/mount -t vboxvfs/, any_args).and_yield(:stderr, vbox_stderr).and_return(1) expect(comm).to receive(:sudo).with(/mount -cit/, any_args) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end end describe ".unmount_virtualbox_shared_folder" do after { cap.unmount_virtualbox_shared_folder(machine, mount_guest_path, folder_options) } it "unmounts shared directory and deletes directory on guest" do expect(comm).to receive(:sudo).with("umount #{mount_guest_path}", anything).and_return(0) expect(comm).to receive(:sudo).with("rmdir #{mount_guest_path}", anything) end it "does not delete guest directory if unmount fails" do expect(comm).to receive(:sudo).with("umount #{mount_guest_path}", anything).and_return(1) expect(comm).not_to receive(:sudo).with("rmdir #{mount_guest_path}", anything) end end end ================================================ FILE: test/unit/plugins/guests/freebsd/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestFreeBSD::Cap::RSync" do let(:caps) do VagrantPlugins::GuestFreeBSD::Plugin .components .guest_capabilities[:freebsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_install" do let(:cap) { caps.get(:rsync_install) } it "installs rsync" do comm.expect_command("pkg install -y rsync") cap.rsync_install(machine) end end describe ".rsync_installed" do let(:cap) { caps.get(:rsync_installed) } it "checks if rsync is installed" do comm.expect_command("which rsync") cap.rsync_installed(machine) end end describe ".rsync_command" do let(:cap) { caps.get(:rsync_command) } it "defaults to 'sudo rsync'" do expect(cap.rsync_command(machine)).to eq("sudo rsync") end end end ================================================ FILE: test/unit/plugins/guests/freebsd/cap/shell_expand_guest_path_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestFreeBSD::Cap::ShellExpandGuestPath" do let(:caps) do VagrantPlugins::GuestFreeBSD::Plugin .components .guest_capabilities[:freebsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end describe "#shell_expand_guest_path" do let(:cap) { caps.get(:shell_expand_guest_path) } it "expands the path" do path = "/home/vagrant/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, "/home/vagrant/folder") cap.shell_expand_guest_path(machine, path) end it "raises an exception if no path was detected" do path = "/home/vagrant/folder" expect { cap.shell_expand_guest_path(machine, path) }. to raise_error(Vagrant::Errors::ShellExpandFailed) end it "returns a path with a space in it" do path = "/home/vagrant folder/folder" path_with_spaces = "/home/vagrant\\ folder/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, path_with_spaces) expect(machine.communicate).to receive(:execute) .with("printf #{path_with_spaces}", {:shell=>"sh"}) cap.shell_expand_guest_path(machine, path) end end end ================================================ FILE: test/unit/plugins/guests/gentoo/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestGentoo::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestGentoo::Plugin .components .guest_capabilities[:gentoo] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/systemctl set-hostname '#{name}'/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/systemctl set-hostname '#{name}'/) end end end end ================================================ FILE: test/unit/plugins/guests/haiku/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestHaiku::Cap::RSync" do let(:caps) do VagrantPlugins::GuestHaiku::Plugin .components .guest_capabilities[:haiku] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_install" do let(:cap) { caps.get(:rsync_install) } it "installs rsync" do comm.expect_command("pkgman install -y rsync") cap.rsync_install(machine) end end describe ".rsync_installed" do let(:cap) { caps.get(:rsync_installed) } it "checks if rsync is installed" do comm.expect_command("test -f /bin/rsync") cap.rsync_installed(machine) end end describe ".rsync_command" do let(:cap) { caps.get(:rsync_command) } it "defaults to 'rsync -zz'" do expect(cap.rsync_command(machine)).to eq("rsync -zz") end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostname '#{name}'/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/hostname '#{name}'/) end end context "multiple networks configured with hostname" do it "adds a new entry only for the hostname" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :hostname=>true, :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(described_class).to receive(:replace_host) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) described_class.change_host_name(machine, name) end it "appends an entry to the loopback interface" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(described_class).to_not receive(:replace_host) expect(described_class).to receive(:add_hostname_to_loopback_interface).once described_class.change_host_name(machine, name) end end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/choose_addressable_ip_addr_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::ChooseAddressableIPAddr" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".choose_addressable_ip_addr" do let(:cap) { caps.get(:choose_addressable_ip_addr) } it "returns the first matching IP address" do possible = ["1.2.3.4", "5.6.7.8"] possible.each do |ip| comm.stub_command("ping -c1 -w1 -W1 #{ip}", exit_code: 0) end result = cap.choose_addressable_ip_addr(machine, possible) expect(result).to eq("1.2.3.4") end it "returns nil when there are no matches" do result = cap.choose_addressable_ip_addr(machine, []) expect(result).to be(nil) end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/file_system_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::FileSystem" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine", communicate: comm) } let(:comm) { double("comm") } before { allow(comm).to receive(:execute) } describe ".create_tmp_path" do let(:cap) { caps.get(:create_tmp_path) } let(:opts) { {} } it "should generate path on guest" do expect(comm).to receive(:execute).with(/mktemp/) cap.create_tmp_path(machine, opts) end it "should capture path generated on guest" do expect(comm).to receive(:execute).with(/mktemp/).and_yield(:stdout, "TMP_PATH") expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH") end it "should strip newlines on path" do expect(comm).to receive(:execute).with(/mktemp/).and_yield(:stdout, "TMP_PATH\n") expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH") end context "when type is a directory" do before { opts[:type] = :directory } it "should create guest path as a directory" do expect(comm).to receive(:execute).with(/-d/) cap.create_tmp_path(machine, opts) end end end describe ".decompress_tgz" do let(:cap) { caps.get(:decompress_tgz) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_tgz(machine, comp, dest, opts) } it "should create temporary directory for extraction" do expect(cap).to receive(:create_tmp_path) end it "should extract file with tar" do expect(comm).to receive(:execute).with(/tar/) end it "should extract file to temporary directory" do expect(comm).to receive(:execute).with(/TMP_DIR/) end it "should remove compressed file from guest" do expect(comm).to receive(:execute).with(/rm .*#{comp}/) end it "should remove extraction directory from guest" do expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) end it "should create parent directories for destination" do expect(comm).to receive(:execute).with(/mkdir -p .*to'/) end context "when type is directory" do before { opts[:type] = :directory } it "should create destination directory" do expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) end end end describe ".decompress_zip" do let(:cap) { caps.get(:decompress_zip) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_zip(machine, comp, dest, opts) } it "should create temporary directory for extraction" do expect(cap).to receive(:create_tmp_path) end it "should extract file with zip" do expect(comm).to receive(:execute).with(/zip/) end it "should extract file to temporary directory" do expect(comm).to receive(:execute).with(/TMP_DIR/) end it "should remove compressed file from guest" do expect(comm).to receive(:execute).with(/rm .*#{comp}/) end it "should remove extraction directory from guest" do expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) end it "should create parent directories for destination" do expect(comm).to receive(:execute).with(/mkdir -p .*to'/) end context "when type is directory" do before { opts[:type] = :directory } it "should create destination directory" do expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) end end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::Halt" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } context "systemd not enabled" do before do allow(machine).to receive(:communicate).and_return(comm) allow(comm).to receive(:test).and_return(false) end after do comm.verify_expectations! end describe ".halt" do let(:cap) { caps.get(:halt) } it "runs the shutdown command" do comm.expect_command("shutdown -h now") cap.halt(machine) end it "does not raise an IOError" do comm.stub_command("shutdown -h now", raise: IOError) expect { cap.halt(machine) }.to_not raise_error end it "does not raise a SSHDisconnected" do comm.stub_command("shutdown -h now", raise: Vagrant::Errors::SSHDisconnected) expect { cap.halt(machine) }.to_not raise_error end end context "systemd enabled" do before do allow(machine).to receive(:communicate).and_return(comm) allow(comm).to receive(:test).and_return(true) end after do comm.verify_expectations! end describe ".halt" do let(:cap) { caps.get(:halt) } it "runs the shutdown command" do comm.expect_command("systemctl poweroff") cap.halt(machine) end end end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/insert_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::InsertPublicKey" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".insert_public_key" do let(:cap) { caps.get(:insert_public_key) } it "inserts the public key" do cap.insert_public_key(machine, "ssh-rsa ...") expect(comm.received_commands[0]).to match(/mkdir -p ~\/.ssh/) expect(comm.received_commands[0]).to match(/chmod 0700 ~\/.ssh/) expect(comm.received_commands[0]).to match(/cat '\/tmp\/vagrant-(.+)' >> ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/chmod 0600 ~\/.ssh\/authorized_keys/) end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/mount_nfs_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::MountNFS" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".mount_nfs_folder" do let(:cap) { caps.get(:mount_nfs_folder) } let(:ip) { "1.2.3.4" } let(:hostpath) { "/host" } let(:guestpath) { "/guest" } before do allow(machine).to receive(:guest).and_return( double("capability", capability: guestpath) ) end it "mounts the folder" do folders = { "/vagrant-nfs" => { type: :nfs, guestpath: "/guest", hostpath: "/host", } } cap.mount_nfs_folder(machine, ip, folders) expect(comm.received_commands[0]).to match(/mkdir -p #{guestpath}/) expect(comm.received_commands[1]).to match(/1.2.3.4:#{hostpath} #{guestpath}/) end it "mounts with options" do folders = { "/vagrant-nfs" => { type: :nfs, guestpath: "/guest", hostpath: "/host", nfs_version: 2, nfs_udp: true, } } cap.mount_nfs_folder(machine, ip, folders) expect(comm.received_commands[1]).to match(/mount -o vers=2,udp/) end it "emits an event" do folders = { "/vagrant-nfs" => { type: :nfs, guestpath: "/guest", hostpath: "/host", } } cap.mount_nfs_folder(machine, ip, folders) expect(comm.received_commands[2]).to include( "/sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT=#{guestpath}") end it "escapes host and guest paths" do folders = { "/vagrant-nfs" => { guestpath: "/guest with spaces", hostpath: "/host's", } } cap.mount_nfs_folder(machine, ip, folders) expect(comm.received_commands[1]).to match(/host\\\'s/) expect(comm.received_commands[1]).to match(/guest\\\ with\\\ spaces/) end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/mount_shared_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::MountSharedFolder" do let(:machine) { double("machine") } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:guest) { double("guest") } before do allow(machine).to receive(:guest).and_return(guest) allow(machine).to receive(:communicate).and_return(communicator) allow(guest).to receive(:capability).and_return(nil) end end ================================================ FILE: test/unit/plugins/guests/linux/cap/mount_smb_shared_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::MountSMBSharedFolder" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine", env: env, config: config) } let(:env) { double("env", host: host, ui: Vagrant::UI::Silent.new, data_dir: double("data_dir")) } let(:host) { double("host") } let(:guest) { double("guest") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config", vm: vm) } let(:vm) { double("vm" ) } let(:mount_owner){ "vagrant" } let(:mount_group){ "vagrant" } let(:mount_uid){ "1000" } let(:mount_gid){ "1000" } let(:mount_name){ "vagrant" } let(:mount_guest_path){ "/vagrant" } let(:folder_options) do Vagrant::Plugin::V2::SyncedFolder::Collection[ { owner: mount_owner, group: mount_group, smb_host: "localhost", smb_username: "user", smb_password: "pass", plugin: folder_plugin } ] end let(:folder_plugin) { double("folder_plugin") } let(:cap){ caps.get(:mount_smb_shared_folder) } before do allow(machine).to receive(:communicate).and_return(comm) allow(host).to receive(:capability?).and_return(false) allow(vm).to receive(:allow_fstab_modification).and_return(true) allow(folder_plugin).to receive(:capability).with(:mount_options, mount_name, mount_guest_path, folder_options). and_return(["uid=#{mount_uid},gid=#{mount_gid},sec=ntlmssp,credentials=/etc/smb_creds_id", mount_uid, mount_gid]) allow(folder_plugin).to receive(:capability).with(:mount_type).and_return("cifs") allow(folder_plugin).to receive(:capability).with(:mount_name, mount_name, folder_options).and_return("//localhost/#{mount_name}") end after do comm.verify_expectations! end describe ".mount_smb_shared_folder" do before do allow(comm).to receive(:sudo).with(any_args).and_return(0) allow(comm).to receive(:execute).with(any_args) allow(machine).to receive(:guest).and_return(guest) allow(guest).to receive(:capability).with(:shell_expand_guest_path, mount_guest_path).and_return(mount_guest_path) allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with("VAGRANT_DISABLE_SMBMFSYMLINKS").and_return(false) allow(ENV).to receive(:[]).with("GEM_SKIP").and_return(false) allow(cap).to receive(:display_mfsymlinks_warning) end it "generates the expected default mount command" do expect(comm).to receive(:sudo).with(/mount -t cifs/, anything).and_return(0) cap.mount_smb_shared_folder(machine, mount_name, mount_guest_path, folder_options) end it "creates directory on guest machine" do expect(comm).to receive(:sudo).with("mkdir -p #{mount_guest_path}") cap.mount_smb_shared_folder(machine, mount_name, mount_guest_path, folder_options) end it "writes username into guest credentials file" do expect(comm).to receive(:sudo).with(/smb_creds.*username=#{folder_options[:smb_username]}/m) cap.mount_smb_shared_folder(machine, mount_name, mount_guest_path, folder_options) end it "writes password into guest credentials file" do expect(comm).to receive(:sudo).with(/smb_creds.*password=#{folder_options[:smb_password]}/m) cap.mount_smb_shared_folder(machine, mount_name, mount_guest_path, folder_options) end it "removes the credentials file before completion" do allow(vm).to receive(:allow_fstab_modification).and_return(false) expect(comm).to receive(:sudo).with(/rm.+smb_creds_.+/) cap.mount_smb_shared_folder(machine, mount_name, mount_guest_path, folder_options) end it "sends upstart notification after mount" do expect(comm).to receive(:sudo).with(/emit/) cap.mount_smb_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end describe ".display_mfsymlinks_warning" do let(:gate_file){ double("gate") } before do allow(env.data_dir).to receive(:join).and_return(gate_file) allow(gate_file).to receive(:exist?).and_return(false) allow(gate_file).to receive(:to_path).and_return("PATH") allow(FileUtils).to receive(:touch) end it "should output warning message" do expect(env.ui).to receive(:warn).with(/VAGRANT_DISABLE_SMBMFSYMLINKS=1/) cap.display_mfsymlinks_warning(env) end it "should not output warning message if gate file exists" do allow(gate_file).to receive(:exist?).and_return(true) expect(env.ui).not_to receive(:warn) cap.display_mfsymlinks_warning(env) end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/mount_virtual_box_shared_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::MountVirtualBoxSharedFolder" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:mount_owner){ "vagrant" } let(:mount_group){ "vagrant" } let(:mount_uid){ "1000" } let(:mount_gid){ "1000" } let(:mount_name){ "vagrant" } let(:mount_guest_path){ "/vagrant" } let(:folder_options) do Vagrant::Plugin::V2::SyncedFolder::Collection[ { owner: mount_owner, group: mount_group, hostpath: "/host/directory/path", plugin: folder_plugin } ] end let(:cap){ caps.get(:mount_virtualbox_shared_folder) } let(:folder_plugin) { double("folder_plugin") } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".mount_virtualbox_shared_folder" do before do allow(comm).to receive(:sudo).with(any_args) allow(comm).to receive(:execute).with(any_args) end it "generates the expected default mount command" do expect(folder_plugin).to receive(:capability).with(:mount_options, mount_name, mount_guest_path, folder_options). and_return(["uid=#{mount_uid},gid=#{mount_gid}", mount_uid, mount_gid]) expect(folder_plugin).to receive(:capability).with(:mount_type).and_return("vboxsf") expect(comm).to receive(:sudo).with("mount -t vboxsf -o uid=#{mount_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end it "automatically chown's the mounted directory on guest" do expect(folder_plugin).to receive(:capability).with(:mount_options, mount_name, mount_guest_path, folder_options). and_return(["uid=#{mount_uid},gid=#{mount_gid}", mount_uid, mount_gid]) expect(folder_plugin).to receive(:capability).with(:mount_type).and_return("vboxsf") expect(comm).to receive(:sudo).with("mount -t vboxsf -o uid=#{mount_uid},gid=#{mount_gid} #{mount_name} #{mount_guest_path}", anything) expect(comm).to receive(:sudo).with("chown #{mount_uid}:#{mount_gid} #{mount_guest_path}") cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end context "with upstart init" do it "emits mount event" do expect(comm).to receive(:sudo).with(/initctl emit/) expect(folder_plugin).to receive(:capability).with(:mount_type).and_return("vboxsf") expect(folder_plugin).to receive(:capability).with(:mount_options, mount_name, mount_guest_path, folder_options). and_return(["uid=#{mount_uid},gid=#{mount_gid}", mount_uid, mount_gid]) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end context "with guest builtin vboxsf module" do let(:vbox_stderr){ <<-EOF mount.vboxsf cannot be used with mainline vboxsf; instead use: mount -cit vboxsf NAME MOUNTPOINT EOF } it "should perform guest mount using builtin module" do expect(folder_plugin).to receive(:capability).with(:mount_options, mount_name, mount_guest_path, folder_options). and_return(["uid=#{mount_uid},gid=#{mount_gid}", mount_uid, mount_gid]) expect(folder_plugin).to receive(:capability).with(:mount_type).and_return("vboxsf") expect(comm).to receive(:sudo).with(/mount -t vboxsf/, any_args).and_yield(:stderr, vbox_stderr).and_return(1) expect(comm).to receive(:sudo).with(/mount -cit/, any_args) cap.mount_virtualbox_shared_folder(machine, mount_name, mount_guest_path, folder_options) end end end describe ".unmount_virtualbox_shared_folder" do after { cap.unmount_virtualbox_shared_folder(machine, mount_guest_path, folder_options) } it "unmounts shared directory and deletes directory on guest" do expect(comm).to receive(:sudo).with("umount #{mount_guest_path}", anything).and_return(0) expect(comm).to receive(:sudo).with("rmdir #{mount_guest_path}", anything) end it "does not delete guest directory if unmount fails" do expect(comm).to receive(:sudo).with("umount #{mount_guest_path}", anything).and_return(1) expect(comm).not_to receive(:sudo).with("rmdir #{mount_guest_path}", anything) end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/network_interfaces_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::NetworkInterfaces" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".network_interfaces" do let(:cap){ caps.get(:network_interfaces) } it "sorts discovered classic interfaces" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "eth1\neth2\neth0") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["eth0", "eth1", "eth2"]) end it "sorts discovered classic interfaces and handles trailing newline" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "eth1\neth2\neth0\n") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["eth0", "eth1", "eth2"]) end it "filters out lo interface" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "lo\neth0") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["eth0"]) end it "sorts discovered predictable network interfaces" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "enp0s8\nenp0s3\nenp0s5") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["enp0s3", "enp0s5", "enp0s8"]) end it "sorts discovered classic interfaces naturally" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "eth1\neth2\neth12\neth0\neth10") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["eth0", "eth1", "eth2", "eth10", "eth12"]) end it "sorts discovered predictable network interfaces naturally" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "enp0s8\nenp0s3\nenp0s5\nenp0s10\nenp1s3") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["enp0s3", "enp0s5", "enp0s8", "enp0s10", "enp1s3"]) end it "sorts ethernet devices discovered with classic naming first in list" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "eth1\neth2\ndocker0\nbridge0\neth0") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["eth0", "eth1", "eth2", "bridge0", "docker0"]) end it "sorts ethernet devices discovered with predictable network interfaces naming first in list" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "enp0s8\ndocker0\nenp0s3\nbridge0\nenp0s5") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["enp0s3", "enp0s5", "enp0s8", "bridge0", "docker0"]) end it "sorts ethernet devices discovered with predictable network interfaces naming first in list with less" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "enp0s3\nenp0s8\ndocker0") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["enp0s3", "enp0s8", "docker0"]) end it "does not include ethernet devices aliases within prefix device listing" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "eth1\neth2\ndocker0\nbridge0\neth0\ndocker1\neth0:0") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["eth0", "eth1", "eth2", "bridge0", "docker0", "docker1", "eth0:0"]) end it "does not include ethernet devices aliases within prefix device listing with dot separators" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "eth1\neth2\ndocker0\nbridge0\neth0\ndocker1\neth0.1@eth0") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["eth0", "eth1", "eth2", "bridge0", "docker0", "docker1", "eth0.1@eth0"]) end it "properly sorts non-consistent device name formats" do expect(comm).to receive(:sudo).twice.and_yield(:stdout, "eth0\neth1\ndocker0\nveth437f7f9\nveth06b3e44\nveth8bb7081") expect(comm).to receive(:sudo).and_yield(:stdout, "lo") result = cap.network_interfaces(machine) expect(result).to eq(["eth0", "eth1", "docker0", "veth8bb7081", "veth437f7f9", "veth06b3e44"]) end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/nfs_client_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::NFSClient" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".nfs_client_installed" do let(:cap) { caps.get(:nfs_client_installed) } it "installs nfs client utilities" do comm.expect_command("test -x /sbin/mount.nfs") cap.nfs_client_installed(machine) end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/persist_mount_shared_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::PersistMountSharedFolder" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:options_gid){ '1234' } let(:options_uid){ '1234' } let(:cap){ caps.get(:persist_mount_shared_folder) } let(:folder_plugin){ double("folder_plugin") } let(:ssh_info) {{ :username => "vagrant" }} let (:fstab_folders) { Vagrant::Plugin::V2::SyncedFolder::Collection[ { "test1" => {guestpath: "/test1", hostpath: "/my/host/path", disabled: false, plugin: folder_plugin, __vagrantfile: true, owner: "vagrant", group: "vagrant", mount_options: ["uid=#{options_uid}", "gid=#{options_gid}"]}, "vagrant" => {guestpath: "/vagrant", hostpath: "/my/host/vagrant", disabled: false, __vagrantfile: true, owner: "vagrant", group: "vagrant", mount_options: ["uid=#{options_uid}", "gid=#{options_gid}}"], plugin: folder_plugin} } ] } let (:folders) { { :folder_type => fstab_folders } } let(:expected_mount_options) { "uid=#{options_uid},gid=#{options_gid}" } before do allow(machine).to receive(:communicate).and_return(comm) allow(machine).to receive(:ssh_info).and_return(ssh_info) allow(folder_plugin).to receive(:capability?).with(:mount_name).and_return(false) allow(folder_plugin).to receive(:capability?).with(:mount_type).and_return(true) allow(folder_plugin).to receive(:capability).with(:mount_options, any_args). and_return(["uid=#{options_uid},gid=#{options_gid}", options_uid, options_gid]) allow(folder_plugin).to receive(:capability).with(:mount_type).and_return("vboxsf") allow(cap).to receive(:fstab_exists?).and_return(true) end after do comm.verify_expectations! end describe ".persist_mount_shared_folder" do let(:ui){ Vagrant::UI::Silent.new } before do allow(comm).to receive(:sudo).with(any_args) allow(machine).to receive(:ui).and_return(ui) end it "inserts folders into /etc/fstab" do expected_entry_vagrant = "vagrant /vagrant vboxsf #{expected_mount_options} 0 0" expected_entry_test = "test1 /test1 vboxsf #{expected_mount_options} 0 0" expect(cap).to receive(:remove_vagrant_managed_fstab) expect(comm).to receive(:sudo).with(/#{expected_entry_test}\n#{expected_entry_vagrant}/) cap.persist_mount_shared_folder(machine, folders) end it "does not insert an empty set of folders" do expect(cap).to receive(:remove_vagrant_managed_fstab) cap.persist_mount_shared_folder(machine, nil) end context "folders do not support mount_type capability" do before do allow(folder_plugin).to receive(:capability?).with(:mount_type).and_return(false) end it "does not inserts folders into /etc/fstab" do expect(cap).to receive(:remove_vagrant_managed_fstab) expect(comm).not_to receive(:sudo).with(/echo '' >> \/etc\/fstab/) cap.persist_mount_shared_folder(machine, folders) end end context "fstab does not exist" do before do allow(cap).to receive(:fstab_exists?).and_return(false) # Ensure /etc/fstab is not being modified expect(comm).not_to receive(:sudo).with(/sed -i .? \/etc\/fstab/) end it "creates /etc/fstab" do expect(cap).to receive(:remove_vagrant_managed_fstab) expect(comm).to receive(:sudo).with(/>> \/etc\/fstab/) cap.persist_mount_shared_folder(machine, []) end it "does not remove contents of /etc/fstab" do expect(cap).to receive(:remove_vagrant_managed_fstab) expect(comm).not_to receive(:sudo).with(/echo '' >> \/etc\/fstab/) cap.persist_mount_shared_folder(machine, nil) end end context "smb folder" do let (:fstab_folders) { Vagrant::Plugin::V2::SyncedFolder::Collection[ { "test1" => {guestpath: "/test1", hostpath: "/my/host/path", disabled: false, plugin: folder_plugin, __vagrantfile: true, owner: "vagrant", group: "vagrant", smb_host: "192.168.42.42", smb_id: "vtg-id1" }, "vagrant" => {guestpath: "/vagrant", hostpath: "/my/host/vagrant", disabled: false, plugin: folder_plugin, __vagrantfile: true, owner: "vagrant", group: "vagrant", smb_host: "192.168.42.42", smb_id: "vtg-id2"} } ] } let (:folders) { { :smb => fstab_folders } } context "folder with mount_name cap" do before do allow(folder_plugin).to receive(:capability).with(:mount_type).and_return("cifs") allow(folder_plugin).to receive(:capability?).with(:mount_name).and_return(true) allow(folder_plugin).to receive(:capability).with(:mount_name, instance_of(String), any_args).and_return("//192.168.42.42/dummyname") end it "inserts folders into /etc/fstab" do expected_entry_vagrant = "//192.168.42.42/dummyname /vagrant cifs #{expected_mount_options} 0 0" expected_entry_test = "//192.168.42.42/dummyname /test1 cifs #{expected_mount_options} 0 0" expect(cap).to receive(:remove_vagrant_managed_fstab) expect(comm).to receive(:sudo).with(/#{expected_entry_test}\n#{expected_entry_vagrant}/) cap.persist_mount_shared_folder(machine, folders) end end end end describe ".remove_vagrant_managed_fstab" do let(:fstab_exists) { true } before do allow(comm).to receive(:sudo).with(any_args) allow(cap).to receive(:fstab_exists?).and_return(fstab_exists) end it "removes vagrant managed fstab entries" do expect(cap).to receive(:contains_vagrant_data?).and_return(true) expect(comm).to receive(:sudo).with("sed -i '/#VAGRANT-BEGIN/,/#VAGRANT-END/d' /etc/fstab") cap.remove_vagrant_managed_fstab(machine) end context "fstab does not exist" do let(:fstab_exists) { false } it "does not try to remove fstab entries" do expect(cap).not_to receive(:contains_vagrant_data?) expect(comm).not_to receive(:sudo).with("sed -i '/#VAGRANT-BEGIN/,/#VAGRANT-END/d' /etc/fstab") cap.remove_vagrant_managed_fstab(machine) end end context "fstab does not contain vagrant data" do before do expect(comm).to receive(:test).with("grep '#VAGRANT-BEGIN' /etc/fstab").and_return(false) end it "does not try to remove fstab entries" do expect(comm).not_to receive(:sudo).with("sed -i '/#VAGRANT-BEGIN/,/#VAGRANT-END/d' /etc/fstab") cap.remove_vagrant_managed_fstab(machine) end end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/port_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::Port" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".port_open_check" do let(:cap) { caps.get(:port_open_check) } it "checks if the port is open" do port = 8080 comm.expect_command("nc -z -w2 127.0.0.1 #{port}") cap.port_open_check(machine, port) end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/reboot_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/guests/linux/cap/reboot") describe "VagrantPlugins::GuestLinux::Cap::Reboot" do let(:described_class) do VagrantPlugins::GuestLinux::Plugin.components.guest_capabilities[:linux].get(:wait_for_reboot) end let(:machine) { double("machine", guest: guest) } let(:guest) { double("guest") } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:ui) { Vagrant::UI::Silent.new } context "systemd not enabled" do before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(machine.guest).to receive(:ready?).and_return(true) allow(machine).to receive(:ui).and_return(ui) allow(communicator).to receive(:test).and_return(false) end after do communicator.verify_expectations! end describe ".reboot" do it "reboots the vm" do allow(communicator).to receive(:execute) expect(communicator).to receive(:execute).with(/reboot/, nil).and_return(0) expect(described_class).to receive(:wait_for_reboot) described_class.reboot(machine) end context "user output" do before do allow(communicator).to receive(:execute) allow(described_class).to receive(:wait_for_reboot) end after { described_class.reboot(machine) } it "sends message to user that guest is rebooting" do expect(ui).to receive(:info).and_call_original end end end context "systemd enabled" do before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(machine.guest).to receive(:ready?).and_return(true) allow(machine).to receive(:ui).and_return(ui) allow(communicator).to receive(:test).and_return(true) end after do communicator.verify_expectations! end it "reboots the vm" do allow(communicator).to receive(:execute) expect(communicator).to receive(:execute).with(/systemctl reboot/, nil).and_return(0) expect(described_class).to receive(:wait_for_reboot) described_class.reboot(machine) end end context "reboot configuration" do before do allow(communicator).to receive(:execute) expect(communicator).to receive(:execute).with(/reboot/, nil).and_return(0) allow(described_class).to receive(:sleep).and_return(described_class::WAIT_SLEEP_TIME) allow(described_class).to receive(:wait_for_reboot).and_raise(Vagrant::Errors::MachineGuestNotReady) end context "default retry duration value" do let(:max_retries) { (described_class::DEFAULT_MAX_REBOOT_RETRY_DURATION / described_class::WAIT_SLEEP_TIME) + 2 } it "should receive expected number of wait_for_reboot calls" do expect(described_class).to receive(:wait_for_reboot).exactly(max_retries).times expect { described_class.reboot(machine) }.to raise_error(Vagrant::Errors::MachineGuestNotReady) end end context "with custom retry duration value" do let(:duration) { 10 } let(:max_retries) { (duration / described_class::WAIT_SLEEP_TIME) + 2 } before do expect(ENV).to receive(:fetch).with("VAGRANT_MAX_REBOOT_RETRY_DURATION", anything).and_return(duration) end it "should receive expected number of wait_for_reboot calls" do expect(described_class).to receive(:wait_for_reboot).exactly(max_retries).times expect { described_class.reboot(machine) }.to raise_error(Vagrant::Errors::MachineGuestNotReady) end end end end describe ".wait_for_reboot" do before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute).and_return(0) allow(guest).to receive(:ready?).and_return(false) end context "when guest is ready" do before { expect(guest).to receive(:ready?).and_return(true) } it "should sleep" do expect(described_class).to receive(:sleep).with(10) described_class.wait_for_reboot(machine) end context "when check script fails" do before { expect(communicator).to receive(:execute).with(/grep/, any_args).and_return(1) } it "should not sleep" do expect(described_class).not_to receive(:sleep) described_class.wait_for_reboot(machine) end end context "when communicator raises an error" do let(:error) { Class.new(StandardError) } before do expect(communicator).to receive(:execute).with(/grep/, any_args).and_raise(error) expect(guest).to receive(:ready?).and_return(true) end it "should sleep once for exception and once for the guest being ready" do expect(described_class).to receive(:sleep).with(10).twice described_class.wait_for_reboot(machine) end context "when communicator raises error more than once" do before { expect(communicator).to receive(:execute).with(/grep/, any_args).and_raise(error) } it "should sleep once and raise error" do expect(described_class).to receive(:sleep).with(10) expect { described_class.wait_for_reboot(machine) }.to raise_error(error) end end end end context "when guest is not ready" do before { expect(guest).to receive(:ready?).and_return(false) } it "should not sleep" do expect(described_class).not_to receive(:sleep) described_class.wait_for_reboot(machine) end end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/remove_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::RemovePublicKey" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".remove_public_key" do let(:cap) { caps.get(:remove_public_key) } it "removes the public key" do cap.remove_public_key(machine, "ssh-rsa ...") expect(comm.received_commands[0]).to match(/grep -v -x -f '\/tmp\/vagrant-(.+)' ~\/\.ssh\/authorized_keys > ~\/.ssh\/authorized_keys\.tmp/) expect(comm.received_commands[0]).to match(/mv ~\/.ssh\/authorized_keys\.tmp ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/chmod 0600 ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/rm -f '\/tmp\/vagrant-(.+)'/) end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::Rsync" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:guest_directory){ "/guest/directory/path" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_installed" do let(:cap) { caps.get(:rsync_installed) } it "checks if the command is installed" do comm.expect_command("which rsync") cap.rsync_installed(machine) end end describe ".rsync_command" do let(:cap) { caps.get(:rsync_command) } it "provides the rsync command to use" do expect(cap.rsync_command(machine)).to eq("sudo rsync") end end describe ".rsync_pre" do let(:cap) { caps.get(:rsync_pre) } it "creates target directory on guest" do comm.expect_command("mkdir -p #{guest_directory}") cap.rsync_pre(machine, :guestpath => guest_directory) end end describe ".rsync_post" do let(:cap) { caps.get(:rsync_post) } let(:host_directory){ '.' } let(:owner) { "vagrant-user" } let(:group) { "vagrant-group" } let(:excludes) { false } let(:options) do { hostpath: host_directory, guestpath: guest_directory, owner: owner, group: group, exclude: excludes } end it "chowns files within the guest directory" do comm.expect_command( "find #{guest_directory} '!' -type l -a '(' ! -user #{owner} -or " \ "! -group #{group} ')' -exec chown #{owner}:#{group} '{}' +" ) cap.rsync_post(machine, options) end context "with excludes provided" do let(:excludes){ ["tmp", "state/*", "path/with a/space"] } it "ignores files that are excluded" do # comm.expect_command( # "find #{guest_directory} -path #{Shellwords.escape(File.join(guest_directory, excludes.first))} -prune -o " \ # "-path #{Shellwords.escape(File.join(guest_directory, excludes.last))} -prune -o '!' " \ # "-path -type l -a '(' ! -user " \ # "#{owner} -or ! -group #{group} ')' -exec chown #{owner}:#{group} '{}' +" # ) cap.rsync_post(machine, options) excludes.each do |ex_path| expect(comm.received_commands.first).to include("-path #{Shellwords.escape(File.join(guest_directory, ex_path))} -prune") end end it "properly escapes excluded directories" do cap.rsync_post(machine, options) exclude_with_space = excludes.detect{|ex| ex.include?(' ')} escaped_exclude_with_space = Shellwords.escape(exclude_with_space) expect(comm.received_commands.first).not_to include(exclude_with_space) expect(comm.received_commands.first).to include(escaped_exclude_with_space) end end end end ================================================ FILE: test/unit/plugins/guests/linux/cap/shell_expand_guest_path_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestLinux::Cap::ShellExpandGuestPath" do let(:caps) do VagrantPlugins::GuestLinux::Plugin .components .guest_capabilities[:linux] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end describe "#shell_expand_guest_path" do let(:cap) { caps.get(:shell_expand_guest_path) } it "expands the path" do path = "/home/vagrant/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, "/home/vagrant/folder") cap.shell_expand_guest_path(machine, path) end it "expands a path with tilde" do path = "~/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, "/home/vagrant/folder") cap.shell_expand_guest_path(machine, path) end it "raises an exception if no path was detected" do path = "/home/vagrant/folder" expect { cap.shell_expand_guest_path(machine, path) }. to raise_error(Vagrant::Errors::ShellExpandFailed) end it "returns a path with a space in it" do path = "/home/vagrant folder/folder" path_with_spaces = "/home/vagrant\\ folder/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, path_with_spaces) expect(machine.communicate).to receive(:execute).with("echo; printf #{path_with_spaces}") cap.shell_expand_guest_path(machine, path) end end end ================================================ FILE: test/unit/plugins/guests/netbsd/cap/shell_expand_guest_path_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestNetBSD::Cap::ShellExpandGuestPath" do let(:caps) do VagrantPlugins::GuestNetBSD::Plugin .components .guest_capabilities[:netbsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end describe "#shell_expand_guest_path" do let(:cap) { caps.get(:shell_expand_guest_path) } it "expands the path" do path = "/home/vagrant/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, "/home/vagrant/folder") cap.shell_expand_guest_path(machine, path) end it "raises an exception if no path was detected" do path = "/home/vagrant/folder" expect { cap.shell_expand_guest_path(machine, path) }. to raise_error(Vagrant::Errors::ShellExpandFailed) end it "returns a path with a space in it" do path = "/home/vagrant folder/folder" path_with_spaces = "/home/vagrant\\ folder/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, path_with_spaces) expect(machine.communicate).to receive(:execute).with("printf #{path_with_spaces}") cap.shell_expand_guest_path(machine, path) end end end ================================================ FILE: test/unit/plugins/guests/omnios/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOmniOS::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestOmniOS::Plugin .components .guest_capabilities[:omnios] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostname '#{name}'/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/hostname '#{name}'/) end end end end ================================================ FILE: test/unit/plugins/guests/omnios/cap/mount_nfs_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOmniOS::Cap:RSync" do let(:caps) do VagrantPlugins::GuestOmniOS::Plugin .components .guest_capabilities[:omnios] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".mount_nfs_folder" do let(:cap) { caps.get(:mount_nfs_folder) } let(:ip) { "1.2.3.4" } let(:hostpath) { "/host" } let(:guestpath) { "/guest" } it "mounts the folder" do folders = { "/vagrant-nfs" => { type: :nfs, guestpath: "/guest", hostpath: "/host", } } cap.mount_nfs_folder(machine, ip, folders) expect(comm.received_commands[0]).to match(/mkdir -p '#{guestpath}'/) expect(comm.received_commands[0]).to match(/'1.2.3.4:#{hostpath}' '#{guestpath}'/) end it "mounts with options" end end ================================================ FILE: test/unit/plugins/guests/omnios/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOmniOS::Cap:RSync" do let(:caps) do VagrantPlugins::GuestOmniOS::Plugin .components .guest_capabilities[:omnios] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_install" do let(:cap) { caps.get(:rsync_install) } it "installs rsync" do cap.rsync_install(machine) expect(comm.received_commands[0]).to match(/pkg install rsync/) end end end ================================================ FILE: test/unit/plugins/guests/openbsd/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOpenBSD::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestOpenBSD::Plugin .components .guest_capabilities[:openbsd] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } let(:networks) {} before do allow(machine).to receive(:communicate).and_return(comm) allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[1]).to match(/hostname '#{name}'/) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/hostname '#{name}'/) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) end end context "multiple networks configured with hostname" do it "adds a new entry only for the hostname" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :hostname=>true, :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(described_class).to receive(:replace_host) expect(described_class).to_not receive(:add_hostname_to_loopback_interface) described_class.change_host_name(machine, name) end it "appends an entry to the loopback interface" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(described_class).to_not receive(:replace_host) expect(described_class).to receive(:add_hostname_to_loopback_interface).once described_class.change_host_name(machine, name) end end end end ================================================ FILE: test/unit/plugins/guests/openbsd/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOpenBSD::Cap::Halt" do let(:caps) do VagrantPlugins::GuestOpenBSD::Plugin .components .guest_capabilities[:openbsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".halt" do let(:cap) { caps.get(:halt) } it "runs the shutdown command" do comm.expect_command("/sbin/shutdown -p -h now") cap.halt(machine) end it "ignores an IOError" do comm.stub_command("/sbin/shutdown -p -h now", raise: IOError) expect { cap.halt(machine) }.to_not raise_error end it "ignores a Vagrant::Errors::SSHDisconnected" do comm.stub_command("/sbin/shutdown -p -h now", raise: Vagrant::Errors::SSHDisconnected) expect { cap.halt(machine) }.to_not raise_error end end end ================================================ FILE: test/unit/plugins/guests/openbsd/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOpenBSD::Cap::RSync" do let(:caps) do VagrantPlugins::GuestOpenBSD::Plugin .components .guest_capabilities[:openbsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_install" do let(:cap) { caps.get(:rsync_install) } describe "successful installation" do it "installs rsync" do cap.rsync_install(machine) expect(comm.received_commands[0]).to match(/pkg_add -I rsync/) expect(comm.received_commands[1]).to match(/pkg_info/) end end describe "failure installation" do before do expect(comm).to receive(:execute).and_raise(Vagrant::Errors::RSyncNotInstalledInGuest, {command: '', output: ''}) end it "raises custom exception" do expect{ cap.rsync_install(machine) }.to raise_error(Vagrant::Errors::RSyncNotInstalledInGuest) end end end describe ".rsync_installed" do let(:cap) { caps.get(:rsync_installed) } it "checks if rsync is installed" do comm.expect_command("which rsync") cap.rsync_installed(machine) end end describe ".rsync_command" do let(:cap) { caps.get(:rsync_command) } it "defaults to 'sudo rsync'" do expect(cap.rsync_command(machine)).to eq("sudo rsync") end end end ================================================ FILE: test/unit/plugins/guests/openbsd/cap/shell_expand_guest_path_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOpenBSD::Cap::ShellExpandGuestPath" do let(:caps) do VagrantPlugins::GuestOpenBSD::Plugin .components .guest_capabilities[:openbsd] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end describe "#shell_expand_guest_path" do let(:cap) { caps.get(:shell_expand_guest_path) } it "expands the path" do path = "/home/vagrant/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, "/home/vagrant/folder") cap.shell_expand_guest_path(machine, path) end it "raises an exception if no path was detected" do path = "/home/vagrant/folder" expect { cap.shell_expand_guest_path(machine, path) }. to raise_error(Vagrant::Errors::ShellExpandFailed) end it "returns a path with a space in it" do path = "/home/vagrant folder/folder" path_with_spaces = "/home/vagrant\\ folder/folder" allow(machine.communicate).to receive(:execute). with(any_args).and_yield(:stdout, path_with_spaces) expect(machine.communicate).to receive(:execute).with("printf #{path_with_spaces}") cap.shell_expand_guest_path(machine, path) end end end ================================================ FILE: test/unit/plugins/guests/openwrt/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOpenWrt::Cap::ChangeHostName" do let(:caps) do VagrantPlugins::GuestOpenWrt::Plugin .components .guest_capabilities[:openwrt] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do let(:cap) { caps.get(:change_host_name) } it "changes the hostname if appropriate" do cap.change_host_name(machine, "testhost") expect(comm.received_commands[0]).to match(/uci get system\.@system\[0\].hostname | grep '^testhost$'/) expect(comm.received_commands[1]).to match(/uci set system.@system\[0\].hostname='testhost'/) expect(comm.received_commands[1]).to match(/uci commit system/) expect(comm.received_commands[1]).to match(/\/etc\/init.d\/system reload/) end end end ================================================ FILE: test/unit/plugins/guests/openwrt/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOpenWrt::Cap::Halt" do let(:plugin) { VagrantPlugins::GuestOpenWrt::Plugin.components.guest_capabilities[:openwrt].get(:halt) } let(:machine) { double("machine") } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:shutdown_command){ "halt" } before do allow(machine).to receive(:communicate).and_return(communicator) end after do communicator.verify_expectations! end describe ".halt" do it "sends a shutdown signal" do communicator.expect_command(shutdown_command) plugin.halt(machine) end it "ignores an IOError" do communicator.stub_command(shutdown_command, raise: IOError) expect { plugin.halt(machine) }.to_not raise_error end it "ignores a Vagrant::Errors::SSHDisconnected" do communicator.stub_command(shutdown_command, raise: Vagrant::Errors::SSHDisconnected) expect { plugin.halt(machine) }.to_not raise_error end end end ================================================ FILE: test/unit/plugins/guests/openwrt/cap/insert_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOpenWrt::Cap::InsertPublicKey" do let(:caps) do VagrantPlugins::GuestOpenWrt::Plugin .components .guest_capabilities[:openwrt] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".insert_public_key" do let(:cap) { caps.get(:insert_public_key) } it "inserts the public key" do cap.insert_public_key(machine, "ssh-rsa ...") expect(comm.received_commands[0]).to match(/printf 'ssh-rsa ...\\n' >> \/etc\/dropbear\/authorized_keys/) end end end ================================================ FILE: test/unit/plugins/guests/openwrt/cap/remove_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestOpenWrt::Cap::RemovePublicKey" do let(:caps) do VagrantPlugins::GuestOpenWrt::Plugin .components .guest_capabilities[:openwrt] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".remove_public_key" do let(:cap) { caps.get(:remove_public_key) } it "removes the public key" do cap.remove_public_key(machine, "ssh-rsa keyvalue comment") expect(comm.received_commands[0]).to match(/if test -f \/etc\/dropbear\/authorized_keys ; then/) expect(comm.received_commands[0]).to match(/sed -i '\/\^.*ssh-rsa keyvalue comment.*\$\/d' \/etc\/dropbear\/authorized_keys/) expect(comm.received_commands[0]).to match(/fi/) end end end ================================================ FILE: test/unit/plugins/guests/openwrt/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::VagrantPlugins::Cap::Rsync" do let(:caps) do VagrantPlugins::GuestOpenWrt::Plugin .components .guest_capabilities[:openwrt] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:guest_directory) { "/guest/directory/path" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_installed" do let(:cap) { caps.get(:rsync_installed) } describe "when rsync is in the path" do it "is true" do comm.stub_command("which rsync", stdout: '/usr/bin/rsync', exit_code: 0) expect(cap.rsync_installed(machine)).to be true end end describe "when rsync is not in the path" do it "is false" do comm.stub_command("which rsync", stdout: '', exit_code: 1) expect(cap.rsync_installed(machine)).to be false end end end describe ".rsync_install" do let(:cap) { caps.get(:rsync_install) } it "installs rsync" do cap.rsync_install(machine) expect(comm.received_commands[0]).to match(/opkg update/) expect(comm.received_commands[0]).to match(/opkg install rsync/) end end describe ".rsync_command" do let(:cap) { caps.get(:rsync_command) } it "provides the rsync command to use" do expect(cap.rsync_command(machine)).to eq("rsync -zz") end end describe ".rsync_pre" do let(:cap) { caps.get(:rsync_pre) } it "creates target directory on guest" do cap.rsync_pre(machine, :guestpath => guest_directory) expect(comm.received_commands[0]).to match(/mkdir -p '\/guest\/directory\/path'/) end end describe ".rsync_post" do let(:cap) { caps.get(:rsync_post) } it "is a no-op" do cap.rsync_post(machine, {}) expect(comm).to_not receive(:execute) end end end ================================================ FILE: test/unit/plugins/guests/openwrt/guest_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 ================================================ FILE: test/unit/plugins/guests/photon/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestPhoton::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestPhoton::Plugin .components .guest_capabilities[:photon] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostname '#{name}'/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/hostname '#{name}'/) end end end end ================================================ FILE: test/unit/plugins/guests/photon/cap/configure_networks_test.rb ================================================ # Copyright (c) 2015 VMware, Inc. All Rights Reserved. require_relative "../../../../base" describe "VagrantPlugins::GuestPhoton::Cap:ConfigureNetworks" do let(:caps) do VagrantPlugins::GuestPhoton::Plugin .components .guest_capabilities[:photon] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) comm.stub_command("ifconfig | grep 'eth' | cut -f1 -d' '", stdout: "eth1\neth2") end after do comm.verify_expectations! end describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end it "creates and starts the networks" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[1]).to match(/ifconfig eth1/) expect(comm.received_commands[1]).to match(/ifconfig eth2 33.33.33.10 netmask 255.255.0.0/) end end end ================================================ FILE: test/unit/plugins/guests/photon/cap/docker_test.rb ================================================ # Copyright (c) 2015 VMware, Inc. All Rights Reserved. require_relative "../../../../base" describe "VagrantPlugins::GuestPhoton::Cap:Docker" do let(:caps) do VagrantPlugins::GuestPhoton::Plugin .components .guest_capabilities[:photon] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".docker_daemon_running" do let(:cap) { caps.get(:docker_daemon_running) } it "installs rsync" do comm.expect_command("test -S /run/docker.sock") cap.docker_daemon_running(machine) end end end ================================================ FILE: test/unit/plugins/guests/pld/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestPld::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestPld::Plugin .components .guest_capabilities[:pld] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[-1]).to match(/service network restart/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/service network restart/) end end end end ================================================ FILE: test/unit/plugins/guests/pld/cap/flavor_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestPld::Cap::Flavor" do let(:caps) do VagrantPlugins::GuestPld::Plugin .components .guest_capabilities[:pld] end let(:machine) { double("machine") } describe ".flavor" do let(:cap) { caps.get(:flavor) } let(:name) { "banana-rama.example.com" } it "is pld" do expect(cap.flavor(machine)).to be(:pld) end end end ================================================ FILE: test/unit/plugins/guests/pld/cap/network_scripts_dir_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestPld::Cap::NetworkScriptsDir" do let(:caps) do VagrantPlugins::GuestPld::Plugin .components .guest_capabilities[:pld] end let(:machine) { double("machine") } describe ".network_scripts_dir" do let(:cap) { caps.get(:network_scripts_dir) } let(:name) { "banana-rama.example.com" } it "is /etc/sysconfig/interfaces" do expect(cap.network_scripts_dir(machine)).to eq("/etc/sysconfig/interfaces") end end end ================================================ FILE: test/unit/plugins/guests/redhat/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestRedHat::Cap::ChangeHostName" do let(:caps) do VagrantPlugins::GuestRedHat::Plugin .components .guest_capabilities[:redhat] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do let(:cap) { caps.get(:change_host_name) } let(:name) { "banana-rama.example.com" } let(:hostname_changed) { true } let(:systemd) { true } let(:hostnamectl) { true } let(:networkd) { true } let(:network_manager) { false } let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: hostname_changed ? 1 : 0) allow(cap).to receive(:systemd?).and_return(systemd) allow(cap).to receive(:hostnamectl?).and_return(hostnamectl) allow(cap).to receive(:systemd_networkd?).and_return(networkd) allow(cap).to receive(:systemd_controlled?).with(anything, /NetworkManager/).and_return(network_manager) allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end context "minimal network config" do it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) cap.change_host_name(machine, name) expect(comm.received_commands[1]).to match(/\/etc\/sysconfig\/network/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) cap.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/\/etc\/sysconfig\/network/) end end context "multiple networks configured with hostname" do it "adds a new entry only for the hostname" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :hostname=>true, :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(cap).to receive(:replace_host) expect(cap).to_not receive(:add_hostname_to_loopback_interface) cap.change_host_name(machine, name) end it "appends an entry to the loopback interface" do networks = [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}], [:public_network, {:ip=>"192.168.0.1", :protocol=>"tcp", :id=>"93a4ad88-0774-4127-a161-ceb715ff372f"}], [:public_network, {:ip=>"192.168.0.2", :protocol=>"tcp", :id=>"5aebe848-7d85-4425-8911-c2003d924120"}] ] allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) expect(cap).to_not receive(:replace_host) expect(cap).to receive(:add_hostname_to_loopback_interface).once cap.change_host_name(machine, name) end end context "when hostnamectl is in use" do let(:hostnamectl) { true } it "sets hostname with hostnamectl" do cap.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostnamectl/) end end context "when hostnamectl is not in use" do let(:hostnamectl) { false } it "sets hostname with hostname command" do cap.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostname -F/) end end context "when host name is already set" do let(:hostname_changed) { false } it "does not change the hostname" do cap.change_host_name(machine, name) expect(comm.received_commands.size).to eq(2) end end context "restarts the network" do context "when networkd is in use" do let(:networkd) { true } it "restarts networkd with systemctl" do cap.change_host_name(machine, name) expect(comm.received_commands[3]).to match(/systemctl restart systemd-networkd/) end end context "when NetworkManager is in use" do let(:networkd) { false } let(:network_manager) { true } it "restarts NetworkManager with systemctl" do cap.change_host_name(machine, name) expect(comm.received_commands[3]).to match(/systemctl restart NetworkManager/) end end context "when networkd and NetworkManager are not in use" do let(:networkd) { false } let(:network_manager) { false } it "restarts the network using service" do cap.change_host_name(machine, name) expect(comm.received_commands[3]).to match(/service network restart/) end end context "when systemd is not in use" do let(:systemd) { false } it "restarts the network using service" do cap.change_host_name(machine, name) expect(comm.received_commands[3]).to match(/service network restart/) end end end end end ================================================ FILE: test/unit/plugins/guests/redhat/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestRedHat::Cap::ConfigureNetworks" do let(:caps) do VagrantPlugins::GuestRedHat::Plugin .components .guest_capabilities[:redhat] end let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config", vm: vm) } let(:guest) { double("guest") } let(:machine) { double("machine", guest: guest, config: config) } let(:networks){ [[:public_network, network_1], [:private_network, network_2]] } let(:vm){ double("vm", networks: networks) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end context "with systems-connections network configuration path" do let(:cap) { caps.get(:configure_networks) } let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end before do allow(guest).to receive(:capability) .with(:flavor) .and_return(:rhel) allow(guest).to receive(:capability) .with(:network_scripts_dir) .and_return("/system-connections") end it "should configure with network manager" do expect(cap).to receive(:configure_network_manager).with(machine, [network_1]) cap.configure_networks(machine, [network_1]) end end describe ".configure_networks" do context "when version is less than 10" do let(:cap) { caps.get(:configure_networks) } before do allow(guest).to receive(:capability) .with(:flavor) .and_return(:rhel) allow(guest).to receive(:capability) .with(:network_scripts_dir) .and_return("/network-scripts") allow(guest).to receive(:capability) .with(:network_interfaces) .and_return(["eth1", "eth2"]) end let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end let(:network_3) do { interface: 2, type: "static", ip: "33.33.33.11", netmask: "255.255.0.0", gateway: "33.33.0.1", } end context "network configuration file" do let(:networks){ [[:public_network, network_1], [:private_network, network_2], [:private_network, network_3]] } let(:tempfile) { double("tempfile") } before do allow(tempfile).to receive(:binmode) allow(tempfile).to receive(:write) allow(tempfile).to receive(:fsync) allow(tempfile).to receive(:close) allow(tempfile).to receive(:path) allow(Tempfile).to receive(:open).and_yield(tempfile) end it "should generate two configuration files" do expect(Tempfile).to receive(:open).twice cap.configure_networks(machine, [network_1, network_2]) end it "should generate three configuration files" do expect(Tempfile).to receive(:open).thrice cap.configure_networks(machine, [network_1, network_2, network_3]) end it "should generate configuration with network_2 IP address" do expect(tempfile).to receive(:write).with(/#{network_2[:ip]}/) cap.configure_networks(machine, [network_1, network_2, network_3]) end it "should generate configuration with network_3 IP address" do expect(tempfile).to receive(:write).with(/#{network_3[:ip]}/) cap.configure_networks(machine, [network_1, network_2, network_3]) end end context "with NetworkManager installed" do let(:net1_nm_controlled) { true } let(:net2_nm_controlled) { true } let(:networks){ [ [:public_network, network_1.merge(nm_controlled: net1_nm_controlled)], [:private_network, network_2.merge(nm_controlled: net2_nm_controlled)] ] } before do allow(cap).to receive(:nmcli?).and_return true end context "with devices managed by NetworkManager" do before do allow(cap).to receive(:nm_controlled?).and_return true end context "with nm_controlled option omitted" do let(:networks){ [ [:public_network, network_1], [:private_network, network_2] ] } it "creates and starts the networks via nmcli" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/nmcli/) expect(comm.received_commands[0]).to_not match(/(ifdown|ifup)/) end end context "with nm_controlled option set to true" do it "creates and starts the networks via nmcli" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/nmcli/) expect(comm.received_commands[0]).to_not match(/(ifdown|ifup)/) end end context "with nm_controlled option set to false" do let(:net1_nm_controlled) { false } let(:net2_nm_controlled) { false } it "creates and starts the networks via ifup and disables devices in NetworkManager" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/nmcli.*disconnect/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to_not match(/ifdown/) end end context "with nm_controlled option set to false on first device" do let(:net1_nm_controlled) { false } let(:net2_nm_controlled) { true } it "creates and starts the networks with one managed manually and one NetworkManager controlled" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/nmcli.*disconnect.*eth1/) expect(comm.received_commands[0]).to match(/ifup.*eth1/) expect(comm.received_commands[0]).to_not match(/ifdown/) end end end context "with devices not managed by NetworkManager" do before do allow(cap).to receive(:nm_controlled?).and_return false end context "with nm_controlled option omitted" do let(:networks){ [ [:public_network, network_1], [:private_network, network_2] ] } it "creates and starts the networks manually" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to_not match(/nmcli c up/) expect(comm.received_commands[0]).to_not match(/nmcli d disconnect/) end end context "with nm_controlled option set to true" do let(:net1_nm_controlled) { true } let(:net2_nm_controlled) { true } it "creates and starts the networks via nmcli" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/NetworkManager/) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to_not match(/ifup/) end end context "with nm_controlled option set to false" do let(:net1_nm_controlled) { false } let(:net2_nm_controlled) { false } it "creates and starts the networks via ifup " do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to_not match(/nmcli c up/) expect(comm.received_commands[0]).to_not match(/nmcli d disconnect/) end end context "with nm_controlled option set to false on first device" do let(:net1_nm_controlled) { false } let(:net2_nm_controlled) { true } it "creates and starts the networks with one managed manually and one NetworkManager controlled" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to_not match(/nmcli.*disconnect/) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/ifup.*eth1/) end end end end context "without NetworkManager installed" do before do allow(cap).to receive(:nmcli?).and_return false end context "with nm_controlled option omitted" do it "creates and starts the networks manually" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/ifdown/) expect(comm.received_commands[0]).to match(/ifup/) expect(comm.received_commands[0]).to_not match(/nmcli/) end end context "with nm_controlled option set" do let(:networks){ [ [:public_network, network_1.merge(nm_controlled: true)], [:private_network, network_2.merge(nm_controlled: true)] ] } it "raises an error" do expect{ cap.configure_networks(machine, [network_1, network_2]) }.to raise_error(Vagrant::Errors::NetworkManagerNotInstalled) end end end end end end ================================================ FILE: test/unit/plugins/guests/redhat/cap/flavor_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestRedHat::Cap::Flavor" do let(:caps) do VagrantPlugins::GuestRedHat::Plugin .components .guest_capabilities[:redhat] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".flavor" do let(:cap) { caps.get(:flavor) } # /etc/os-release was added in EL7+ context "without /etc/os-release file" do { "" => :rhel }.each do |str, expected| it "returns #{expected} for #{str}" do comm.stub_command("test -f /etc/os-release", exit_code: 1) expect(cap.flavor(machine)).to be(expected) end end end context "with /etc/os-release file" do { "7" => :rhel_7, "8" => :rhel_8, "9.0" => :rhel_9, "9.1" => :rhel_9, "" => :rhel, "banana" => :rhel, }.each do |str, expected| it "returns #{expected} for #{str}" do comm.stub_command("test -f /etc/os-release", exit_code: 0) comm.stub_command("source /etc/os-release && printf $VERSION_ID", stdout: str) expect(cap.flavor(machine)).to be(expected) end end end end end ================================================ FILE: test/unit/plugins/guests/redhat/cap/network_scripts_dir_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestRedHat::Cap::NetworkScriptsDir" do let(:caps) do VagrantPlugins::GuestRedHat::Plugin .components .guest_capabilities[:redhat] end let(:is_legacy) { false } let(:communicator) { double("communicator") } let(:machine) { double("machine", communicate: communicator) } before do allow(communicator).to receive(:test).with("test -d /etc/sysconfig/network-scripts").and_return(is_legacy) end describe ".network_scripts_dir" do let(:cap) { caps.get(:network_scripts_dir) } let(:name) { "banana-rama.example.com" } it "is /etc/NetworkManager/system-connections" do expect(cap.network_scripts_dir(machine)).to eq("/etc/NetworkManager/system-connections") end context 'when version is legacy' do let(:is_legacy) { true } it "is /etc/sysconfig/network-scripts" do expect(cap.network_scripts_dir(machine)).to eq("/etc/sysconfig/network-scripts") end end end end ================================================ FILE: test/unit/plugins/guests/redhat/cap/nfs_client_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestRedHat::Cap:NFSClient" do let(:caps) do VagrantPlugins::GuestRedHat::Plugin .components .guest_capabilities[:redhat] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".nfs_client_install" do let(:cap) { caps.get(:nfs_client_install) } it "installs nfs client" do cap.nfs_client_install(machine) expect(comm.received_commands[0]).to match(/install nfs-utils/) expect(comm.received_commands[0]).to match(/\/bin\/systemctl restart rpcbind nfs-server/) end end end ================================================ FILE: test/unit/plugins/guests/redhat/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestRedHat::Cap:RSync" do let(:caps) do VagrantPlugins::GuestRedHat::Plugin .components .guest_capabilities[:redhat] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_install" do let(:cap) { caps.get(:rsync_install) } it "installs rsync" do cap.rsync_install(machine) expect(comm.received_commands[0]).to match(/install rsync/) end end end ================================================ FILE: test/unit/plugins/guests/redhat/cap/smb_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestRedHat::Cap::SMB" do let(:described_class) do VagrantPlugins::GuestRedHat::Plugin .components .guest_capabilities[:redhat] .get(:smb_install) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".smb_install" do it "installs smb when /sbin/mount.cifs does not exist" do comm.stub_command("test -f /sbin/mount.cifs", exit_code: 1) described_class.smb_install(machine) expect(comm.received_commands[1]).to match(/if command -v dnf; then/) expect(comm.received_commands[1]).to match(/dnf -y install cifs-utils/) end it "does not install smb when /sbin/mount.cifs exists" do comm.stub_command("test -f /sbin/mount.cifs", exit_code: 0) described_class.smb_install(machine) expect(comm.received_commands.join("")).to_not match(/update/) end end end ================================================ FILE: test/unit/plugins/guests/rocky/cap/flavor_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestRocky::Cap::Flavor" do let(:caps) do VagrantPlugins::GuestRocky::Plugin .components .guest_capabilities[:rocky] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".flavor" do let(:cap) { caps.get(:flavor) } { "" => :rocky, "8.2" => :rocky_8, "9" => :rocky_9, "invalid" => :rocky }.each do |str, expected| it "returns #{expected} for #{str}" do comm.stub_command("source /etc/os-release && printf $VERSION_ID", stdout: str) expect(cap.flavor(machine)).to be(expected) end end end end ================================================ FILE: test/unit/plugins/guests/slackware/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSlackware::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestSlackware::Plugin .components .guest_capabilities[:slackware] .get(:change_host_name) end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) described_class.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/hostname -F \/etc\/hostname/) end it "does not change the hostname if already set" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 0) described_class.change_host_name(machine, name) expect(comm).to_not receive(:sudo).with(/hostname -F \/etc\/hostname/) end end end end ================================================ FILE: test/unit/plugins/guests/slackware/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSlackware::Cap::ConfigureNetworks" do let(:caps) do VagrantPlugins::GuestSlackware::Plugin .components .guest_capabilities[:slackware] end let(:guest) { double("guest") } let(:machine) { double("machine", guest: guest) } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } before do allow(guest).to receive(:capability).with(:network_interfaces) .and_return(["eth1", "eth2"]) end let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end it "creates and starts the networks" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/\/etc\/rc.d\/rc.inet1/) end end end ================================================ FILE: test/unit/plugins/guests/smartos/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSmartos::Cap::ChangeHostName" do let(:caps) do VagrantPlugins::GuestSmartos::Plugin .components .guest_capabilities[:smartos] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config", smartos: VagrantPlugins::GuestSmartos::Config.new) } before do allow(machine).to receive(:communicate).and_return(comm) allow(machine).to receive(:config).and_return(config) end after do comm.verify_expectations! end describe ".change_host_name" do let(:cap) { caps.get(:change_host_name) } it "changes the hostname if appropriate" do cap.change_host_name(machine, "testhost") expect(comm.received_commands[0]).to match(/if hostname | grep 'testhost' ; then/) expect(comm.received_commands[0]).to match(/exit 0/) expect(comm.received_commands[0]).to match(/fi/) expect(comm.received_commands[0]).to match(/if \[ -d \/usbkey \] && \[ "\$\(zonename\)" == "global" \] ; then/) expect(comm.received_commands[0]).to match(/pfexec sed -i '' 's\/hostname=\.\*\/hostname=testhost\/' \/usbkey\/config/) expect(comm.received_commands[0]).to match(/fi/) expect(comm.received_commands[0]).to match(/pfexec echo 'testhost' > \/etc\/nodename/) expect(comm.received_commands[0]).to match(/pfexec hostname testhost/) end end end ================================================ FILE: test/unit/plugins/guests/smartos/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::VagrantPlugins::Cap::ConfigureNetworks" do let(:plugin) { VagrantPlugins::GuestSmartos::Plugin.components.guest_capabilities[:smartos].get(:configure_networks) } let(:machine) { double("machine") } let(:config) { double("config", smartos: VagrantPlugins::GuestSmartos::Config.new) } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:config).and_return(config) end after do communicator.verify_expectations! end describe ".configure_networks" do let(:interface) { "eth0" } let(:device) { "e1000g#{interface}" } describe 'dhcp' do let(:network) { {interface: interface, type: :dhcp} } it "plumbs the device" do communicator.expect_command(%Q(pfexec /sbin/ifconfig #{device} plumb)) plugin.configure_networks(machine, [network]) end it "starts dhcp for the device" do communicator.expect_command(%Q(pfexec /sbin/ifconfig #{device} dhcp start)) plugin.configure_networks(machine, [network]) end end describe 'static' do let(:network) { {interface: interface, type: :static, ip: '1.1.1.1', netmask: '255.255.255.0'} } it "plumbs the network" do communicator.expect_command(%Q(pfexec /sbin/ifconfig #{device} plumb)) plugin.configure_networks(machine, [network]) end it "starts sets netmask and IP for the device" do communicator.expect_command(%Q(pfexec /sbin/ifconfig #{device} inet 1.1.1.1 netmask 255.255.255.0)) plugin.configure_networks(machine, [network]) end it "starts enables the device" do communicator.expect_command(%Q(pfexec /sbin/ifconfig #{device} up)) plugin.configure_networks(machine, [network]) end it "starts writes out a hostname file" do communicator.expect_command(%Q(pfexec sh -c "echo '1.1.1.1' > /etc/hostname.#{device}")) plugin.configure_networks(machine, [network]) end end end end ================================================ FILE: test/unit/plugins/guests/smartos/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSmartos::Cap::Halt" do let(:plugin) { VagrantPlugins::GuestSmartos::Plugin.components.guest_capabilities[:smartos].get(:halt) } let(:machine) { double("machine") } let(:config) { double("config", smartos: double("smartos", suexec_cmd: 'pfexec')) } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:shutdown_command){ "pfexec /usr/sbin/poweroff" } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:config).and_return(config) end after do communicator.verify_expectations! end describe ".halt" do it "sends a shutdown signal" do communicator.expect_command(shutdown_command) plugin.halt(machine) end it "ignores an IOError" do communicator.stub_command(shutdown_command, raise: IOError) expect { plugin.halt(machine) }.to_not raise_error end it "ignores a Vagrant::Errors::SSHDisconnected" do communicator.stub_command(shutdown_command, raise: Vagrant::Errors::SSHDisconnected) expect { plugin.halt(machine) }.to_not raise_error end end end ================================================ FILE: test/unit/plugins/guests/smartos/cap/insert_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSmartos::Cap::InsertPublicKey" do let(:caps) do VagrantPlugins::GuestSmartos::Plugin .components .guest_capabilities[:smartos] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".insert_public_key" do let(:cap) { caps.get(:insert_public_key) } it "inserts the public key" do cap.insert_public_key(machine, "ssh-rsa ...") expect(comm.received_commands[0]).to match(/if \[ -d \/usbkey \] && \[ "\$\(zonename\)" == "global" \] ; then/) expect(comm.received_commands[0]).to match(/printf 'ssh-rsa ...\\n' >> \/usbkey\/config.inc\/authorized_keys/) expect(comm.received_commands[0]).to match(/cp \/usbkey\/config.inc\/authorized_keys ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/else/) expect(comm.received_commands[0]).to match(/mkdir -p ~\/.ssh/) expect(comm.received_commands[0]).to match(/chmod 0700 ~\/.ssh/) expect(comm.received_commands[0]).to match(/printf 'ssh-rsa ...\\n' >> ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/chmod 0600 ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/fi/) end end end ================================================ FILE: test/unit/plugins/guests/smartos/cap/mount_nfs_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/guests/smartos/config" describe "VagrantPlugins::GuestSmartos::Cap::MountNFS" do let(:caps) do VagrantPlugins::GuestSmartos::Plugin .components .guest_capabilities[:smartos] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config", smartos: VagrantPlugins::GuestSmartos::Config.new) } before do allow(machine).to receive(:communicate).and_return(comm) allow(machine).to receive(:config).and_return(config) end after do comm.verify_expectations! end describe ".mount_nfs_folder" do let(:cap) { caps.get(:mount_nfs_folder) } it "mounts the folder" do cap.mount_nfs_folder(machine, '1.1.1.1', {'nfs' => {guestpath: '/mountpoint', hostpath: '/some/share'}}) expect(comm.received_commands[0]).to match(/if \[ -d \/usbkey \] && \[ "\$\(zonename\)" == "global" \] ; then/) expect(comm.received_commands[0]).to match(/pfexec mkdir -p \/usbkey\/config.inc/) expect(comm.received_commands[0]).to match(/printf '1\.1\.1\.1:\/some\/share:\/mountpoint' | pfexec tee -a \/usbkey\/config.inc\/nfs_mounts/) expect(comm.received_commands[0]).to match(/fi/) expect(comm.received_commands[0]).to match(/pfexec mkdir -p \/mountpoint/) expect(comm.received_commands[0]).to match(/pfexec \/usr\/sbin\/mount -F nfs '1\.1\.1\.1:\/some\/share' '\/mountpoint'/) end end end ================================================ FILE: test/unit/plugins/guests/smartos/cap/remove_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSmartos::Cap::RemovePublicKey" do let(:caps) do VagrantPlugins::GuestSmartos::Plugin .components .guest_capabilities[:smartos] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".remove_public_key" do let(:cap) { caps.get(:remove_public_key) } it "removes the public key" do cap.remove_public_key(machine, "ssh-rsa keyvalue comment") expect(comm.received_commands[0]).to match(/if test -f \/usbkey\/config.inc\/authorized_keys ; then/) expect(comm.received_commands[0]).to match(/sed -i '' '\/\^.*ssh-rsa keyvalue comment.*\$\/d' \/usbkey\/config.inc\/authorized_keys/) expect(comm.received_commands[0]).to match(/fi/) expect(comm.received_commands[0]).to match(/if test -f ~\/.ssh\/authorized_keys ; then/) expect(comm.received_commands[0]).to match(/sed -i '' '\/\^.*ssh-rsa keyvalue comment.*\$\/d' ~\/.ssh\/authorized_keys/) expect(comm.received_commands[0]).to match(/fi/) end end end ================================================ FILE: test/unit/plugins/guests/smartos/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::VagrantPlugins::Cap::Rsync" do let(:plugin) { VagrantPlugins::GuestSmartos::Plugin.components.guest_capabilities[:smartos].get(:rsync_installed) } let(:machine) { double("machine") } let(:config) { double("config", smartos: VagrantPlugins::GuestSmartos::Config.new) } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:config).and_return(config) end after do communicator.verify_expectations! end describe ".rsync_installed" do describe "when rsync is in the path" do it "is true" do communicator.stub_command("which rsync", stdout: '/usr/bin/rsync', exit_code: 0) expect(plugin.rsync_installed(machine)).to be true end end describe "when rsync is not in the path" do it "is false" do communicator.stub_command("which rsync", stdout: '', exit_code: 1) expect(plugin.rsync_installed(machine)).to be false end end end describe ".rsync_pre" do it 'makes the guestpath directory with pfexec' do communicator.expect_command("pfexec mkdir -p '/sync_dir'") plugin.rsync_pre(machine, guestpath: '/sync_dir') end end describe ".rsync_post" do it 'chowns incorrectly owned files in sync dir' do communicator.expect_command("pfexec find /sync_dir '!' -type l -a '(' ! -user somebody -or ! -group somegroup ')' -exec chown somebody:somegroup '{}' +") plugin.rsync_post(machine, guestpath: '/sync_dir', owner: 'somebody', group: 'somegroup') end end end ================================================ FILE: test/unit/plugins/guests/solaris/cap/file_system_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSolaris::Cap::FileSystem" do let(:caps) do VagrantPlugins::GuestSolaris::Plugin .components .guest_capabilities[:solaris] end let(:machine) { double("machine", communicate: comm) } let(:comm) { double("comm") } before { allow(comm).to receive(:execute) } describe ".create_tmp_path" do let(:cap) { caps.get(:create_tmp_path) } let(:opts) { {} } it "should generate path on guest" do expect(comm).to receive(:execute).with(/mktemp/) cap.create_tmp_path(machine, opts) end it "should capture path generated on guest" do expect(comm).to receive(:execute).with(/mktemp/).and_yield(:stdout, "TMP_PATH") expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH") end it "should strip newlines on path" do expect(comm).to receive(:execute).with(/mktemp/).and_yield(:stdout, "TMP_PATH\n") expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH") end context "when type is a directory" do before { opts[:type] = :directory } it "should create guest path as a directory" do expect(comm).to receive(:execute).with(/-d/) cap.create_tmp_path(machine, opts) end end end describe ".decompress_tgz" do let(:cap) { caps.get(:decompress_tgz) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_tgz(machine, comp, dest, opts) } it "should create temporary directory for extraction" do expect(cap).to receive(:create_tmp_path) end it "should extract file with tar" do expect(comm).to receive(:execute).with(/tar/) end it "should extract file to temporary directory" do expect(comm).to receive(:execute).with(/TMP_DIR/) end it "should remove compressed file from guest" do expect(comm).to receive(:execute).with(/rm .*#{comp}/) end it "should remove extraction directory from guest" do expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) end it "should create parent directories for destination" do expect(comm).to receive(:execute).with(/mkdir -p .*to'/) end context "when type is directory" do before { opts[:type] = :directory } it "should create destination directory" do expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) end end end describe ".decompress_zip" do let(:cap) { caps.get(:decompress_zip) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_zip(machine, comp, dest, opts) } it "should create temporary directory for extraction" do expect(cap).to receive(:create_tmp_path) end it "should extract file with zip" do expect(comm).to receive(:execute).with(/zip/) end it "should extract file to temporary directory" do expect(comm).to receive(:execute).with(/TMP_DIR/) end it "should remove compressed file from guest" do expect(comm).to receive(:execute).with(/rm .*#{comp}/) end it "should remove extraction directory from guest" do expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) end it "should create parent directories for destination" do expect(comm).to receive(:execute).with(/mkdir -p .*to'/) end context "when type is directory" do before { opts[:type] = :directory } it "should create destination directory" do expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) end end end end ================================================ FILE: test/unit/plugins/guests/solaris/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSolaris::Cap::Halt" do let(:caps) do VagrantPlugins::GuestSolaris::Plugin .components .guest_capabilities[:solaris] end let(:shutdown_command){ "sudo /usr/sbin/shutdown -y -i5 -g0" } let(:machine) { double("machine", config: double("config", solaris: double("solaris", suexec_cmd: 'sudo'))) } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".halt" do let(:cap) { caps.get(:halt) } it "runs the shutdown command" do comm.expect_command(shutdown_command) cap.halt(machine) end it "ignores an IOError" do comm.stub_command(shutdown_command, raise: IOError) expect { cap.halt(machine) }.to_not raise_error end it "ignores a Vagrant::Errors::SSHDisconnected" do comm.stub_command(shutdown_command, raise: Vagrant::Errors::SSHDisconnected) expect { cap.halt(machine) }.to_not raise_error end end end ================================================ FILE: test/unit/plugins/guests/solaris11/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSolaris11::Cap::ChangeHostName" do let(:caps) do VagrantPlugins::GuestSolaris11::Plugin .components .guest_capabilities[:solaris11] end let(:machine) { double("machine", config: double("config", solaris11: double("solaris11", suexec_cmd: 'sudo'))) } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".change_host_name" do let(:cap) { caps.get(:change_host_name) } let(:name) { "solaris11.domain.com" } it "changes the hostname" do allow(machine.communicate).to receive(:test).and_return(false) allow(machine.communicate).to receive(:execute) expect(machine.communicate).to receive(:execute).with("sudo /usr/sbin/svccfg -s system/identity:node setprop config/nodename=\"#{name}\"") expect(machine.communicate).to receive(:execute).with("sudo /usr/sbin/svccfg -s system/identity:node setprop config/loopback=\"#{name}\"") expect(machine.communicate).to receive(:execute).with("sudo /usr/sbin/svccfg -s system/identity:node refresh ") expect(machine.communicate).to receive(:execute).with("sudo /usr/sbin/svcadm restart system/identity:node ") cap.change_host_name(machine, name) end end end ================================================ FILE: test/unit/plugins/guests/solaris11/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSolaris11::Cap::ConfigureNetworks" do let(:caps) do VagrantPlugins::GuestSolaris11::Plugin .components .guest_capabilities[:solaris11] end let(:machine) { double("machine", config: double("config", solaris11: double("solaris11", suexec_cmd: 'sudo', device: 'net'))) } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end let(:networks) { [network_1, network_2] } it "configures the guests network if static" do allow(machine.communicate).to receive(:test).and_return(true) cap.configure_networks(machine, networks) expect(comm.received_commands[1]).to eq("sudo ipadm delete-addr net1/v4") expect(comm.received_commands[2]).to eq("sudo ipadm create-addr -T static -a 33.33.33.10/16 net1/v4") end it "configures the guests network if dhcp" do allow(machine.communicate).to receive(:test).and_return(true) cap.configure_networks(machine, networks) expect(comm.received_commands[0]).to eq("sudo ipadm create-addr -T addrconf net0/v4") end end end ================================================ FILE: test/unit/plugins/guests/suse/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSUSE::Cap::ChangeHostName" do let(:caps) do VagrantPlugins::GuestSUSE::Plugin .components .guest_capabilities[:suse] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:cap) { caps.get(:change_host_name) } let(:name) { "banana-rama.example.com" } let(:basename) { "banana-rama" } before do allow(machine).to receive(:communicate).and_return(comm) allow(cap).to receive(:hostnamectl?).and_return(true) end after do comm.verify_expectations! end describe ".change_host_name" do context "minimal network config" do let(:networks) { [ [:forwarded_port, {:guest=>22, :host=>2222, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}] ] } before do allow(machine).to receive_message_chain(:config, :vm, :networks).and_return(networks) end it "sets the hostname" do comm.stub_command('test "$(hostnamectl --static status)" = "#{basename}"', exit_code: 1) cap.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/echo #{name} > \/etc\/HOSTNAME/) expect(comm.received_commands[2]).to match(/hostnamectl set-hostname '#{basename}'/) end it "does not change the hostname if already set" do comm.stub_command('test "$(hostnamectl --static status)" = "#{basename}"', exit_code: 0) cap.change_host_name(machine, name) expect(comm.received_commands.size).to eq(3) end context "hostnamectl is not present" do before do allow(cap).to receive(:hostnamectl?).and_return(false) end it "sets the hostname" do comm.stub_command("hostname -f | grep '^#{name}$'", exit_code: 1) cap.change_host_name(machine, name) expect(comm.received_commands[2]).to match(/echo #{name} > \/etc\/HOSTNAME/) expect(comm.received_commands[2]).to match(/hostname '#{basename}'/) end end end end end ================================================ FILE: test/unit/plugins/guests/suse/cap/configure_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSUSE::Cap::ConfigureNetworks" do let(:caps) do VagrantPlugins::GuestSUSE::Plugin .components .guest_capabilities[:suse] end let(:guest) { double("guest") } let(:machine) { double("machine", guest: guest) } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".configure_networks" do let(:cap) { caps.get(:configure_networks) } before do allow(guest).to receive(:capability).with(:network_scripts_dir) .and_return("/scripts") allow(guest).to receive(:capability).with(:network_interfaces) .and_return(["eth1", "eth2"]) end let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end it "creates and starts the networks" do cap.configure_networks(machine, [network_1, network_2]) expect(comm.received_commands[0]).to match(/\/sbin\/ifdown 'eth1'/) expect(comm.received_commands[0]).to match(/\/sbin\/ifup 'eth1'/) expect(comm.received_commands[0]).to match(/\/sbin\/ifdown 'eth2'/) expect(comm.received_commands[0]).to match(/\/sbin\/ifup 'eth2'/) end end end ================================================ FILE: test/unit/plugins/guests/suse/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSUSE::Cap::Halt" do let(:caps) do VagrantPlugins::GuestSUSE::Plugin .components .guest_capabilities[:suse] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".halt" do let(:cap) { caps.get(:halt) } it "runs systemctl shutdown when systemctl is present" do comm.stub_command('test -e /usr/bin/systemctl', exit_code: 0) comm.expect_command('test -e /usr/bin/systemctl') comm.expect_command("/usr/bin/systemctl poweroff &") cap.halt(machine) end it "runs shutdown when systemctl is not present" do comm.stub_command('test -e /usr/bin/systemctl', exit_code: 1) comm.expect_command('test -e /usr/bin/systemctl') comm.expect_command("/sbin/shutdown -h now &") cap.halt(machine) end it "does not raise an IOError" do comm.stub_command('test -e /usr/bin/systemctl', exit_code: 0) comm.stub_command("/usr/bin/systemctl poweroff &", raise: IOError) expect { cap.halt(machine) }.to_not raise_error end it "ignores a Vagrant::Errors::SSHDisconnected" do comm.stub_command('test -e /usr/bin/systemctl', exit_code: 1) comm.stub_command("/sbin/shutdown -h now &", raise: Vagrant::Errors::SSHDisconnected) expect { cap.halt(machine) }.to_not raise_error end end end ================================================ FILE: test/unit/plugins/guests/suse/cap/network_scripts_dir_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSUSE::Cap::NetworkScriptsDir" do let(:caps) do VagrantPlugins::GuestSUSE::Plugin .components .guest_capabilities[:suse] end let(:machine) { double("machine") } describe ".network_scripts_dir" do let(:cap) { caps.get(:network_scripts_dir) } it "runs /etc/sysconfig/network" do expect(cap.network_scripts_dir(machine)).to eq("/etc/sysconfig/network") end end end ================================================ FILE: test/unit/plugins/guests/suse/cap/nfs_client_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSUSE::Cap::NFSClient" do let(:caps) do VagrantPlugins::GuestSUSE::Plugin .components .guest_capabilities[:suse] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".nfs_client_install" do let(:cap) { caps.get(:nfs_client_install) } it "installs nfs client utilities" do cap.nfs_client_install(machine) expect(comm.received_commands[0]).to match(/zypper -n install nfs-client/) expect(comm.received_commands[0]).to match(/systemctl restart rpcbind/) expect(comm.received_commands[0]).to match(/systemctl restart nfs-client.target/) end end end ================================================ FILE: test/unit/plugins/guests/suse/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestSUSE::Cap::RSync" do let(:caps) do VagrantPlugins::GuestSUSE::Plugin .components .guest_capabilities[:suse] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".rsync_install" do let(:cap) { caps.get(:rsync_install) } it "installs rsync" do comm.expect_command("zypper -n install rsync") cap.rsync_install(machine) end end describe ".rsync_installed" do let(:cap) { caps.get(:rsync_installed) } it "checks if rsync is installed" do comm.expect_command("test -f /usr/bin/rsync") cap.rsync_installed(machine) end end end ================================================ FILE: test/unit/plugins/guests/tinycore/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestTinyCore::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestTinyCore::Plugin.components.guest_capabilities[:tinycore].get(:change_host_name) end let(:machine) { double("machine") } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:old_hostname) { 'boot2docker' } before do allow(machine).to receive(:communicate).and_return(communicator) communicator.stub_command('hostname -f', stdout: old_hostname) end after do communicator.verify_expectations! end describe ".change_host_name" do it "refreshes the hostname service with the sethostname command" do communicator.expect_command(%q(/usr/bin/sethostname newhostname.newdomain.tld)) described_class.change_host_name(machine, 'newhostname.newdomain.tld') end end end ================================================ FILE: test/unit/plugins/guests/tinycore/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestTinyCore::Cap::Halt" do let(:caps) do VagrantPlugins::GuestTinyCore::Plugin .components .guest_capabilities[:tinycore] end let(:shutdown_command){ "poweroff" } let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".halt" do let(:cap) { caps.get(:halt) } it "runs the shutdown command" do comm.expect_command(shutdown_command) cap.halt(machine) end it "ignores an IOError" do comm.stub_command(shutdown_command, raise: IOError) expect { cap.halt(machine) }.to_not raise_error end it "ignores a Vagrant::Errors::SSHDisconnected" do comm.stub_command(shutdown_command, raise: Vagrant::Errors::SSHDisconnected) expect { cap.halt(machine) }.to_not raise_error end end end ================================================ FILE: test/unit/plugins/guests/windows/cap/change_host_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/guests/windows/cap/change_host_name") describe "VagrantPlugins::GuestWindows::Cap::ChangeHostName" do let(:described_class) do VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows].get(:change_host_name) end let(:machine) { double("machine", guest: guest) } let(:guest) { double("guest") } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(communicator) end after do communicator.verify_expectations! end describe ".change_host_name" do let(:rename_script) { <<-EOH $computer = Get-WmiObject -Class Win32_ComputerSystem $retval = $computer.rename("newhostname").returnvalue if ($retval -eq 0) { shutdown /r /t 5 /f /d p:4:1 /c "Vagrant Rename Computer" } exit $retval EOH } it "changes the hostname" do communicator.stub_command( 'if (!([System.Net.Dns]::GetHostName() -eq \'newhostname\')) { exit 0 } exit 1', exit_code: 0) communicator.stub_command(rename_script, exit_code: 0) allow(machine.guest).to receive(:capability) allow(machine.guest).to receive(:capability?) described_class.change_host_name_and_wait(machine, 'newhostname', 0) end end end ================================================ FILE: test/unit/plugins/guests/windows/cap/file_system_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::GuestWindows::Cap::FileSystem" do let(:caps) do VagrantPlugins::GuestWindows::Plugin .components .guest_capabilities[:windows] end let(:machine) { double("machine", communicate: comm) } let(:comm) { double("comm") } before { allow(comm).to receive(:execute) } describe ".create_tmp_path" do let(:cap) { caps.get(:create_tmp_path) } let(:opts) { {} } it "should generate path on guest" do expect(comm).to receive(:execute).with(/GetRandomFileName/, any_args) cap.create_tmp_path(machine, opts) end it "should capture path generated on guest" do expect(comm).to receive(:execute).with(/Write-Output/, any_args).and_yield(:stdout, "TMP_PATH") expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH") end it "should strip newlines on path" do expect(comm).to receive(:execute).with(/Write-Output/, any_args).and_yield(:stdout, "TMP_PATH\r\n") expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH") end context "when type is a directory" do before { opts[:type] = :directory } it "should create guest path as a directory" do expect(comm).to receive(:execute).with(/CreateDirectory/, any_args) cap.create_tmp_path(machine, opts) end end end describe ".decompress_zip" do let(:cap) { caps.get(:decompress_zip) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_zip(machine, comp, dest, opts) } it "should create temporary directory for extraction" do expect(cap).to receive(:create_tmp_path) end it "should extract file with zip" do expect(comm).to receive(:execute).with(/copyhere/, any_args) end it "should extract file to temporary directory" do expect(comm).to receive(:execute).with(/TMP_DIR/, any_args) end it "should remove compressed file from guest" do expect(comm).to receive(:execute).with(/Remove-Item .*#{comp}/, any_args) end it "should remove extraction directory from guest" do expect(comm).to receive(:execute).with(/Remove-Item .*TMP_DIR/, any_args) end it "should create parent directories for destination" do expect(comm).to receive(:execute).with(/New-Item .*Directory .*to\\"/, any_args) end context "when type is directory" do before { opts[:type] = :directory } it "should create destination directory" do expect(comm).to receive(:execute).with(/New-Item .*Directory .*destination"/, any_args) end end end end ================================================ FILE: test/unit/plugins/guests/windows/cap/halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/guests/windows/cap/halt") describe "VagrantPlugins::GuestWindows::Cap::Halt" do let(:described_class) do VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows].get(:halt) end let(:machine) { double("machine") } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(communicator) end after do communicator.verify_expectations! end describe ".halt" do it "cancels any existing scheduled shut down" do communicator.expect_command("shutdown -a") described_class.halt(machine) end it "shuts down immediately" do communicator.expect_command('shutdown /s /t 1 /c "Vagrant Halt" /f /d p:4:1') described_class.halt(machine) end end end ================================================ FILE: test/unit/plugins/guests/windows/cap/insert_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../base" require_relative "../../../../../../plugins/communicators/winssh/communicator" describe "VagrantPlugins::GuestWindows::Cap::InsertPublicKey" do let(:caps) do VagrantPlugins::GuestWindows::Plugin .components .guest_capabilities[:windows] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:auth_keys_check_result){ 1 } before do @tempfile = Tempfile.new("vagrant-test") allow(Tempfile).to receive(:new).and_return(@tempfile) allow(comm).to receive(:is_a?).and_return(true) allow(machine).to receive(:communicate).and_return(comm) allow(comm).to receive(:execute).with(/Write-Output .+/, shell: "powershell").and_yield(:stdout, "TEMP\r\nHOME\r\n") allow(comm).to receive(:execute).with(/New-Item -Path .+/, shell: "powershell") allow(comm).to receive(:execute).with(/dir .+authorized_keys/, shell: "cmd", error_check: false).and_return(auth_keys_check_result) allow(comm).to receive(:create_remote_directory) end after do @tempfile.delete end describe ".insert_public_key" do let(:cap) { caps.get(:insert_public_key) } context "when authorized_keys exists on guest" do let(:auth_keys_check_result){ 0 } before do expect(@tempfile).to receive(:delete).and_return(true) expect(@tempfile).to receive(:delete).and_call_original end it "inserts the public key" do expect(comm).to receive(:download) expect(comm).to receive(:upload) expect(comm).to receive(:execute).with(/Set-Acl .*/, shell: "powershell") cap.insert_public_key(machine, "ssh-rsa ...") expect(File.read(@tempfile.path)).to include("ssh-rsa ...") end end context "when authorized_keys does not exist on guest" do before do expect(@tempfile).to receive(:delete).and_return(true) expect(@tempfile).to receive(:delete).and_call_original end it "inserts the public key" do expect(comm).to_not receive(:download) expect(comm).to receive(:upload) expect(comm).to receive(:execute).with(/Set-Acl .*/, shell: "powershell") cap.insert_public_key(machine, "ssh-rsa ...") expect(File.read(@tempfile.path)).to include("ssh-rsa ...") end end context "when required directories cannot be fetched from the guest" do before do expect(comm).to receive(:execute).with(/Write-Output .+/, shell: "powershell").and_yield(:stdout, "TEMP\r\n") end it "should raise an error" do expect{ cap.insert_public_key(machine, "ssh-rsa ...") }.to raise_error(VagrantPlugins::GuestWindows::Errors::PublicKeyDirectoryFailure) end end end end ================================================ FILE: test/unit/plugins/guests/windows/cap/mount_shared_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/guests/windows/cap/mount_shared_folder") describe "VagrantPlugins::GuestWindows::Cap::MountSharedFolder" do let(:machine) { double("machine") } let(:communicator) { double(:execute) } let(:config) { double("config") } let(:vm) { double("vm") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute) allow(machine).to receive(:config).and_return(config) allow(config).to receive(:vm).and_return(vm) allow(vm).to receive(:communicator).and_return(:winrm) end describe "virtualbox" do let(:described_class) do VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows].get(:mount_virtualbox_shared_folder) end describe ".mount_shared_folder" do it "should call mount_volume script with correct args" do expect(Vagrant::Util::TemplateRenderer).to receive(:render).with( /.+scripts\/mount_volume.ps1/, options: { mount_point: "guestpath", share_name: "name", vm_provider_unc_path: "\\\\vboxsvr\\name", }) described_class.mount_virtualbox_shared_folder(machine, 'name', 'guestpath', {}) end it "should replace invalid Windows share chars" do expect(Vagrant::Util::TemplateRenderer).to receive(:render).with( kind_of(String), options: { mount_point: kind_of(String), share_name: "invalid-windows_sharename", vm_provider_unc_path: "\\\\vboxsvr\\invalid-windows_sharename", }) described_class.mount_virtualbox_shared_folder(machine, "/invalid-windows/sharename", "guestpath", {}) end end end describe "vmware" do let(:described_class) do VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows].get(:mount_vmware_shared_folder) end describe ".mount_shared_folder" do it "should call mount_volume script with correct args" do expect(Vagrant::Util::TemplateRenderer).to receive(:render).with( /.+scripts\/mount_volume.ps1/, options: { mount_point: "guestpath", share_name: "name", vm_provider_unc_path: "\\\\vmware-host\\Shared Folders\\name", }) described_class.mount_vmware_shared_folder(machine, 'name', 'guestpath', {}) end end end describe "parallels" do let(:described_class) do VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows].get(:mount_parallels_shared_folder) end describe ".mount_shared_folder" do it "should call mount_volume script with correct args" do expect(Vagrant::Util::TemplateRenderer).to receive(:render).with( /.+scripts\/mount_volume.ps1/, options: { mount_point: "guestpath", share_name: "name", vm_provider_unc_path: "\\\\psf\\name", }) described_class.mount_parallels_shared_folder(machine, 'name', 'guestpath', {}) end end end describe "smb" do let(:described_class) do VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows].get(:mount_smb_shared_folder) end describe ".mount_shared_folder" do it "should call mount_volume script with correct args" do expect(Vagrant::Util::TemplateRenderer).to receive(:render).with( /.+scripts\/mount_volume.ps1/, options: { mount_point: "guestpath", share_name: "name", vm_provider_unc_path: "\\\\host\\name", }) expect(machine.communicate).to receive(:execute).with("cmdkey /add:host /user:user /pass:\"pass\"", {:shell=>:powershell, :elevated=>true}) described_class.mount_smb_shared_folder(machine, 'name', 'guestpath', {:smb_username => "user", :smb_password => "pass", :smb_host => "host"}) end end end describe "virtualbox-ssh" do let(:described_class) do VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows].get(:mount_virtualbox_shared_folder) end before do allow(vm).to receive(:communicator).and_return(:ssh) end describe ".mount_shared_folder" do it "should call mount_volume script via ssh" do expect(communicator).to receive(:execute).with(/powershell/, shell: "sh") described_class.mount_virtualbox_shared_folder(machine, 'name', 'guestpath', {}) end end end end ================================================ FILE: test/unit/plugins/guests/windows/cap/reboot_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/guests/windows/cap/reboot") describe "VagrantPlugins::GuestWindows::Cap::Reboot" do let(:described_class) do VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows].get(:wait_for_reboot) end let(:vm) { double("vm") } let(:config) { double("config") } let(:machine) { double("machine", ui: ui) } let(:guest) { double("guest") } let(:communicator) { double("communicator") } let(:ui) { Vagrant::UI::Silent.new } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(machine.guest).to receive(:ready?).and_return(true) allow(machine).to receive(:config).and_return(config) allow(config).to receive(:vm).and_return(vm) end describe ".reboot" do before do allow(vm).to receive(:communicator).and_return(:winrm) end it "reboots the vm" do allow(communicator).to receive(:execute) expect(communicator).to receive(:test).with(/# Function/, { error_check: false, shell: :powershell }).and_return(0) expect(communicator).to receive(:execute).with(/shutdown/, { shell: :powershell }).and_return(0) expect(described_class).to receive(:wait_for_reboot) described_class.reboot(machine) end context "user output" do before do allow(communicator).to receive(:execute) allow(described_class).to receive(:wait_for_reboot) end after { described_class.reboot(machine) } it "sends message to user that guest is rebooting" do expect(communicator).to receive(:test).and_return(true) expect(ui).to receive(:info).and_call_original end end context "with exceptions while waiting for reboot" do before { allow(described_class).to receive(:sleep) } it "should retry on any standard error" do allow(communicator).to receive(:execute) expect(communicator).to receive(:test).with(/# Function/, { error_check: false, shell: :powershell }).and_return(0) expect(communicator).to receive(:execute).with(/shutdown/, { shell: :powershell }).and_return(0) expect(described_class).to receive(:wait_for_reboot).and_raise(StandardError) expect(described_class).to receive(:wait_for_reboot) described_class.reboot(machine) end it "should not retry when exception is not a standard error" do allow(communicator).to receive(:execute) expect(communicator).to receive(:test).with(/# Function/, { error_check: false, shell: :powershell }).and_return(0) expect(communicator).to receive(:execute).with(/shutdown/, { shell: :powershell }).and_return(0) expect(described_class).to receive(:wait_for_reboot).and_raise(Exception) expect { described_class.reboot(machine) }.to raise_error(Exception) end end end describe "winrm communicator" do before do allow(vm).to receive(:communicator).and_return(:winrm) end describe ".wait_for_reboot" do it "runs reboot detect script" do expect(communicator).to receive(:execute).with(/# Function/, { error_check: false, shell: :powershell }).and_return(0) allow(communicator).to receive(:execute) described_class.wait_for_reboot(machine) end it "fixes symlinks to network shares" do allow(communicator).to receive(:execute).and_return(0) expect(communicator).to receive(:execute).with('net use', { error_check: false, shell: :powershell }) described_class.wait_for_reboot(machine) end end end describe "ssh communicator" do before do allow(vm).to receive(:communicator).and_return(:ssh) end describe ".wait_for_reboot" do it "does execute Windows reboot detect script" do expect(communicator).to receive(:execute).with(/# Function/, { error_check: false, shell: :powershell }).and_return(0) expect(communicator).to receive(:execute).with('net use', { error_check: false, shell: :powershell }) described_class.wait_for_reboot(machine) end end end context "reboot configuration" do before do allow(communicator).to receive(:execute) expect(communicator).to receive(:test).with(/# Function/, { error_check: false, shell: :powershell }).and_return(0) expect(communicator).to receive(:execute).with(/shutdown/, { shell: :powershell }).and_return(0) allow(described_class).to receive(:sleep) allow(described_class).to receive(:wait_for_reboot).and_raise(StandardError) end context "default retry duration value" do let(:max_retries) { (described_class::DEFAULT_MAX_REBOOT_RETRY_DURATION / described_class::WAIT_SLEEP_TIME) + 2 } it "should receive expected number of wait_for_reboot calls" do expect(described_class).to receive(:wait_for_reboot).exactly(max_retries).times expect { described_class.reboot(machine) }.to raise_error(StandardError) end end context "with custom retry duration value" do let(:duration) { 10 } let(:max_retries) { (duration / described_class::WAIT_SLEEP_TIME) + 2 } before do expect(ENV).to receive(:fetch).with("VAGRANT_MAX_REBOOT_RETRY_DURATION", anything).and_return(duration) end it "should receive expected number of wait_for_reboot calls" do expect(described_class).to receive(:wait_for_reboot).exactly(max_retries).times expect { described_class.reboot(machine) }.to raise_error(StandardError) end end end end ================================================ FILE: test/unit/plugins/guests/windows/cap/remove_public_key_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require_relative "../../../../base" require_relative "../../../../../../plugins/communicators/winssh/communicator" describe "VagrantPlugins::GuestWindows::Cap::RemovePublicKey" do let(:caps) do VagrantPlugins::GuestWindows::Plugin .components .guest_capabilities[:windows] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:public_key_insecure){ "ssh-rsa...insecure" } let(:public_key_other){ "ssh-rsa...other" } let(:auth_keys_check_result){ 1 } before do @tempfile = Tempfile.new("vagrant-test") @tempfile.puts(public_key_insecure) @tempfile.puts(public_key_other) @tempfile.flush @tempfile.rewind allow(Tempfile).to receive(:new).and_return(@tempfile) allow(comm).to receive(:is_a?).and_return(true) allow(machine).to receive(:communicate).and_return(comm) allow(comm).to receive(:execute).with(/Write-Output .+/, shell: "powershell").and_yield(:stdout, "TEMP\r\nHOME\r\n") allow(comm).to receive(:execute).with(/New-Item -Path/, shell: "powershell") allow(comm).to receive(:execute).with(/dir .+\.ssh/, shell: "cmd") allow(comm).to receive(:execute).with(/dir .+authorized_keys/, shell: "cmd", error_check: false).and_return(auth_keys_check_result) allow(comm).to receive(:create_remote_directory) end after do @tempfile.delete end describe ".remove_public_key" do let(:cap) { caps.get(:remove_public_key) } context "when authorized_keys exists on guest" do let(:auth_keys_check_result){ 0 } before do expect(@tempfile).to receive(:delete).and_return(true) expect(@tempfile).to receive(:delete).and_call_original end it "removes the public key" do expect(comm).to receive(:download) expect(comm).to receive(:upload) expect(comm).to receive(:execute).with(/Set-Acl .*/, shell: "powershell") cap.remove_public_key(machine, public_key_insecure) expect(File.read(@tempfile.path)).to include(public_key_other) expect(File.read(@tempfile.path)).to_not include(public_key_insecure) end end context "when authorized_keys does not exist on guest" do it "does nothing" do expect(comm).to_not receive(:download) expect(comm).to receive(:upload) expect(comm).to receive(:execute).with(/Set-Acl .*/, shell: "powershell") cap.remove_public_key(machine, public_key_insecure) end end end end ================================================ FILE: test/unit/plugins/guests/windows/cap/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/guests/windows/cap/rsync") describe "VagrantPlugins::GuestWindows::Cap::RSync" do let(:described_class) do VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows].get(:rsync_pre) end let(:machine) { double("machine") } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(communicator) end after do communicator.verify_expectations! end describe ".rsync_pre" do it 'makes the guestpath directory with mkdir' do communicator.expect_command("mkdir \"/sync_dir\" -force") described_class.rsync_pre(machine, guestpath: '/sync_dir') end end end ================================================ FILE: test/unit/plugins/guests/windows/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/guests/windows/config") describe VagrantPlugins::GuestWindows::Config do let(:machine) { double("machine") } subject { described_class.new } it "is valid by default" do subject.finalize! result = subject.validate(machine) expect(result["Windows Guest"]).to be_empty end describe "default values" do before { subject.finalize! } its("set_work_network") { should == false } end describe "attributes" do [:set_work_network].each do |attribute| it "should not default #{attribute} if overridden" do subject.send("#{attribute}=".to_sym, 10) subject.finalize! expect(subject.send(attribute)).to be(10) end it "should return error #{attribute} if nil" do subject.send("#{attribute}=".to_sym, nil) subject.finalize! result = subject.validate(machine) expect(result["Windows Guest"]).to include("windows.#{attribute} cannot be nil.") end end end end ================================================ FILE: test/unit/plugins/guests/windows/guest_network_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/guests/windows/guest_network") describe "VagrantPlugins::GuestWindows::GuestNetwork" do let(:communicator) { double("communicator") } let(:subject) { VagrantPlugins::GuestWindows::GuestNetwork.new(communicator) } describe ".is_dhcp_enabled" do it "should query the NIC by ordinal index" do expect(communicator).to receive(:test).with( /.+Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "Index=7 and DHCPEnabled=True"/). and_return(true) expect(subject.is_dhcp_enabled(7)).to be(true) end it "should return false for non-DHCP NICs" do expect(communicator).to receive(:test).with( /.+Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "Index=8 and DHCPEnabled=True"/). and_return(false) expect(subject.is_dhcp_enabled(8)).to be(false) end end describe ".configure_static_interface" do it "should configure IP using netsh" do expect(communicator).to receive(:execute).with( "netsh interface ip set address \"Local Area Connection 2\" static 192.168.33.10 255.255.255.0"). and_return(0) subject.configure_static_interface(7, "Local Area Connection 2", "192.168.33.10", "255.255.255.0") end end describe ".configure_dhcp_interface" do it "should configure DHCP when DHCP is disabled" do allow(communicator).to receive(:test).and_return(false) # is DHCP enabled? expect(communicator).to receive(:execute).with( "netsh interface ip set address \"Local Area Connection 2\" dhcp"). and_return(0) subject.configure_dhcp_interface(7, "Local Area Connection 2") end it "should not configure DHCP when DHCP is enabled" do allow(communicator).to receive(:test).and_return(true) # is DHCP enabled? expect(communicator).to_not receive(:execute) subject.configure_dhcp_interface(7, "Local Area Connection 2") end end end ================================================ FILE: test/unit/plugins/hosts/bsd/cap/nfs_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/bsd/cap/nfs" describe VagrantPlugins::HostBSD::Cap::NFS do include_context "unit" describe ".nfs_export" do let(:environment) { double("environment", host: host) } let(:host) { double("host") } let(:ui) { Vagrant::UI::Silent.new } let(:id) { "UUID" } let(:ips) { [] } let(:folders) { {} } before do allow(host).to receive(:capability).and_return("") allow(Vagrant::Util::TemplateRenderer).to receive(:render).and_return("") allow(described_class).to receive(:sleep) allow(described_class).to receive(:nfs_cleanup) allow(described_class).to receive(:system) allow(described_class).to receive(:nfs_running?).and_return(true) allow(File).to receive(:writable?).with("/etc/exports") allow(Vagrant::Util::Subprocess).to receive(:execute).with("nfsd", "checkexports"). and_return(Vagrant::Util::Subprocess::Result.new(0, "", "")) end it "should execute successfully when no folders are defined" do expect { described_class.nfs_export(environment, ui, id, ips, folders) }. not_to raise_error end context "with single folder defined" do let(:folders) { {"/vagrant" => { type: :nfs, guestpath: "/vagrant", hostpath: "/Users/vagrant/paths", disabled: false}} } it "should execute successfully" do expect { described_class.nfs_export(environment, ui, id, ips, folders) }. not_to raise_error end it "should resolve the host path" do expect(host).to receive(:capability).with(:resolve_host_path, folders["/vagrant"][:hostpath]).and_return("") described_class.nfs_export(environment, ui, id, ips, folders) end end context "when nfsd is not running" do before { allow(described_class).to receive(:nfs_running?).and_return(false) } it "should restart nfsd" do expect(host).to receive(:capability).with(:nfs_restart_command).and_return(["restart"]) expect(described_class).to receive(:system).with("restart") described_class.nfs_export(environment, ui, id, ips, folders) end end context "when nfsd is running" do before { allow(described_class).to receive(:nfs_running?).and_return(true) } it "should update nfsd" do expect(host).to receive(:capability).with(:nfs_update_command).and_return(["update"]) expect(described_class).to receive(:system).with("update") described_class.nfs_export(environment, ui, id, ips, folders) end end end end ================================================ FILE: test/unit/plugins/hosts/bsd/cap/path_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/bsd/cap/path" describe VagrantPlugins::HostBSD::Cap::Path do describe ".resolve_host_path" do let(:env) { double("environment") } let(:path) { double("path") } it "should return the path object provided" do expect(described_class.resolve_host_path(env, path)).to eq(path) end end end ================================================ FILE: test/unit/plugins/hosts/bsd/cap/ssh_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/bsd/cap/ssh" describe VagrantPlugins::HostBSD::Cap::SSH do let(:subject){ VagrantPlugins::HostBSD::Cap::SSH } let(:env){ double("env") } let(:key_path){ double("key_path") } it "should set file as user only read/write" do expect(key_path).to receive(:chmod).with(0600) subject.set_ssh_key_permissions(env, key_path) end end ================================================ FILE: test/unit/plugins/hosts/darwin/cap/configured_ip_addresses_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/darwin/cap/configured_ip_addresses" describe VagrantPlugins::HostDarwin::Cap::ConfiguredIPAddresses do let(:subject){ VagrantPlugins::HostDarwin::Cap::ConfiguredIPAddresses } let(:interfaces){ ["192.168.1.2"] } before{ allow(Socket).to receive(:getifaddrs).and_return( interfaces.map{|i| double(:socket, addr: Addrinfo.ip(i))}) } it "should get list of available addresses" do expect(subject.configured_ip_addresses(nil)).to eq(["192.168.1.2"]) end context "with loopback address" do let(:interfaces){ ["192.168.1.2", "127.0.0.1"] } it "should not include loopback address" do expect(subject.configured_ip_addresses(nil)).not_to include(["127.0.0.1"]) end end context "with IPv6 address" do let(:interfaces){ ["192.168.1.2", "2001:200:dff:fff1:216:3eff:feb1:44d7"] } it "should not include IPv6 address" do expect(subject.configured_ip_addresses(nil)).not_to include(["2001:200:dff:fff1:216:3eff:feb1:44d7"]) end end end ================================================ FILE: test/unit/plugins/hosts/darwin/cap/fs_iso_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/darwin/cap/fs_iso" describe VagrantPlugins::HostDarwin::Cap::FsISO do include_context "unit" let(:subject){ VagrantPlugins::HostDarwin::Cap::FsISO } let(:env) { double("env") } describe ".isofs_available" do it "finds iso building utility when available" do expect(Vagrant::Util::Which).to receive(:which).and_return(true) expect(subject.isofs_available(env)).to eq(true) end end describe ".create_iso" do let(:file_destination) { "/woo/out.iso" } before do allow(file_destination).to receive(:nil?).and_return(false) end it "builds an iso" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "hdiutil", "makehybrid", "-iso", "-joliet", "-ov", "-o", /.iso/, /\/foo\/src/ ).and_return(double(exit_code: 0)) expect(FileUtils).to receive(:mkdir_p).with(Pathname.new(file_destination).dirname) output = subject.create_iso(env, "/foo/src", file_destination: file_destination) expect(output.to_s).to eq("/woo/out.iso") end it "builds an iso with volume_id" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "hdiutil", "makehybrid", "-iso", "-joliet", "-ov", "-default-volume-name", "cidata", "-o", /.iso/, /\/foo\/src/ ).and_return(double(exit_code: 0)) expect(FileUtils).to receive(:mkdir_p).with(Pathname.new(file_destination).dirname) output = subject.create_iso(env, "/foo/src", file_destination: file_destination, volume_id: "cidata") expect(output.to_s).to eq("/woo/out.iso") end it "builds an iso given a file destination without an extension" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "hdiutil", "makehybrid", "-iso", "-joliet", "-ov", "-o", /.iso/, /\/foo\/src/ ).and_return(double(exit_code: 0)) expect(FileUtils).to receive(:mkdir_p).with(Pathname.new("/woo/out_dir")) output = subject.create_iso(env, "/foo/src", file_destination: "/woo/out_dir") expect(output.to_s).to match(/\/woo\/out_dir\/[\w]{6}_vagrant.iso/) end it "builds an iso when no file destination is given" do allow(Tempfile).to receive(:new).and_return(file_destination) allow(file_destination).to receive(:path).and_return(file_destination) allow(file_destination).to receive(:delete) expect(Vagrant::Util::Subprocess).to receive(:execute).with( "hdiutil", "makehybrid", "-iso", "-joliet", "-ov", "-o", /.iso/, /\/foo\/src/ ).and_return(double(exit_code: 0)) # Should create a directory wherever Tempfile creates files by default expect(FileUtils).to receive(:mkdir_p).with(Pathname.new(file_destination).dirname) allow(file_destination).to receive(:close) allow(file_destination).to receive(:unlink) output = subject.create_iso(env, "/foo/src") expect(output.to_s).to eq(file_destination) end it "raises an error if iso build failed" do allow(Vagrant::Util::Subprocess).to receive(:execute).with(any_args).and_return(double(stdout: "nope", stderr: "nope", exit_code: 1)) expect(FileUtils).to receive(:mkdir_p).with(Pathname.new(file_destination).dirname) expect{ subject.create_iso(env, "/foo/src", file_destination: file_destination) }.to raise_error(Vagrant::Errors::ISOBuildFailed) end end end ================================================ FILE: test/unit/plugins/hosts/darwin/cap/nfs_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/darwin/cap/nfs" describe VagrantPlugins::HostDarwin::Cap::NFS do include_context "unit" let(:subject){ VagrantPlugins::HostDarwin::Cap::NFS } it "exists" do expect(subject).to_not be(nil) end it "should use nfs/exports_darwin as its template" do expect(subject.nfs_exports_template(nil)).to eq("nfs/exports_darwin") end end ================================================ FILE: test/unit/plugins/hosts/darwin/cap/path_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/darwin/cap/path" require_relative "../../../../../../plugins/hosts/darwin/cap/version" describe VagrantPlugins::HostDarwin::Cap::Path do include_context "unit" let(:caps) do VagrantPlugins::HostDarwin::Plugin .components .host_capabilities[:darwin] end let(:host) { @host } let(:hosts) { { darwin: [VagrantPlugins::HostDarwin::Host, :bsd] } } let(:env) do ienv = isolated_environment ienv.vagrantfile("") env = ienv.create_vagrant_env @host = Vagrant::Host.new( :darwin, hosts, {darwin: caps}, env ) env.instance_variable_set(:@host, @host) env end describe ".resolve_host_path" do let(:cap) { caps.get(:resolve_host_path) } let(:path) { "/test/vagrant/path" } let(:firmlink_map) { {} } let(:macos_version) { Gem::Version.new("10.15.1") } let(:result) { Vagrant::Util::Subprocess::Result.new(0, macos_version, "") } before do # allow(env).to receive(:host). # and_return(host) # allow(host).to receive(:capability). # with(:version). # and_return(macos_version) allow(Vagrant::Util::Subprocess).to receive(:execute). with("sw_vers", "-productVersion"). and_return(result) allow(described_class).to receive(:firmlink_map). and_return(firmlink_map) end it "should not change the path when no firmlinks are defined" do expect(cap.resolve_host_path(env, path)).to eq(path) end context "when firmlink map contains non-matching values" do let(:firmlink_map) { {"/users" => "users", "/system" => "system"} } it "should not change the path" do expect(cap.resolve_host_path(env, path)).to eq(path) end end context "when firmlink map contains matching value" do let(:firmlink_map) { {"/users" => "users", "/test" => "test"} } it "should update the path" do expect(cap.resolve_host_path(env, path)).not_to eq(path) end it "should prefix the path with the defined data path" do expect(cap.resolve_host_path(env, path)).to start_with(described_class.const_get(:FIRMLINK_DATA_PATH)) end end context "when firmlink map match points to different named target" do let(:firmlink_map) { {"/users" => "users", "/test" => "other"} } it "should update the path" do expect(cap.resolve_host_path(env, path)).not_to eq(path) end it "should prefix the path with the defined data path" do expect(cap.resolve_host_path(env, path)). to start_with(described_class.const_get(:FIRMLINK_DATA_PATH)) end it "should include the updated path name" do expect(cap.resolve_host_path(env, path)).to include("other") end end context "when macos version is later than catalina" do let(:macos_version) { Gem::Version.new("10.16.1") } it "should not update the path" do expect(cap.resolve_host_path(env, path)).to eq(path) end it "should not prefix the path with the defined data path" do expect(cap.resolve_host_path(env, path)). not_to start_with(described_class.const_get(:FIRMLINK_DATA_PATH)) end end end describe ".firmlink_map" do let(:cap) { caps.get(:firmlink_map) } before { described_class.reset! } context "when firmlink definition file does not exist" do before { expect(File).to receive(:exist?). with(described_class.const_get(:FIRMLINK_DEFS)).and_return(false) } it "should return an empty hash" do expect(described_class.firmlink_map).to eq({}) end end context "when firmlink definition file exists with values" do before do expect(File).to receive(:exist?).with(described_class.const_get(:FIRMLINK_DEFS)).and_return(true) expect(File).to receive(:readlines).with.(described_class.const_get(:FIRMLINK_DEFS)). and_return(["/System\tSystem\n", "/Users\tUsers\n", "/Library/Something\tLibrary/Somethingelse"]) it "should generate a non-empty hash" do expect(described_class.firmlink_map).not_to be_empty end it "should properly create entries" do result = described_class.firmlink_map expect(result["/System"]).to eq("System") expect(result["/Users"]).to eq("Users") expect(result["/Library/Something"]).to eq("Library/Somethingelse") end it "should only load values once" do describe_class.firmlink_app expect(File).not_to receive(:readlines) describe_class.firmlink_app end end end end end ================================================ FILE: test/unit/plugins/hosts/darwin/cap/rdp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/darwin/cap/rdp" describe VagrantPlugins::HostDarwin::Cap::RDP do let(:rdp_info) do { host: "host", port: "port", username: "username", } end it "includes the default options" do path = described_class.generate_config_file(rdp_info) result = File.readlines(path).map(&:chomp) expect(result).to include("drivestoredirect:s:*") expect(result).to include("full address:s:host:port") expect(result).to include("prompt for credentials:i:1") expect(result).to include("username:s:username") end it "includes extra RDP arguments" do rdp_info.merge!(extra_args: ["screen mode id:i:0"]) path = described_class.generate_config_file(rdp_info) result = File.readlines(path).map(&:chomp) expect(result).to include("screen mode id:i:0") end it "opens the RDP file" do env = double(:env) allow(described_class).to receive(:generate_config_file).and_return("/path") expect(Vagrant::Util::Subprocess).to receive(:execute).with("open", "/path") described_class.rdp_client(env, rdp_info) end end ================================================ FILE: test/unit/plugins/hosts/darwin/cap/smb_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/darwin/cap/smb" describe VagrantPlugins::HostDarwin::Cap::SMB do include_context "unit" let(:subject){ VagrantPlugins::HostDarwin::Cap::SMB } let(:machine){ double(:machine) } let(:env){ double(:env) } let(:options){ {} } let(:result){ Vagrant::Util::Subprocess::Result } before{ allow(subject).to receive(:machine_id).and_return("CUSTOM_ID") } describe ".smb_installed" do it "is installed if sharing binary exists" do expect(File).to receive(:exist?).with("/usr/sbin/sharing").and_return(true) expect(subject.smb_installed(nil)).to be(true) end it "is not installed if sharing binary does not exist" do expect(File).to receive(:exist?).with("/usr/sbin/sharing").and_return(false) expect(subject.smb_installed(nil)).to be(false) end end describe ".smb_start" do before{ allow(Vagrant::Util::Subprocess).to receive(:execute) .and_return(result.new(0, "SMB-NT", "")) } it "should check for NT compatible password" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("pwpolicy", "gethashtypes"). and_return(result.new(0, "SMB-NT", "")) subject.smb_start(env) end it "should raise error if NT compatible password is not set" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("pwpolicy", "gethashtypes"). and_return(result.new(0, "", "")) expect{ subject.smb_start(env) }.to raise_error(VagrantPlugins::SyncedFolderSMB::Errors::SMBCredentialsMissing) end it "should ignore if the command returns non-zero" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("pwpolicy", "gethashtypes"). and_return(result.new(1, "", "")) subject.smb_start(env) end it "should not load smb preferences if it is already loaded" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("launchctl", "list", /preferences/).and_return(result.new(0, "", "")) expect(Vagrant::Util::Subprocess).not_to receive(:execute).with(/sudo/, /launchctl/, "load", "-w", /preferences/) subject.smb_start(env) end it "should load smb preferences if it is not already loaded" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("launchctl", "list", /preferences/).and_return(result.new(1, "", "")) expect(Vagrant::Util::Subprocess).to receive(:execute).with(/sudo/, /launchctl/, "load", "-w", /preferences/).and_return(result.new(0, "", "")) subject.smb_start(env) end it "should raise error if load smb preferences fails" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("launchctl", "list", /preferences/).and_return(result.new(1, "", "")) expect(Vagrant::Util::Subprocess).to receive(:execute).with(/sudo/, /launchctl/, "load", "-w", /preferences/).and_return(result.new(1, "", "")) expect{ subject.smb_start(env) }.to raise_error(VagrantPlugins::SyncedFolderSMB::Errors::SMBStartFailed) end it "should not load smbd if it is already loaded" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("launchctl", "list", /smbd/).and_return(result.new(0, "", "")) expect(Vagrant::Util::Subprocess).not_to receive(:execute).with(/sudo/, /launchctl/, "load", "-w", /smbd/) subject.smb_start(env) end it "should load smbd if it is not already loaded" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("launchctl", "list", /smbd/).and_return(result.new(1, "", "")) expect(Vagrant::Util::Subprocess).to receive(:execute).with(/sudo/, /launchctl/, "load", "-w", /smbd/).and_return(result.new(0, "", "")) subject.smb_start(env) end it "should raise error if load smbd fails" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("launchctl", "list", /smbd/).and_return(result.new(1, "", "")) expect(Vagrant::Util::Subprocess).to receive(:execute).with(/sudo/, /launchctl/, "load", "-w", /smbd/).and_return(result.new(1, "", "")) expect{ subject.smb_start(env) }.to raise_error(VagrantPlugins::SyncedFolderSMB::Errors::SMBStartFailed) end end describe ".smb_cleanup" do after{ subject.smb_cleanup(env, machine, options) } it "should search for shares with generated machine ID" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "/usr/bin/sudo", /sharing/, "-l").and_return(result.new(0, "", "")) end it "should remove shares individually" do expect(Vagrant::Util::Subprocess).to receive(:execute). with("/usr/bin/sudo", /sharing/, "-l"). and_return(result.new(0, "name: vgt-CUSTOM_ID-1\nname: vgt-CUSTOM_ID-2\n", "")) expect(Vagrant::Util::Subprocess).to receive(:execute).with(/sudo/, /sharing/, anything, /CUSTOM_ID/). twice.and_return(result.new(0, "", "")) end end describe ".smb_prepare" do let(:folders){ {"/first/path" => {hostpath: "/first/host", smb_id: "ID1"}, "/second/path" => {hostpath: "/second/host"}} } before{ allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(result.new(0, "", "")) } it "should provide ID value if not set" do subject.smb_prepare(env, machine, folders, options) expect(folders["/second/path"][:smb_id]).to start_with("vgt-") end it "should not modify ID if already set" do subject.smb_prepare(env, machine, folders, options) expect(folders["/first/path"][:smb_id]).to eq("ID1") end it "should raise error when sharing command fails" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(result.new(1, "", "")) expect{ subject.smb_prepare(env, machine, folders, options) }.to raise_error( VagrantPlugins::SyncedFolderSMB::Errors::DefineShareFailed) end it "should add shares individually" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(/sudo/, any_args).twice.and_return(result.new(0, "", "")) subject.smb_prepare(env, machine, folders, options) end end end ================================================ FILE: test/unit/plugins/hosts/darwin/cap/version_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/darwin/cap/version" describe VagrantPlugins::HostDarwin::Cap::Version do describe ".version" do let(:product_version) { "10.5.1" } let(:env) { double(:env) } let(:exit_code) { 0 } let(:stderr) { "" } let(:stdout) { product_version } let(:result) { Vagrant::Util::Subprocess::Result.new(exit_code, stdout, stderr) } before do allow(Vagrant::Util::Subprocess).to receive(:execute). with("sw_vers", "-productVersion"). and_return(result) end it "should return a Gem::Version" do expect(described_class.version(env)).to be_a(Gem::Version) end it "should equal the defined version" do expect(described_class.version(env)).to eq(Gem::Version.new(product_version)) end context "when version cannot be parsed" do let(:product_version) { "invalid" } it "should raise a failure error" do expect { described_class.version(env) }. to raise_error(Vagrant::Errors::DarwinVersionFailed) end end context "when command execution fails" do let(:exit_code) { 1 } it "should raise a failure error" do expect { described_class.version(env) }. to raise_error(Vagrant::Errors::DarwinVersionFailed) end end end end ================================================ FILE: test/unit/plugins/hosts/linux/cap/fs_iso_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/linux/cap/fs_iso" describe VagrantPlugins::HostLinux::Cap::FsISO do include_context "unit" let(:subject){ VagrantPlugins::HostLinux::Cap::FsISO } let(:env) { double("env") } describe ".isofs_available" do it "finds iso building utility when available" do expect(Vagrant::Util::Which).to receive(:which).and_return(true) expect(subject.isofs_available(env)).to eq(true) end end describe ".create_iso" do let(:file_destination) { "/woo/out.iso" } before do allow(file_destination).to receive(:nil?).and_return(false) allow(FileUtils).to receive(:mkdir_p) end it "builds an iso" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "mkisofs", "-joliet", "-o", /.iso/, /\/foo\/src/ ).and_return(double(exit_code: 0)) output = subject.create_iso(env, "/foo/src", file_destination: file_destination) expect(output.to_s).to eq(file_destination) end it "builds an iso with volume_id" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "mkisofs", "-joliet", "-volid", "cidata", "-o", /.iso/, /\/foo\/src/ ).and_return(double(exit_code: 0)) output = subject.create_iso(env, "/foo/src", file_destination: file_destination, volume_id: "cidata") expect(output.to_s).to eq(file_destination) end it "builds an iso given a file destination without an extension" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "mkisofs", "-joliet", "-o", /.iso/, /\/foo\/src/ ).and_return(double(exit_code: 0)) output = subject.create_iso(env, "/foo/src", file_destination: "/woo/out_dir") expect(output.to_s).to match(/\/woo\/out_dir\/[\w]{6}_vagrant.iso/) end it "raises an error if iso build failed" do allow(Vagrant::Util::Subprocess).to receive(:execute).with(any_args).and_return(double(stdout: "nope", stderr: "nope", exit_code: 1)) expect{ subject.create_iso(env, "/foo/src", file_destination: "/woo/out.iso") }.to raise_error(Vagrant::Errors::ISOBuildFailed) end end end ================================================ FILE: test/unit/plugins/hosts/linux/cap/nfs_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/linux/cap/nfs" require_relative "../../../../../../lib/vagrant/util" describe VagrantPlugins::HostLinux::Cap::NFS do include_context "unit" let(:caps) do VagrantPlugins::HostLinux::Plugin .components .host_capabilities[:linux] end let(:tmp_exports_path) do @tmp_exports ||= temporary_file end let(:exports_path){ VagrantPlugins::HostLinux::Cap::NFS::NFS_EXPORTS_PATH } let(:env){ double(:env) } let(:ui){ Vagrant::UI::Silent.new } let(:host){ double(:host) } before do @original_exports_path = VagrantPlugins::HostLinux::Cap::NFS::NFS_EXPORTS_PATH VagrantPlugins::HostLinux::Cap::NFS.send(:remove_const, :NFS_EXPORTS_PATH) VagrantPlugins::HostLinux::Cap::NFS.const_set(:NFS_EXPORTS_PATH, tmp_exports_path.to_s) allow(Vagrant::Util::Subprocess).to receive(:execute).with("systemctl", "list-units", any_args). and_return(Vagrant::Util::Subprocess::Result.new(1, "", "")) allow(Vagrant::Util::Platform).to receive(:systemd?).and_return(false) end after do VagrantPlugins::HostLinux::Cap::NFS.send(:remove_const, :NFS_EXPORTS_PATH) VagrantPlugins::HostLinux::Cap::NFS.const_set(:NFS_EXPORTS_PATH, @original_exports_path) VagrantPlugins::HostLinux::Cap::NFS.reset! File.unlink(tmp_exports_path.to_s) if File.exist?(tmp_exports_path.to_s) @tmp_exports = nil end describe ".nfs_service_name_systemd" do let(:cap){ VagrantPlugins::HostLinux::Cap::NFS } context "without service match" do it "should use default service name" do expect(cap.nfs_service_name_systemd).to eq(cap.const_get(:NFS_DEFAULT_NAME_SYSTEMD)) end end context "with service match" do let(:custom_nfs_service_name){ "custom-nfs-server-service-name" } before{ expect(Vagrant::Util::Subprocess).to receive(:execute).with("systemctl", "list-units", any_args). and_return(Vagrant::Util::Subprocess::Result.new(0, custom_nfs_service_name, "")) } it "should use the matched service name" do expect(cap.nfs_service_name_systemd).to eq(custom_nfs_service_name) end end end describe ".nfs_service_name_sysv" do let(:cap){ VagrantPlugins::HostLinux::Cap::NFS } context "without service match" do it "should use default service name" do expect(cap.nfs_service_name_sysv).to eq(cap.const_get(:NFS_DEFAULT_NAME_SYSV)) end end context "with service match" do let(:custom_nfs_service_name){ "/etc/init.d/custom-nfs-server-service-name" } before{ expect(Dir).to receive(:glob).with(/.+init\.d.+/).and_return([custom_nfs_service_name]) } it "should use the matched service name" do expect(cap.nfs_service_name_sysv).to eq(File.basename(custom_nfs_service_name)) end end end describe ".nfs_check_command" do let(:cap){ caps.get(:nfs_check_command) } context "without systemd" do before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(false) } it "should use init.d script" do expect(cap.nfs_check_command(env)).to include("init.d") end end context "with systemd" do before do expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(true) end it "should use systemctl" do expect(cap.nfs_check_command(env)).to include("systemctl") end end end describe ".nfs_start_command" do let(:cap){ caps.get(:nfs_start_command) } context "without systemd" do before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(false) } it "should use init.d script" do expect(cap.nfs_start_command(env)).to include("init.d") end end context "with systemd" do before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(true) } it "should use systemctl" do expect(cap.nfs_start_command(env)).to include("systemctl") end end end describe ".nfs_export" do let(:cap){ caps.get(:nfs_export) } before do allow(env).to receive(:host).and_return(host) allow(host).to receive(:capability).with(:nfs_apply_command).and_return("/bin/true") allow(host).to receive(:capability).with(:nfs_check_command).and_return("/bin/true") allow(host).to receive(:capability).with(:nfs_start_command).and_return("/bin/true") allow(Vagrant::Util::Subprocess).to receive(:execute).and_call_original allow(Vagrant::Util::Subprocess).to receive(:execute).with("sudo", "/bin/true").and_return(double(:result, exit_code: 0)) allow(Vagrant::Util::Subprocess).to receive(:execute).with("/bin/true").and_return(double(:result, exit_code: 0)) end it "should export new entries" do cap.nfs_export(env, ui, SecureRandom.uuid, ["127.0.0.1", "127.0.0.1"], "tmp" => {:hostpath => "/tmp"}) exports_content = File.read(exports_path) expect(exports_content.scan(/\/tmp.*127\.0\.0\.1/).length).to be(1) end it "should not remove existing entries" do File.write(exports_path, "/custom/directory hostname1(rw,sync,no_subtree_check)") cap.nfs_export(env, ui, SecureRandom.uuid, ["127.0.0.1", "127.0.0.1"], "tmp" => {:hostpath => "/tmp"}) exports_content = File.read(exports_path) expect(exports_content.scan(/\/tmp.*127\.0\.0\.1/).length).to be(1) expect(exports_content).to match(/\/custom\/directory.*hostname1/) end it "should remove entries no longer valid" do valid_id = SecureRandom.uuid other_id = SecureRandom.uuid content =<<-EOH # VAGRANT-BEGIN: #{Process.uid} #{other_id} "/tmp" 127.0.0.1(rw,no_subtree_check,all_squash,anonuid=,anongid=,fsid=) # VAGRANT-END: #{Process.uid} #{other_id} # VAGRANT-BEGIN: #{Process.uid} #{valid_id} "/var" 127.0.0.1(rw,no_subtree_check,all_squash,anonuid=,anongid=,fsid=) # VAGRANT-END: #{Process.uid} #{valid_id} EOH File.write(exports_path, content) cap.nfs_export(env, ui, valid_id, ["127.0.0.1"], "home" => {:hostpath => "/home"}) exports_content = File.read(exports_path) expect(exports_content).to include("/home") expect(exports_content).to include("/tmp") expect(exports_content).not_to include("/var") end it "throws an exception with at least 2 different nfs options" do folders = {"/vagrant"=> {:hostpath=>"/home/vagrant", :linux__nfs_options=>["rw","all_squash"]}, "/var/www/project"=> {:hostpath=>"/home/vagrant", :linux__nfs_options=>["rw","sync"]}} expect { cap.nfs_export(env, ui, SecureRandom.uuid, ["127.0.0.1"], folders) }. to raise_error Vagrant::Errors::NFSDupePerms end it "writes only 1 hostpath for multiple exports" do folders = {"/vagrant"=> {:hostpath=>"/home/vagrant", :linux__nfs_options=>["rw","all_squash"]}, "/var/www/otherproject"=> {:hostpath=>"/newhome/otherproject", :linux__nfs_options=>["rw","all_squash"]}, "/var/www/project"=> {:hostpath=>"/home/vagrant", :linux__nfs_options=>["rw","all_squash"]}} valid_id = SecureRandom.uuid content =<<-EOH # VAGRANT-BEGIN: #{Process.uid} #{valid_id} "/home/vagrant" 127.0.0.1(rw,all_squash,anonuid=,anongid=,fsid=) "/newhome/otherproject" 127.0.0.1(rw,all_squash,anonuid=,anongid=,fsid=) # VAGRANT-END: #{Process.uid} #{valid_id} EOH cap.nfs_export(env, ui, valid_id, ["127.0.0.1"], folders) exports_content = File.read(exports_path) expect(exports_content).to eq(content) end end describe ".nfs_prune" do let(:cap){ caps.get(:nfs_prune) } before do allow(Vagrant::Util::Subprocess).to receive(:execute).with("mv", any_args). and_call_original end it "should remove entries no longer valid" do invalid_id = SecureRandom.uuid valid_id = SecureRandom.uuid content =<<-EOH # VAGRANT-BEGIN: #{Process.uid} #{invalid_id} "/tmp" 127.0.0.1(rw,no_subtree_check,all_squash,anonuid=,anongid=,fsid=) # VAGRANT-END: #{Process.uid} #{invalid_id} # VAGRANT-BEGIN: #{Process.uid} #{valid_id} "/var" 127.0.0.1(rw,no_subtree_check,all_squash,anonuid=,anongid=,fsid=) # VAGRANT-END: #{Process.uid} #{valid_id} EOH File.write(exports_path, content) cap.nfs_prune(env, ui, [valid_id]) exports_content = File.read(exports_path) expect(exports_content).to include(valid_id) expect(exports_content).not_to include(invalid_id) expect(exports_content).to include("/var") expect(exports_content).not_to include("/tmp") end end describe ".nfs_write_exports" do before do File.write(tmp_exports_path, "original content") allow(Vagrant::Util::Subprocess).to receive(:execute).with("mv", any_args). and_call_original end it "should write updated contents to file" do described_class.nfs_write_exports("new content") exports_content = File.read(exports_path) expect(exports_content).to include("new content") expect(exports_content).not_to include("original content") end it "should only update contents if different" do original_stat = File.stat(exports_path) described_class.nfs_write_exports("original content") updated_stat = File.stat(exports_path) expect(original_stat).to eq(updated_stat) end it "should retain existing file permissions" do File.chmod(0600, exports_path) original_stat = File.stat(exports_path) described_class.nfs_write_exports("original content") updated_stat = File.stat(exports_path) expect(original_stat.mode).to eq(updated_stat.mode) end it "should raise exception when failing to move new exports file" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return( Vagrant::Util::Subprocess::Result.new(1, "Failed to move file", "") ) expect{ described_class.nfs_write_exports("new content") }.to raise_error(Vagrant::Errors::NFSExportsFailed) end context "exports file modification" do let(:tmp_stat) { double("tmp_stat", uid: 100, gid: 100, mode: tmp_mode) } let(:tmp_mode) { 0 } let(:exports_stat) { double("stat", uid: exports_uid, gid: exports_gid, mode: exports_mode, :directory? => true, :writable? => true, :world_writable? => true, :sticky? => true) } let(:exports_uid) { -1 } let(:exports_gid) { -1 } let(:exports_mode) { 0 } let(:new_exports_file) { double("new_exports_file", path: "/dev/null/exports") } let(:new_exports_path) { new_exports_file.path } before do allow(File).to receive(:stat).and_call_original allow(File).to receive(:join).with(Dir.tmpdir, "vagrant-exports").and_return(new_exports_path) allow(File).to receive(:open).with(new_exports_path, "w+").and_return(new_exports_file) allow(File).to receive(:stat).with(new_exports_path).and_return(tmp_stat) allow(File).to receive(:stat).with(tmp_exports_path.to_s).and_return(exports_stat) allow(new_exports_file).to receive(:puts) allow(new_exports_file).to receive(:close) allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(Vagrant::Util::Subprocess::Result.new(0, "", "")) end it "should retain existing file owner and group IDs" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args).to include("sudo") expect(args).to include("chown") }.and_return(Vagrant::Util::Subprocess::Result.new(0, "", "")) described_class.nfs_write_exports("new content") end it "should raise custom exception when chown fails" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args).to include("sudo") expect(args).to include("chown") }.and_return(Vagrant::Util::Subprocess::Result.new(1, "", "")) expect { described_class.nfs_write_exports("new content") }.to raise_error(Vagrant::Errors::NFSExportsFailed) end context "when user has write access to exports file" do let(:file_writable?) { true } let(:dir_writable?) { false } let(:exports_pathname) { double("exports_pathname", writable?: file_writable?, dirname: exports_dir_pathname) } let(:exports_dir_pathname) { double("exports_dir_pathname", writable?: dir_writable?) } before do allow(File).to receive(:stat).and_return(exports_stat) allow(File).to receive(:exist?).and_return(false) allow(Pathname).to receive(:new).with(tmp_exports_path.to_s).and_return(exports_pathname) end it "should use sudo when moving new file" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args).to include("sudo") expect(args).to include("mv") }.and_return(Vagrant::Util::Subprocess::Result.new(0, "", "")) described_class.nfs_write_exports("new content") end context "and write access to exports parent directory" do let(:dir_writable?) { true } it "should not use sudo when moving new file" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args).not_to include("sudo") expect(args).to include("mv") }.and_return(Vagrant::Util::Subprocess::Result.new(0, "", "")) described_class.nfs_write_exports("new content") end end end end end describe ".modinfo_path" do let(:cap){ VagrantPlugins::HostLinux::Cap::NFS } context "with modinfo on PATH" do before do expect(Vagrant::Util::Which).to receive(:which).with("modinfo").and_return("/usr/bin/modinfo") end it "should use full path to modinfo" do expect(cap.modinfo_path).to eq("/usr/bin/modinfo") end end context "with modinfo at /sbin/modinfo" do before do expect(Vagrant::Util::Which).to receive(:which).with("modinfo").and_return(nil) expect(File).to receive(:file?).with("/sbin/modinfo").and_return(true) end it "should use /sbin/modinfo" do expect(cap.modinfo_path).to eq("/sbin/modinfo") end end context "modinfo not found" do before do expect(Vagrant::Util::Which).to receive(:which).with("modinfo").and_return(nil) expect(File).to receive(:file?).with("/sbin/modinfo").and_return(false) end it "should use modinfo" do expect(cap.modinfo_path).to eq("modinfo") end end context "with cached value for modinfo_path" do before do cap.instance_variable_set(:@_modinfo_path, "/usr/local/bin/modinfo") end it "should use cached value" do expect(cap.modinfo_path).to eq("/usr/local/bin/modinfo") end end end end ================================================ FILE: test/unit/plugins/hosts/linux/cap/ssh_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/linux/cap/ssh" describe VagrantPlugins::HostLinux::Cap::SSH do let(:subject){ VagrantPlugins::HostLinux::Cap::SSH } let(:env){ double("env") } let(:key_path){ double("key_path") } it "should set file as user only read/write" do expect(key_path).to receive(:chmod).with(0600) subject.set_ssh_key_permissions(env, key_path) end end ================================================ FILE: test/unit/plugins/hosts/void/cap/nfs_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/void/cap/nfs" require_relative "../../../../../../lib/vagrant/util" describe VagrantPlugins::HostVoid::Cap::NFS do include_context "unit" let(:caps) do VagrantPlugins::HostVoid::Plugin .components .host_capabilities[:void] end let(:env) { double("env") } context ".nfs_check_command" do it "should provide nfs_check_command capability" do expect(caps.get(:nfs_check_command)).to eq(described_class) end it "should return command to execute" do expect(caps.get(:nfs_check_command).nfs_check_command(env)).to be_a(String) end end context ".nfs_start_command" do it "should provide nfs_start_command capability" do expect(caps.get(:nfs_start_command)).to eq(described_class) end it "should return command to execute" do expect(caps.get(:nfs_start_command).nfs_start_command(env)).to be_a(String) end end context ".nfs_installed" do let(:exit_code) { 0 } let(:result) { Vagrant::Util::Subprocess::Result.new(exit_code, "", "") } before { allow(Vagrant::Util::Subprocess).to receive(:execute). with("/usr/bin/xbps-query", "nfs-utils").and_return(result) } it "should provide nfs_installed capability" do expect(caps.get(:nfs_installed)).to eq(described_class) end context "when installed" do it "should return true" do expect(caps.get(:nfs_installed).nfs_installed(env)).to be_truthy end end context "when not installed" do let(:exit_code) { 1 } it "should return false" do expect(caps.get(:nfs_installed).nfs_installed(env)).to be_falsey end end end end ================================================ FILE: test/unit/plugins/hosts/windows/cap/configure_ip_addresses_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/windows/cap/configured_ip_addresses" describe VagrantPlugins::HostWindows::Cap::ConfiguredIPAddresses do let(:subject){ VagrantPlugins::HostWindows::Cap::ConfiguredIPAddresses } let(:result){ Vagrant::Util::Subprocess::Result } let(:addresses){ [] } let(:execute_result){ result.new(0, {ip_addresses: addresses}.to_json, "") } before{ allow(Vagrant::Util::PowerShell).to receive(:execute). and_return(execute_result) } it "should return an array" do expect(subject.configured_ip_addresses(nil)).to be_kind_of(Array) end context "with single address returned" do let(:addresses){ "ADDRESS" } it "should return an array" do expect(subject.configured_ip_addresses(nil)).to eq([addresses]) end end context "with multiple addresses returned" do let(:addresses){ ["ADDRESS1", "ADDRESS2"] } it "should return an array" do expect(subject.configured_ip_addresses(nil)).to eq(addresses) end end context "with failed script execution" do let(:execute_result){ result.new(1, "", "") } it "should raise error" do expect{ subject.configured_ip_addresses(nil) }.to raise_error( Vagrant::Errors::PowerShellError) end end end ================================================ FILE: test/unit/plugins/hosts/windows/cap/fs_iso_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/windows/cap/fs_iso" describe VagrantPlugins::HostWindows::Cap::FsISO do include_context "unit" let(:subject){ VagrantPlugins::HostWindows::Cap::FsISO } let(:env) { double("env") } before do allow(Vagrant::Util::Which).to receive(:which).and_return(true) end describe ".isofs_available" do it "finds iso building utility when available" do expect(Vagrant::Util::Which).to receive(:which).and_return(true) expect(subject.isofs_available(env)).to eq(true) end end describe ".create_iso" do let(:file_destination) { "/woo/out.iso" } before do allow(file_destination).to receive(:nil?).and_return(false) allow(FileUtils).to receive(:mkdir_p) end it "builds an iso" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "oscdimg.exe", "-j1", "-o", "-m", /\/foo\/src/, /.iso/ ).and_return(double(exit_code: 0)) output = subject.create_iso(env, "/foo/src", file_destination: file_destination) expect(output.to_s).to eq(file_destination) end it "builds an iso with volume_id" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "oscdimg.exe", "-j1", "-o", "-m", "-ltest", /\/foo\/src/, /.iso/ ).and_return(double(exit_code: 0)) output = subject.create_iso(env, "/foo/src", file_destination: file_destination, volume_id: "test") expect(output.to_s).to eq(file_destination) end it "builds an iso given a file destination without an extension" do expect(Vagrant::Util::Subprocess).to receive(:execute).with( "oscdimg.exe", "-j1", "-o", "-m", /\/foo\/src/, /.iso/ ).and_return(double(exit_code: 0)) output = subject.create_iso(env, "/foo/src", file_destination: "/woo/out_dir") expect(output.to_s).to match(/\/woo\/out_dir\/[\w]{6}_vagrant.iso/) end it "raises an error if iso build failed" do allow(Vagrant::Util::Subprocess).to receive(:execute).with(any_args).and_return(double(stdout: "nope", stderr: "nope", exit_code: 1)) expect{ subject.create_iso(env, "/foo/src", file_destination: file_destination) }.to raise_error(Vagrant::Errors::ISOBuildFailed) end end describe ".oscdimg_path" do it "returns executable name when found in PATH" do expect(Vagrant::Util::Which).to receive(:which).and_return(true) expect(subject.oscdimg_path).to eq("oscdimg.exe") end it "returns full path when executable detected" do expect(Vagrant::Util::Which).to receive(:which).and_return(false) expect(File).to receive(:executable?).with(/.+oscdimg.exe$/).and_return(true) expect(subject.oscdimg_path).to match(/.+oscdimg.exe$/) end it "raises an error when not found" do expect(Vagrant::Util::Which).to receive(:which).and_return(false) expect(File).to receive(:executable?).with(/.+oscdimg.exe$/).and_return(false) expect { subject.oscdimg_path }.to raise_error(Vagrant::Errors::OscdimgCommandMissingError) end end end ================================================ FILE: test/unit/plugins/hosts/windows/cap/smb_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/windows/cap/smb" describe VagrantPlugins::HostWindows::Cap::SMB do let(:subject){ VagrantPlugins::HostWindows::Cap::SMB } let(:machine){ double(:machine, env: double(:machine_env, ui: Vagrant::UI::Silent.new)) } let(:env){ double(:env) } let(:options){ {} } let(:result){ Vagrant::Util::Subprocess::Result } let(:powershell_version){ "3" } let(:smblist){ <<-EOF Name : vgt-CUSTOM_ID-1 Path : /a/path Description : vgt-CUSTOM_ID-1 Name : vgt-CUSTOM_ID-2 Path : /other/path Description : vgt-CUSTOM_ID-2 Name : my-share Path : /my/path Description : Not Vagrant Owned Name : scoped-share Scope : * Path : /scoped/path Description : Scoped Path EOF } let(:netsharelist){ <<-EOF Share name Resource Remark ----------------------------------------------- vgt-CUSTOM_ID-1 /a/path vgt-CUSTOM_ID-1 vgt-CUSTOM_ID-2 /other/path vgt-CUSTOM_ID-2 my-share /my/path Not Vagran... The command completed successfully. EOF } let(:netshare1){ <<-EOF Share name vgt-CUSTOM_ID-1 Path /a/path Remark vgt-CUSTOM_ID-1 EOF } let(:netshare2){ <<-EOF Share name vgt-CUSTOM_ID-2 Path /other/path Remark vgt-CUSTOM_ID-2 EOF } let(:netshare_my){ <<-EOF Share name my-share Path /my/path Remark Not Vagrant Owned EOF } before do allow(subject).to receive(:machine_id).and_return("CUSTOM_ID") allow(Vagrant::Util::PowerShell).to receive(:version).and_return(powershell_version) allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return("") allow(subject).to receive(:sleep) end describe ".smb_mount_options" do it "should provide smb version of at least 2" do result = subject.smb_mount_options(nil) ver = result.detect{|i| i.start_with?("vers") }.to_s.split("=", 2).last.to_s.to_i expect(ver).to be >= 2 end end describe ".smb_installed" do context "when powershell version is greater than 2" do it "is valid installation" do expect(subject.smb_installed(nil)).to eq(true) end end context "when powershell version is less than 3" do let(:powershell_version){ "2" } it "is not a valid installation" do expect(subject.smb_installed(nil)).to eq(false) end end end describe ".smb_cleanup" do before do allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/Get-SmbShare/). and_return(smblist) allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/net share/).and_return(netsharelist) allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/net share vgt-CUSTOM_ID-1/).and_return(netshare1) allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/net share vgt-CUSTOM_ID-2/).and_return(netshare2) allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/net share my/).and_return(netshare_my) allow(Vagrant::Util::PowerShell).to receive(:execute).and_return(result.new(0, "", "")) end after{ subject.smb_cleanup(env, machine, options) } it "should pause after warning user" do expect(machine.env.ui).to receive(:warn).and_call_original expect(subject).to receive(:sleep) end it "should remove owned shares" do expect(Vagrant::Util::PowerShell).to receive(:execute) do |*args| expect(args).to include("vgt-CUSTOM_ID-1") expect(args).to include("vgt-CUSTOM_ID-2") result.new(0, "", "") end end it "should not remove owned shares" do expect(Vagrant::Util::PowerShell).to receive(:execute) do |*args| expect(args).not_to include("my-share") result.new(0, "", "") end end it "should remove all shares in single call" do expect(Vagrant::Util::PowerShell).to receive(:execute).with(any_args, sudo: true).once end context "when no shares are defined" do before do expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/Get-SmbShare/). and_return("") end it "should not attempt to remove shares" do expect(Vagrant::Util::PowerShell).not_to receive(:execute).with(any_args, sudo: true) end it "should not warn user" do expect(machine.env.ui).not_to receive(:warn) end end context "when Get-SmbShare is not available" do before do expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/Get-SmbShare/).and_return(nil) end it "should fetch list using net.exe" do expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/net share/).and_return("") end it "should remove owned shares" do expect(Vagrant::Util::PowerShell).to receive(:execute) do |*args| expect(args).to include("vgt-CUSTOM_ID-1") expect(args).to include("vgt-CUSTOM_ID-2") result.new(0, "", "") end end it "should not remove owned shares" do expect(Vagrant::Util::PowerShell).to receive(:execute) do |*args| expect(args).not_to include("my-share") result.new(0, "", "") end end end end describe ".smb_prepare" do let(:folders){ {"/first/path" => {hostpath: "/host/1"}, "/second/path" => {hostpath: "/host/2", smb_id: "ID1"}} } let(:options){ {} } before{ allow(Vagrant::Util::PowerShell).to receive(:execute).and_return(result.new(0, "", "")) } it "should add ID when not defined" do subject.smb_prepare(env, machine, folders, options) expect(folders["/first/path"][:smb_id]).to start_with("vgt-") end it "should not modify ID when defined" do subject.smb_prepare(env, machine, folders, options) expect(folders["/second/path"][:smb_id]).to eq("ID1") end it "should pause after warning user" do expect(machine.env.ui).to receive(:warn).and_call_original expect(subject).to receive(:sleep) subject.smb_prepare(env, machine, folders, options) end it "should add all shares in single call" do expect(Vagrant::Util::PowerShell).to receive(:execute).with(any_args, sudo: true).once subject.smb_prepare(env, machine, folders, options) end context "when share already exists" do let(:shares){ {"ID1" => {"Path" => "/host/2"}} } before do allow(File).to receive(:expand_path).and_call_original expect(subject).to receive(:existing_shares).and_return(shares) end it "should expand paths when comparing existing to requested" do expect(File).to receive(:expand_path).at_least(2).with("/host/2").and_return("expanded_path") subject.smb_prepare(env, machine, folders, options) end context "with different path" do let(:shares){ {"ID1" => {"Path" => "/host/3"}} } it "should raise an error" do expect{ subject.smb_prepare(env, machine, folders, options) }.to raise_error(VagrantPlugins::SyncedFolderSMB::Errors::SMBNameError) end end end context "when no shared are defined" do after{ subject.smb_prepare(env, machine, {}, options) } it "should not attempt to add shares" do expect(Vagrant::Util::PowerShell).not_to receive(:execute).with(any_args, sudo: true) end it "should not warn user" do expect(machine.env.ui).not_to receive(:warn) end end context "when more than 10 shares are defined" do let(:folders) { Hash[12.times.map{|i| ["/path#{i}", {hostpath: "/host#{i}"}]}] } after{ subject.smb_prepare(env, machine, folders, options) } it "should execute multiple powershell commands" do expect(Vagrant::Util::PowerShell).to receive(:execute).twice.with(any_args, sudo: true) end end end describe ".get_smbshares" do before { expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return(smblist) } it "should return a Hash of share information" do expect(subject.get_smbshares).to be_a(Hash) end it "should provide name and description for share" do shares = subject.get_smbshares expect(shares["vgt-CUSTOM_ID-1"]).to be_a(Hash) expect(shares["vgt-CUSTOM_ID-1"]["Path"]).to eq("/a/path") expect(shares["vgt-CUSTOM_ID-1"]["Description"]).to eq("vgt-CUSTOM_ID-1") end it "should properly handle share with scope information" do shares = subject.get_smbshares expect(shares["scoped-share"]).to be_a(Hash) expect(shares["scoped-share"]["Path"]).to eq("/scoped/path") expect(shares["scoped-share"]["Description"]).to eq("Scoped Path") end end end ================================================ FILE: test/unit/plugins/hosts/windows/cap/ssh_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/hosts/windows/cap/ssh" describe VagrantPlugins::HostWindows::Cap::SSH do let(:subject){ VagrantPlugins::HostWindows::Cap::SSH } let(:result){ Vagrant::Util::Subprocess::Result.new(exit_code, stdout, stderr) } let(:exit_code){ 0 } let(:stdout){ "" } let(:stderr){ "" } let(:key_path){ double("keypath", to_s: "keypath") } let(:env){ double("env") } before do allow(Vagrant::Util::PowerShell).to receive(:execute).and_return(result) end it "should execute PowerShell script" do expect(Vagrant::Util::PowerShell).to receive(:execute).with( /set_ssh_key_permissions.ps1/, "-KeyPath", key_path.to_s, any_args ).and_return(result) subject.set_ssh_key_permissions(env, key_path) end it "should return the result" do expect(subject.set_ssh_key_permissions(env, key_path)).to eq(result) end context "when command fails" do let(:exit_code){ 1 } it "should raise an error" do expect{ subject.set_ssh_key_permissions(env, key_path) }.to raise_error(Vagrant::Errors::PowerShellError) end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/cloud_init_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/cloud_init") describe VagrantPlugins::Kernel_V2::VagrantConfigCloudInit do include_context "unit" subject { described_class.new(:user_data) } let(:provider) { double("provider") } let(:machine) { double("machine", name: "rspec", provider: provider, env: Vagrant::Environment.new) } def assert_invalid errors = subject.validate(machine) if errors.empty? raise "No errors: #{errors.inspect}" end end def assert_valid errors = subject.validate(machine) if !errors.empty? raise "Errors: #{errors.inspect}" end end before do env = double("env") subject.content_type = "text/cloud-config" subject.inline = <<-CONFIG package_update: true CONFIG end describe "#validate" do context "with defaults" do it "is a valid config" do subject.finalize! assert_valid end it "sets a content_type" do subject.finalize! expect(subject.content_type).to eq("text/cloud-config") end context "with no type set" do let(:type_subject) { described_class.new } before do type_subject.content_type = "text/cloud-config" type_subject.inline = <<-CONFIG package_update: true CONFIG end it "defaults to a type" do type_subject.finalize! expect(type_subject.type).to eq(:user_data) end end end context "with an invalid option set" do before do subject.content_type = "text/not-real-option" end it "is an invalid config" do subject.finalize! assert_invalid end end context "with both path and inline set" do before do subject.path = "path/to/option" subject.inline = "package_update: true" end it "is an invalid config" do subject.finalize! assert_invalid end end context "with inline set as an invalid type" do before do subject.path = :i_am_a_symbol end it "is an invalid config" do subject.finalize! assert_invalid end end context "with path set as an invalid type" do before do subject.inline = :i_am_a_symbol end it "is an invalid config" do subject.finalize! assert_invalid end end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/disk_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/disk") describe VagrantPlugins::Kernel_V2::VagrantConfigDisk do include_context "unit" let(:type) { :disk } subject { described_class.new(type) } let(:ui) { Vagrant::UI::Silent.new } let(:env) { double("env", ui: ui) } let(:provider) { double("provider") } let(:machine) { double("machine", name: "name", provider: provider, env: env, provider_name: :virtualbox) } def assert_invalid errors = subject.validate(machine) if errors.empty? raise "No errors: #{errors.inspect}" end end def assert_valid errors = subject.validate(machine) if !errors.empty? raise "Errors: #{errors.inspect}" end end before do allow(provider).to receive(:capability?).with(:validate_disk_ext).and_return(true) allow(provider).to receive(:capability).with(:validate_disk_ext, "vdi").and_return(true) allow(provider).to receive(:capability?).with(:set_default_disk_ext).and_return(true) allow(provider).to receive(:capability).with(:set_default_disk_ext).and_return("vdi") end describe "with defaults" do before do subject.name = "foo" subject.size = 100 end it "is valid with test defaults" do subject.finalize! assert_valid end it "sets a disk type" do subject.finalize! expect(subject.type).to eq(type) end it "defaults to non-primary disk" do subject.finalize! expect(subject.primary).to eq(false) end end describe "with an invalid config" do before do subject.name = "bar" end it "raises an error if size not set" do subject.finalize! assert_invalid end context "with an invalid disk extension" do before do subject.size = 100 subject.disk_ext = "fake" allow(provider).to receive(:capability?).with(:validate_disk_ext).and_return(true) allow(provider).to receive(:capability).with(:validate_disk_ext, "fake").and_return(false) allow(provider).to receive(:capability?).with(:default_disk_exts).and_return(true) allow(provider).to receive(:capability).with(:default_disk_exts).and_return(["vdi", "vmdk"]) end it "raises an error" do subject.finalize! assert_invalid end end end describe "config for dvd type" do let(:iso_path) { "/tmp/untitled.iso" } before do subject.type = :dvd subject.name = "untitled" allow(File).to receive(:file?).with(iso_path).and_return(true) subject.file = iso_path end it "is valid with test defaults" do subject.finalize! assert_valid end it "is invalid if file path is unset" do subject.file = nil subject.finalize! assert_invalid end it "is invalid if primary" do subject.primary = true subject.finalize! assert_invalid end end describe "#add_provider_config" do it "normalizes provider config" do test_provider_config = {provider__something: "special" } subject.add_provider_config(**test_provider_config) expect(subject.provider_config).to eq( { provider: {something: "special" }} ) end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/package_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/package") describe VagrantPlugins::Kernel_V2::PackageConfig do subject { described_class.new } describe "#name" do it "defaults to nil" do subject.finalize! expect(subject.name).to be_nil end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/push_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/push") describe VagrantPlugins::Kernel_V2::PushConfig do include_context "unit" subject { described_class.new } describe "#define" do let(:pushes) { subject.instance_variable_get(:@__defined_pushes) } it "pushes the strategy and block onto the defined pushes array" do subject.define("foo") { "bar" } subject.define("foo") { "zip" } subject.define("foo") { "zap" } expect(pushes.size).to eq(1) expect(pushes[:foo].size).to eq(3) expect(pushes[:foo][0]).to be_a(Array) expect(pushes[:foo][0][0]).to eq(:foo) expect(pushes[:foo][0][1]).to be_a(Proc) end context "when no strategy is given" do it "defaults to the name" do subject.define("foo") { "bar" } expect(pushes.size).to eq(1) expect(pushes[:foo].size).to eq(1) expect(pushes[:foo][0]).to be_a(Array) expect(pushes[:foo][0][0]).to eq(:foo) expect(pushes[:foo][0][1]).to be_a(Proc) end end context "when a strategy is given" do it "uses the strategy" do subject.define("foo", strategy: "bacon") { "bar" } expect(pushes.size).to eq(1) expect(pushes[:foo].size).to eq(1) expect(pushes[:foo][0]).to be_a(Array) expect(pushes[:foo][0][0]).to eq(:bacon) expect(pushes[:foo][0][1]).to be_a(Proc) end end end describe "#merge" do it "appends defined pushes" do a = described_class.new.tap do |i| i.define("foo") { "bar" } i.define("bar") { "bar" } end b = described_class.new.tap do |i| i.define("foo") { "zip" } end result = a.merge(b) pushes = result.instance_variable_get(:@__defined_pushes) expect(pushes[:foo]).to be_a(Array) expect(pushes[:foo].size).to eq(2) expect(pushes[:bar]).to be_a(Array) expect(pushes[:bar].size).to eq(1) end end describe "#__compiled_pushes" do it "raises an exception if not finalized" do subject.instance_variable_set(:@__finalized, false) expect { subject.__compiled_pushes }.to raise_error(RuntimeError) end it "returns a copy of the compiled pushes" do pushes = { foo: "bar" } subject.instance_variable_set(:@__finalized, true) subject.instance_variable_set(:@__compiled_pushes, pushes) expect(subject.__compiled_pushes).to_not be(pushes) expect(subject.__compiled_pushes).to eq(pushes) end end describe "#finalize!" do let(:pushes) { a.merge(b).tap { |r| r.finalize! }.__compiled_pushes } let(:key) { pushes[:foo][0] } let(:config) { pushes[:foo][1] } let(:unset) { Vagrant.plugin("2", :config).const_get(:UNSET_VALUE) } let(:dummy_klass) { Vagrant::Config::V2::DummyConfig } before do register_plugin("2") do |plugin| plugin.name "foo" plugin.push(:foo) do Class.new(Vagrant.plugin("2", :push)) end plugin.config(:foo, :push) do Class.new(Vagrant.plugin("2", :config)) do attr_accessor :bar attr_accessor :zip def initialize @bar = self.class.const_get(:UNSET_VALUE) @zip = self.class.const_get(:UNSET_VALUE) end end end end end it "compiles the proper configuration with a single strategy" do instance = described_class.new.tap do |i| i.define "foo" end instance.finalize! pushes = instance.__compiled_pushes strategy, config = pushes[:foo] expect(strategy).to eq(:foo) expect(config.bar).to be(unset) end it "compiles the proper configuration with a single strategy and block" do instance = described_class.new.tap do |i| i.define "foo" do |b| b.bar = 42 end end instance.finalize! pushes = instance.__compiled_pushes strategy, config = pushes[:foo] expect(strategy).to eq(:foo) expect(config.bar).to eq(42) end it "compiles the proper config with a name and explicit strategy" do instance = described_class.new.tap do |i| i.define "bar", strategy: "foo" end instance.finalize! pushes = instance.__compiled_pushes strategy, config = pushes[:bar] expect(strategy).to eq(:foo) expect(config.bar).to be(unset) end it "compiles the proper config with a name and explicit strategy with block" do instance = described_class.new.tap do |i| i.define "bar", strategy: "foo" do |b| b.bar = 42 end end instance.finalize! pushes = instance.__compiled_pushes strategy, config = pushes[:bar] expect(strategy).to eq(:foo) expect(config.bar).to eq(42) end context "with the same name but different strategy" do context "with no block" do let(:a) do described_class.new.tap do |i| i.define("foo", strategy: "bar") end end let(:b) do described_class.new.tap do |i| i.define("foo", strategy: "zip") end end it "chooses the last config" do expect(key).to eq(:zip) expect(config).to be_kind_of(dummy_klass) end end context "with a block" do let(:a) do described_class.new.tap do |i| i.define("foo", strategy: "bar") do |p| p.bar = "a" end end end let(:b) do described_class.new.tap do |i| i.define("foo", strategy: "zip") do |p| p.zip = "b" end end end it "chooses the last config" do expect(key).to eq(:zip) expect(config).to be_kind_of(dummy_klass) end end context "with a block, then no block" do let(:a) do described_class.new.tap do |i| i.define("foo", strategy: "bar") do |p| p.bar, p.zip = "a", "a" end end end let(:b) do described_class.new.tap do |i| i.define("foo", strategy: "zip") end end it "chooses the last config" do expect(key).to eq(:zip) expect(config).to be_kind_of(dummy_klass) end end context "with no block, then a block" do let(:a) do described_class.new.tap do |i| i.define("foo", strategy: "bar") end end let(:b) do described_class.new.tap do |i| i.define("foo", strategy: "zip") do |p| p.bar, p.zip = "b", "b" end end end it "chooses the last config" do expect(key).to eq(:zip) expect(config).to be_kind_of(dummy_klass) end end end context "with the same name twice" do context "with no block" do let(:a) do described_class.new.tap do |i| i.define("foo") end end let(:b) do described_class.new.tap do |i| i.define("foo") end end it "merges the configs" do expect(key).to eq(:foo) expect(config.bar).to be(unset) expect(config.zip).to be(unset) end end context "with a block" do let(:a) do described_class.new.tap do |i| i.define("foo") do |p| p.bar = "a" end end end let(:b) do described_class.new.tap do |i| i.define("foo") do |p| p.zip = "b" end end end it "merges the configs" do expect(key).to eq(:foo) expect(config.bar).to eq("a") expect(config.zip).to eq("b") end end context "with a block, then no block" do let(:a) do described_class.new.tap do |i| i.define("foo") do |p| p.bar = "a" end end end let(:b) do described_class.new.tap do |i| i.define("foo") end end it "merges the configs" do expect(key).to eq(:foo) expect(config.bar).to eq("a") expect(config.zip).to be(unset) end end context "with no block, then a block" do let(:a) do described_class.new.tap do |i| i.define("foo", strategy: "bar") end end let(:b) do described_class.new.tap do |i| i.define("foo", strategy: "zip") do |p| p.zip = "b" end end end it "merges the configs" do expect(key).to eq(:zip) expect(config).to be_kind_of(dummy_klass) end end end it "sets @__finalized to true" do subject.finalize! expect(subject.instance_variable_get(:@__finalized)).to be(true) end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/ssh_connect_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/ssh_connect") describe VagrantPlugins::Kernel_V2::SSHConnectConfig 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } subject { described_class.new } describe "#verify_host_key" do it "defaults to :never" do subject.finalize! expect(subject.verify_host_key).to eq(:never) end it "should modify true value to :accepts_new_or_local_tunnel" do subject.verify_host_key = true subject.finalize! expect(subject.verify_host_key).to eq(:accepts_new_or_local_tunnel) end it "should modify :very value to :accept_new" do subject.verify_host_key = :very subject.finalize! expect(subject.verify_host_key).to eq(:accept_new) end it "should modify :secure to :always" do subject.verify_host_key = :secure subject.finalize! expect(subject.verify_host_key).to eq(:always) end end describe "#key_type" do it "defaults to :auto" do subject.finalize! expect(subject.key_type).to eq(:auto) end it "should allow supported key type" do subject.key_type = :ed25519 subject.finalize! errors = subject.validate(machine) expect(errors).to be_empty end it "should not allow unsupported key type" do subject.key_type = :unknown_type subject.finalize! errors = subject.validate(machine) expect(errors).not_to be_empty end it "should convert string values to symbol" do subject.key_type = "ecdsa521" subject.finalize! expect(subject.key_type).to eq(:ecdsa521) end end describe "#config" do let(:config_file) { "/path/to/config" } before do # NOTE: The machine instance must be initialized before # any mocks on File are registered. Otherwise it # will cause a failure attempting to create the # instance machine allow(File).to receive(:file?). with(/#{Regexp.escape(config_file)}/). and_return(true) end it "defaults to nil" do subject.finalize! expect(subject.config).to be_nil end it "should return the set path" do subject.config = config_file subject.finalize! expect(subject.config).to eq(config_file) end it "should validate when path exists" do subject.config = config_file subject.finalize! machine expect(File).to receive(:file?). with(/#{Regexp.escape(config_file)}/). and_return(true) expect(subject.validate(machine)).to be_empty end it "should not validate when path does not exist" do subject.config = config_file subject.finalize! expect(File).to receive(:file?). with(/#{Regexp.escape(config_file)}/). and_return(false) expect(subject.validate(machine)).not_to be_empty end end describe "#remote_user" do let(:username) { double("username") } let(:remote_user) { double("remote_user") } it "should default to username value" do subject.username = username subject.finalize! expect(subject.remote_user).to eq(subject.username) end it "should be set to provided value" do subject.username = username subject.remote_user = remote_user subject.finalize! expect(subject.remote_user).to eq(remote_user) end end describe "#connect_timeout" do let(:timeout_value) { 1 } it "should default to the default value" do subject.finalize! expect(subject.connect_timeout). to eq(described_class.const_get(:DEFAULT_SSH_CONNECT_TIMEOUT)) end it "should be set to provided value" do subject.connect_timeout = timeout_value subject.finalize! expect(subject.connect_timeout).to eq(timeout_value) end it "should cast given value to integer" do subject.connect_timeout = timeout_value.to_s subject.finalize! expect(subject.connect_timeout).to eq(timeout_value) end it "should properly validate" do subject.connect_timeout = timeout_value subject.finalize! expect(subject.validate(machine)).to be_empty end context "when value cannot be cast" do let(:timeout_value) { :value } it "should not raise an error" do subject.connect_timeout = timeout_value expect { subject.finalize! }.not_to raise_error end it "should not validate" do subject.connect_timeout = timeout_value subject.finalize! expect(subject.validate(machine)).not_to be_empty end end context "when value is less than 1" do let(:timeout_value) { 0 } it "should not raise an error" do subject.connect_timeout = timeout_value expect { subject.finalize! }.not_to raise_error end it "should not validate" do subject.connnect_timeout = timeout_value subject.finalize! expect(subject.validate(machine)).not_to be_empty end end end describe "#connect_retries" do let(:retry_value) { 1 } it "should default to the default value" do subject.finalize! expect(subject.connect_retries). to eq(described_class.const_get(:DEFAULT_SSH_CONNECT_RETRIES)) end it "should be set to the provided value" do subject.connect_retries = retry_value subject.finalize! expect(subject.connect_retries).to eq(retry_value) end it "should properly validate" do subject.connect_retries = retry_value subject.finalize! expect(subject.validate(machine)).to be_empty end context "when value is a float" do let(:retry_value) { 2.3 } it "should not raise an error" do subject.connect_retries = retry_value expect { subject.finalize! }.not_to raise_error end it "should not validate" do subject.connect_retries = retry_value subject.finalize! expect(subject.validate(machine)).not_to be_empty end end context "when value is not numeric" do let(:retry_value) { "2" } it "should not raise an error" do subject.connect_retries = retry_value expect { subject.finalize! }.not_to raise_error end it "should not validate" do subject.connect_retries = retry_value subject.finalize! expect(subject.validate(machine)).not_to be_empty end end context "when value is less than 0" do let(:retry_value) { -1 } it "should not validate" do subject.connect_retries = retry_value subject.finalize! expect(subject.validate(machine)).not_to be_empty end end end describe "#connect_retry_delay" do let(:delay_value) { 1 } it "should default to the default value" do subject.finalize! expect(subject.connect_retry_delay). to eq(described_class.const_get(:DEFAULT_SSH_CONNECT_RETRY_DELAY)) end it "should be set to the provided value" do subject.connect_retry_delay = delay_value subject.finalize! expect(subject.connect_retry_delay).to eq(delay_value) end it "should properly validate" do subject.connect_retry_delay = delay_value subject.finalize! expect(subject.validate(machine)).to be_empty end context "when value is a float" do let(:delay_value) { 2.3 } it "should validate" do subject.connect_retry_delay = delay_value subject.finalize! expect(subject.validate(machine)).to be_empty end end context "when value is not numeric" do let(:delay_value) { "2" } it "should not raise an error" do subject.connect_retry_delay = delay_value expect { subject.finalize! }.not_to raise_error end it "should not validate" do subject.connect_retry_delay = delay_value subject.finalize! expect(subject.validate(machine)).not_to be_empty end end context "when value is less than 0" do let(:delay_value) { -1 } it "should not validate" do subject.connect_retry_delay = delay_value subject.finalize! expect(subject.validate(machine)).not_to be_empty end end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/ssh_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/ssh") describe VagrantPlugins::Kernel_V2::SSHConfig do subject { described_class.new } describe "#default" do it "defaults to vagrant username" do subject.finalize! expect(subject.default.port).to eq(22) expect(subject.default.username).to eq("vagrant") end end describe "#sudo_command" do it "defaults properly" do subject.finalize! expect(subject.sudo_command).to eq("sudo -E -H %c") end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/trigger_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/trigger") describe VagrantPlugins::Kernel_V2::TriggerConfig do include_context "unit" subject { described_class.new } let(:machine) { double("machine") } def assert_invalid errors = subject.validate(machine) if !errors.values.any? { |v| !v.empty? } raise "No errors: #{errors.inspect}" end end def assert_valid errors = subject.validate(machine) if !errors.values.all? { |v| v.empty? } raise "Errors: #{errors.inspect}" end end before do env = double("env") allow(env).to receive(:root_path).and_return(nil) allow(machine).to receive(:env).and_return(env) allow(machine).to receive(:provider_config).and_return(nil) allow(machine).to receive(:provider_options).and_return({}) end it "is valid with test defaults" do subject.finalize! assert_valid end let (:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} } let (:splat) { [:up, :destroy, :halt] } let (:arr) { [[:up, :destroy, :halt]] } describe "creating a before trigger" do it "creates a trigger with the splat syntax" do subject.before(:up, hash_block) bf_trigger = subject.instance_variable_get(:@_before_triggers) expect(bf_trigger.size).to eq(1) expect(bf_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) end it "creates a trigger with the array syntax" do subject.before([:up], hash_block) bf_trigger = subject.instance_variable_get(:@_before_triggers) expect(bf_trigger.size).to eq(1) expect(bf_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) end it "creates a trigger with the block syntax" do subject.before :up do |trigger| trigger.name = "rspec" end bf_trigger = subject.instance_variable_get(:@_before_triggers) expect(bf_trigger.size).to eq(1) expect(bf_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) end it "creates multiple triggers with the splat syntax" do subject.before(splat, hash_block) bf_trigger = subject.instance_variable_get(:@_before_triggers) expect(bf_trigger.size).to eq(3) bf_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } end it "creates multiple triggers with the block syntax" do subject.before splat do |trigger| trigger.name = "rspec" end bf_trigger = subject.instance_variable_get(:@_before_triggers) expect(bf_trigger.size).to eq(3) bf_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } end it "creates multiple triggers with the array syntax" do subject.before(arr, hash_block) bf_trigger = subject.instance_variable_get(:@_before_triggers) expect(bf_trigger.size).to eq(3) bf_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } end end describe "creating an after trigger" do it "creates a trigger with the splat syntax" do subject.after(:up, hash_block) af_trigger = subject.instance_variable_get(:@_after_triggers) expect(af_trigger.size).to eq(1) expect(af_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) end it "creates a trigger with the array syntax" do subject.after([:up], hash_block) af_trigger = subject.instance_variable_get(:@_after_triggers) expect(af_trigger.size).to eq(1) expect(af_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) end it "creates a trigger with the block syntax" do subject.after :up do |trigger| trigger.name = "rspec" end af_trigger = subject.instance_variable_get(:@_after_triggers) expect(af_trigger.size).to eq(1) expect(af_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) end it "creates multiple triggers with the splat syntax" do subject.after(splat, hash_block) af_trigger = subject.instance_variable_get(:@_after_triggers) expect(af_trigger.size).to eq(3) af_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } end it "creates multiple triggers with the block syntax" do subject.after splat do |trigger| trigger.name = "rspec" end af_trigger = subject.instance_variable_get(:@_after_triggers) expect(af_trigger.size).to eq(3) af_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } end it "creates multiple triggers with the array syntax" do subject.after(arr, hash_block) af_trigger = subject.instance_variable_get(:@_after_triggers) expect(af_trigger.size).to eq(3) af_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } end end describe "#create_trigger" do let(:command) { :up } let(:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} } it "returns a new VagrantConfigTrigger object if given a hash" do trigger = subject.create_trigger(command, hash_block) expect(trigger).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) end it "returns a new VagrantConfigTrigger object if given a block" do block = Proc.new { |b| b.info = "test"} trigger = subject.create_trigger(command, block) expect(trigger).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) end end describe "#merge" do it "merges defined triggers" do a = described_class.new() b = described_class.new() a.before(splat, hash_block) a.after(arr, hash_block) b.before(splat, hash_block) b.after(arr, hash_block) result = a.merge(b) bf_trigger = result.instance_variable_get(:@_before_triggers) af_trigger = result.instance_variable_get(:@_after_triggers) expect(bf_trigger).to be_a(Array) expect(af_trigger).to be_a(Array) expect(bf_trigger.size).to eq(6) expect(af_trigger.size).to eq(6) end it "merges the other triggers if a class is empty" do a = described_class.new() b = described_class.new() a.before(splat, hash_block) a.after(arr, hash_block) b_bf_trigger = b.instance_variable_get(:@_before_triggers) b_af_trigger = b.instance_variable_get(:@_after_triggers) result = a.merge(b) bf_trigger = result.instance_variable_get(:@_before_triggers) af_trigger = result.instance_variable_get(:@_after_triggers) expect(bf_trigger.size).to eq(3) expect(af_trigger.size).to eq(3) end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/vagrant_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/vagrant") describe VagrantPlugins::Kernel_V2::VagrantConfig do subject { described_class.new } let(:machine){ double("machine") } describe "#host" do it "defaults to :detect" do subject.finalize! expect(subject.host).to eq(:detect) end it "symbolizes" do subject.host = "foo" subject.finalize! expect(subject.host).to eq(:foo) end end describe "#sensitive" do after{ Vagrant::Util::CredentialScrubber.reset! } it "accepts string value" do subject.sensitive = "test" subject.finalize! expect(subject.sensitive).to eq("test") end it "accepts array of values" do subject.sensitive = ["test1", "test2"] subject.finalize! expect(subject.sensitive).to eq(["test1", "test2"]) end it "does not accept non-string values" do subject.sensitive = 1 subject.finalize! result = subject.validate(machine) expect(result).to be_a(Hash) expect(result.values).not_to be_empty end it "registers single sensitive value to be scrubbed" do subject.sensitive = "test" expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with("test") subject.finalize! end it "registers multiple sensitive values to be scrubbed" do subject.sensitive = ["test1", "test2"] expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with("test1") expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with("test2") subject.finalize! end end describe "#plugins" do it "converts string into hash of plugins" do subject.plugins = "vagrant-plugin" subject.finalize! expect(subject.plugins).to be_a(Hash) end it "converts array of strings into hash of plugins" do subject.plugins = ["vagrant-plugin", "vagrant-other-plugin"] subject.finalize! expect(subject.plugins).to be_a(Hash) expect(subject.plugins.keys).to eq(["vagrant-plugin", "vagrant-other-plugin"]) end it "does not convert hash" do plugins = {"vagrant-plugin" => {}} subject.plugins = plugins subject.finalize expect(subject.plugins).to eq(plugins) end it "converts array of mixed strings and hashes" do subject.plugins = ["vagrant-plugin", {"vagrant-other-plugin" => {:version => "1"}}] subject.finalize! expect(subject.plugins["vagrant-plugin"]).to eq({}) expect(subject.plugins["vagrant-other-plugin"]).to eq({"version" => "1"}) end it "generates a validation error when incorrect type is provided" do subject.plugins = 0 subject.finalize! result = subject.validate(machine) expect(result.values).not_to be_empty end it "generates a validation error when invalid option is provided" do subject.plugins = {"vagrant-plugin" => {"badkey" => true}} subject.finalize! result = subject.validate(machine) expect(result.values).not_to be_empty end it "generates a validation error when options are incorrect type" do subject.plugins = {"vagrant-plugin" => 1} subject.finalize! result = subject.validate(machine) expect(result.values).not_to be_empty end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/vm_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/vm") describe VagrantPlugins::Kernel_V2::VMConfig do include_context "unit" subject { described_class.new } let(:provider) { double("provider") } let(:machine) { double("machine", provider: provider, provider_name: "provider", name: "default") } def assert_invalid errors = subject.validate(machine) if !errors.values.any? { |v| !v.empty? } raise "No errors: #{errors.inspect}" end end def assert_valid errors = subject.validate(machine) if !errors.values.all? { |v| v.empty? } raise "Errors: #{errors.inspect}" end end def find_network(name) network_definitions = subject.networks.map do |n| n[1] end network_definitions.find {|n| n[:id] == name} end before do env = double("env") allow(env).to receive(:root_path).and_return(nil) allow(machine).to receive(:env).and_return(env) allow(machine).to receive(:provider_config).and_return(nil) allow(machine).to receive(:provider_options).and_return({}) allow(machine).to receive_message_chain(:synced_folders, :types).and_return( {} ) allow(provider).to receive(:capability?).with(:validate_disk_ext).and_return(true) allow(provider).to receive(:capability).with(:validate_disk_ext, "vdi").and_return(true) allow(provider).to receive(:capability?).with(:set_default_disk_ext).and_return(true) allow(provider).to receive(:capability).with(:set_default_disk_ext).and_return("vdi") subject.box = "foo" end it "is valid with test defaults" do subject.finalize! assert_valid end it "validates disables_host_modification option" do subject.allow_hosts_modification = true subject.finalize! assert_valid subject.allow_hosts_modification = false subject.finalize! assert_valid subject.allow_hosts_modification = "truthy" subject.finalize! assert_invalid end it "does not check for fstab caps if already set" do expect(machine).to_not receive(:synced_folder_types) subject.allow_fstab_modification = true subject.finalize! assert_valid end describe "#base_mac" do it "defaults properly" do subject.finalize! expect(subject.base_mac).to be_nil end end describe "#base_address" do it "defaults properly" do subject.finalize! expect(subject.base_address).to be_nil end end describe "#box" do it "is required" do subject.box = nil subject.finalize! assert_invalid end it "cannot be an empty string" do subject.box = "" subject.finalize! assert_invalid end it "is not required if the provider says so" do machine.provider_options[:box_optional] = true subject.box = nil subject.finalize! assert_valid end it "is invalid if clone is set" do subject.clone = "foo" subject.finalize! assert_invalid end end describe "#box_architecture" do it "is not required" do subject.box_architecture = nil subject.finalize! assert_valid end it "is :auto by default" do subject.finalize! assert_valid expect(subject.box_architecture).to eq(:auto) end it "can be set to custom value" do subject.box_architecture = "test-arch" subject.finalize! assert_valid expect(subject.box_architecture).to eq("test-arch") end it "is converted to string" do subject.box_architecture = :test_arch subject.finalize! assert_valid expect(subject.box_architecture).to eq("test_arch") end end context "#box_check_update" do it "defaults to true" do with_temp_env("VAGRANT_BOX_UPDATE_CHECK_DISABLE" => "") do subject.finalize! expect(subject.box_check_update).to be(true) end end it "is false if VAGRANT_BOX_UPDATE_CHECK_DISABLE is set" do with_temp_env("VAGRANT_BOX_UPDATE_CHECK_DISABLE" => "1") do subject.finalize! expect(subject.box_check_update).to be(false) end end end describe "#box_url" do it "defaults to nil" do subject.finalize! expect(subject.box_url).to be_nil end it "turns into an array" do subject.box_url = "foo" subject.finalize! expect(subject.box_url).to eq( ["foo"]) end it "keeps in array" do subject.box_url = ["foo", "bar"] subject.finalize! expect(subject.box_url).to eq( ["foo", "bar"]) end end context "#box_version" do it "defaults to nil" do subject.finalize! expect(subject.box_version).to be_nil end it "errors if invalid version" do subject.box_version = "nope" subject.finalize! expect { assert_valid }.to raise_error(RuntimeError) end it "can have complex constraints" do subject.box_version = ">= 0, ~> 1.0" subject.finalize! assert_valid end ["1", 1, "1.0", 1.0].each do |valid| it "is valid: #{valid}" do subject.box_version = valid subject.finalize! assert_valid end end end describe "#communicator" do it "is nil by default" do subject.finalize! expect(subject.communicator).to be_nil end end describe "#guest" do it "is nil by default" do subject.finalize! expect(subject.guest).to be_nil end it "is symbolized" do subject.guest = "foo" subject.finalize! expect(subject.guest).to eq(:foo) end end describe "#hostname" do ["a", "foo", "foo-bar", "baz0"].each do |valid| it "is valid: #{valid}" do subject.hostname = valid subject.finalize! assert_valid end end end describe "#network(s)" do it "defaults to forwarding SSH by default" do subject.finalize! n = subject.networks expect(n.length).to eq(1) expect(n[0][0]).to eq(:forwarded_port) expect(n[0][1][:guest]).to eq(22) expect(n[0][1][:host]).to eq(2222) expect(n[0][1][:host_ip]).to eq("127.0.0.1") expect(n[0][1][:id]).to eq("ssh") end it "defaults to forwarding WinRM if communicator is winrm" do subject.communicator = "winrm" subject.finalize! n = subject.networks expect(n.length).to eq(3) expect(n[0][0]).to eq(:forwarded_port) expect(n[0][1][:guest]).to eq(5985) expect(n[0][1][:host]).to eq(55985) expect(n[0][1][:host_ip]).to eq("127.0.0.1") expect(n[0][1][:id]).to eq("winrm") expect(n[1][0]).to eq(:forwarded_port) expect(n[1][1][:guest]).to eq(5986) expect(n[1][1][:host]).to eq(55986) expect(n[1][1][:host_ip]).to eq("127.0.0.1") expect(n[1][1][:id]).to eq("winrm-ssl") end it "forwards ssh even if the communicator is winrm" do subject.communicator = "winrm" subject.finalize! n = subject.networks expect(n.length).to eq(3) expect(n[0][0]).to eq(:forwarded_port) expect(n[0][1][:guest]).to eq(5985) expect(n[0][1][:host]).to eq(55985) expect(n[0][1][:host_ip]).to eq("127.0.0.1") expect(n[0][1][:id]).to eq("winrm") expect(n[1][0]).to eq(:forwarded_port) expect(n[1][1][:guest]).to eq(5986) expect(n[1][1][:host]).to eq(55986) expect(n[1][1][:host_ip]).to eq("127.0.0.1") expect(n[1][1][:id]).to eq("winrm-ssl") expect(n[2][0]).to eq(:forwarded_port) expect(n[2][1][:guest]).to eq(22) expect(n[2][1][:host]).to eq(2222) expect(n[2][1][:host_ip]).to eq("127.0.0.1") expect(n[2][1][:id]).to eq("ssh") end it "allows overriding SSH" do subject.network "forwarded_port", guest: 22, host: 14100, id: "ssh" subject.finalize! n = subject.networks expect(n.length).to eq(1) expect(n[0][0]).to eq(:forwarded_port) expect(n[0][1][:guest]).to eq(22) expect(n[0][1][:host]).to eq(14100) expect(n[0][1][:id]).to eq("ssh") end it "allows overriding WinRM" do subject.communicator = :winrm subject.network "forwarded_port", guest: 5985, host: 14100, id: "winrm" subject.finalize! winrm_network = find_network 'winrm' expect(winrm_network[:guest]).to eq(5985) expect(winrm_network[:host]).to eq(14100) expect(winrm_network[:id]).to eq("winrm") end it "allows overriding WinRM SSL" do subject.communicator = :winrm subject.network "forwarded_port", guest: 5986, host: 14100, id: "winrm-ssl" subject.finalize! winrmssl_network = find_network 'winrm-ssl' expect(winrmssl_network[:guest]).to eq(5986) expect(winrmssl_network[:host]).to eq(14100) expect(winrmssl_network[:id]).to eq("winrm-ssl") end it "turns all forwarded port ports to ints" do subject.network "forwarded_port", guest: "45", host: "4545", id: "test" subject.finalize! n = subject.networks.find do |type, data| type == :forwarded_port && data[:id] == "test" end expect(n).to_not be_nil expect(n[1][:guest]).to eq(45) expect(n[1][:host]).to eq(4545) end it "is an error if forwarding a port too low" do subject.network "forwarded_port", guest: "45", host: "-5" subject.finalize! assert_invalid end it "is an error if forwarding a port too high" do subject.network "forwarded_port", guest: "45", host: "74545" subject.finalize! assert_invalid end it "is an error if multiple networks set hostname" do subject.network "public_network", ip: "192.168.0.1", hostname: true subject.network "public_network", ip: "192.168.0.2", hostname: true subject.finalize! assert_invalid end it "is an error if networks set hostname without ip" do subject.network "public_network", hostname: true subject.finalize! assert_invalid end it "is not an error if hostname non-bool" do subject.network "public_network", ip: "192.168.0.1", hostname: "true" subject.finalize! assert_valid end it "is not an error if one hostname is true" do subject.network "public_network", ip: "192.168.0.1", hostname: true subject.network "public_network", ip: "192.168.0.2", hostname: false subject.finalize! assert_valid end end describe "#post_up_message" do it "defaults to empty string" do subject.finalize! expect(subject.post_up_message).to eq("") end it "can be set" do subject.post_up_message = "foo" subject.finalize! expect(subject.post_up_message).to eq("foo") end end describe "#provider and #__providers" do it "returns the providers in order" do subject.provider "foo" subject.provider "bar" subject.finalize! expect(subject.__providers).to eq([:foo, :bar]) end describe "merging" do it "prioritizes new orders in later configs" do subject.provider "foo" other = described_class.new other.provider "bar" merged = subject.merge(other) expect(merged.__providers).to eq([:foo, :bar]) end it "prioritizes duplicates in new orders in later configs" do subject.provider "foo" other = described_class.new other.provider "bar" other.provider "foo" merged = subject.merge(other) expect(merged.__providers).to eq([:foo, :bar]) end end end describe "#provider and #get_provider_config" do it "compiles the configurations for a provider" do subject.provider "virtualbox" do |vb| vb.gui = true end subject.provider "virtualbox" do |vb| vb.name = "foo" end subject.finalize! config = subject.get_provider_config(:virtualbox) expect(config.name).to eq("foo") expect(config.gui).to be(true) end it "raises an exception if there is a problem loading" do subject.provider "virtualbox" do |vb| # Purposeful bad variable vm.foo = "bar" end expect { subject.finalize! }. to raise_error(Vagrant::Errors::VagrantfileLoadError) end it "ignores providers entirely if flag is provided" do subject.provider "virtualbox" do |vb| vb.nope = true end subject.provider "virtualbox" do |vb| vb.not_real = "foo" end subject.finalize! errors = subject.validate(machine, true) expect(errors).to eq({"vm"=>[]}) end end describe "#provision" do it "stores the provisioners" do subject.provision("shell", inline: "foo") subject.provision("shell", inline: "bar", run: "always") { |s| s.path = "baz" } subject.provision("shell", inline: "foo", communicator_required: false) subject.finalize! r = subject.provisioners expect(r.length).to eql(3) expect(r[0].run).to be_nil expect(r[0].config.inline).to eql("foo") expect(r[1].config.inline).to eql("bar") expect(r[1].config.path).to eql("baz") expect(r[1].run).to eql(:always) expect(r[1].communicator_required).to eql(true) expect(r[2].communicator_required).to eql(false) end it "allows provisioner settings to be overridden" do subject.provision("s", path: "foo", type: "shell") { |s| s.inline = "foo" } subject.provision("s", inline: "bar", type: "shell") { |s| s.args = "bar" } subject.finalize! r = subject.provisioners expect(r.length).to eql(1) expect(r[0].config.args).to eql("bar") expect(r[0].config.inline).to eql("bar") expect(r[0].config.path).to eql("foo") end it "marks as invalid if a bad name" do subject.provision("nope", inline: "foo") subject.finalize! r = subject.provisioners expect(r.length).to eql(1) expect(r[0]).to be_invalid end it "allows provisioners that don't define any config" do register_plugin("2") do |p| p.name "foo" # This plugin registers a dummy provisioner # without registering a provisioner config p.provisioner(:foo) do Class.new Vagrant::plugin("2", :provisioner) end end subject.provision("foo") do |c| c.bar = "baz" end # This should succeed without errors expect{ subject.finalize! }.to_not raise_error end it "generates a uuid if no name was provided" do allow(SecureRandom).to receive(:uuid).and_return("MY_CUSTOM_VALUE") subject.provision("shell", path: "foo") { |s| s.inline = "foo" } subject.finalize! r = subject.provisioners expect(r[0].id).to eq("MY_CUSTOM_VALUE") end it "sets id as name if a name was provided" do subject.provision("ghost", type: "shell", path: "motoko") { |s| s.inline = "motoko" } subject.finalize! r = subject.provisioners expect(r[0].id).to eq(:ghost) end describe "merging" do it "ignores non-overriding runs" do subject.provision("shell", inline: "foo", run: "once") other = described_class.new other.provision("shell", inline: "bar", run: "always") merged = subject.merge(other) merged_provs = merged.provisioners expect(merged_provs.length).to eql(2) expect(merged_provs[0].run).to eq("once") expect(merged_provs[1].run).to eq("always") end it "does not merge duplicate provisioners" do subject.provision("shell", inline: "foo") subject.provision("shell", inline: "bar") merged = subject.merge(subject) merged_provs = merged.provisioners expect(merged_provs.length).to eql(2) end it "copies the configs" do subject.provision("shell", inline: "foo") subject_provs = subject.provisioners other = described_class.new other.provision("shell", inline: "bar") merged = subject.merge(other) merged_provs = merged.provisioners expect(merged_provs.length).to eql(2) expect(merged_provs[0].config.inline). to eq(subject_provs[0].config.inline) expect(merged_provs[0].config.object_id). to_not eq(subject_provs[0].config.object_id) end it "uses the proper order when merging overrides" do subject.provision("original", inline: "foo", type: "shell") subject.provision("other", inline: "other", type: "shell") other = described_class.new other.provision("shell", inline: "bar") other.provision("original", inline: "foo-overload", type: "shell") merged = subject.merge(other) merged_provs = merged.provisioners expect(merged_provs.length).to eql(3) expect(merged_provs[0].config.inline). to eq("other") expect(merged_provs[1].config.inline). to eq("bar") expect(merged_provs[2].config.inline). to eq("foo-overload") end it "can preserve order for overrides" do subject.provision("original", inline: "foo", type: "shell") subject.provision("other", inline: "other", type: "shell") other = described_class.new other.provision("shell", inline: "bar") other.provision( "original", inline: "foo-overload", type: "shell", preserve_order: true) merged = subject.merge(other) merged_provs = merged.provisioners expect(merged_provs.length).to eql(3) expect(merged_provs[0].config.inline). to eq("foo-overload") expect(merged_provs[1].config.inline). to eq("other") expect(merged_provs[2].config.inline). to eq("bar") end end end describe "#disk" do before(:each) do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). with("disks").and_return("true") end it "stores the disks" do subject.disk(:disk, size: 100, primary: true) subject.disk(:disk, size: 1000, name: "storage") subject.finalize! assert_valid d = subject.disks expect(d.length).to eql(2) expect(d[0].size).to eql(100) expect(d[1].size).to eql(1000) expect(d[1].name).to eql("storage") end it "raises an error with duplicate names" do subject.disk(:disk, size: 100, name: "foo") subject.disk(:disk, size: 1000, name: "foo", primary: false) subject.finalize! assert_invalid end it "raises an error with duplicate disk files" do allow(File).to receive(:file?).with("bar.vmdk").and_return(true) subject.disk(:disk, size: 100, name: "foo1", file: "bar.vmdk") subject.disk(:disk, size: 100, name: "foo2", file: "bar.vmdk") subject.finalize! assert_invalid end it "does not merge duplicate disks" do subject.disk(:disk, size: 1000, primary: false, name: "storage") subject.disk(:disk, size: 1000, primary: false, name: "backup") merged = subject.merge(subject) merged_disks = merged.disks expect(merged_disks.length).to eql(2) end it "ignores non-overriding runs" do subject.disk(:disk, name: "foo") other = described_class.new other.disk(:disk, name: "bar", primary: false) merged = subject.merge(other) merged_disks = merged.disks expect(merged_disks.length).to eql(2) expect(merged_disks[0].name).to eq("foo") expect(merged_disks[1].name).to eq("bar") end it "adds provider config with `__` config form" do subject.disk(:disk, size: 1000, primary: false, name: "storage", provider__something: "special") expect(subject.disks[0].provider_config).to eq({:provider=>{:something=>"special"}}) end it "adds provider config with Hash config form" do subject.disk(:disk, size: 1000, primary: false, name: "storage", provider: {something: "special"}) expect(subject.disks[0].provider_config).to eq({:provider=>{:something=>"special"}}) end end describe "#synced_folder(s)" do it "defaults to sharing the current directory" do subject.finalize! sf = subject.synced_folders expect(sf.length).to eq(1) expect(sf).to have_key("/vagrant") expect(sf["/vagrant"][:disabled]).to_not be end it "allows overriding settings on the /vagrant sf" do subject.synced_folder(".", "/vagrant", disabled: true) subject.finalize! sf = subject.synced_folders expect(sf.length).to eq(1) expect(sf).to have_key("/vagrant") expect(sf["/vagrant"][:disabled]).to be(true) end it "allows overriding previously set options" do subject.synced_folder(".", "/vagrant", disabled: true) subject.synced_folder(".", "/vagrant", foo: :bar) subject.finalize! sf = subject.synced_folders expect(sf.length).to eq(1) expect(sf).to have_key("/vagrant") expect(sf["/vagrant"][:disabled]).to be(false) expect(sf["/vagrant"][:foo]).to eq(:bar) end # This is a little bit of a special case since nfs can be specified # as `type: "nfs"` or `nfs: true` it "properly overrides nfs" do subject.synced_folder(".", "/vagrant", nfs: true) subject.synced_folder(".", "/vagrant", type: "rsync") subject.finalize! sf = subject.synced_folders expect(sf.length).to eq(1) expect(sf).to have_key("/vagrant") expect(sf["/vagrant"][:type]).to be(:rsync) expect(sf["/vagrant"][:nfs]).to eq(nil) end it "is not an error if guest path is empty but name is not" do subject.synced_folder(".", "", name: "my-vagrant-folder") subject.finalize! assert_valid end it "allows providing custom name via options" do subject.synced_folder(".", "/vagrant", name: "my-vagrant-folder") sf = subject.synced_folders expect(sf).to have_key("my-vagrant-folder") expect(sf["my-vagrant-folder"][:guestpath]).to eq("/vagrant") expect(sf["my-vagrant-folder"][:hostpath]).to eq(".") end it "allows providing custom name without guest path" do subject.synced_folder(".", name: "my-vagrant-folder") sf = subject.synced_folders expect(sf).to have_key("my-vagrant-folder") expect(sf["my-vagrant-folder"][:hostpath]).to eq(".") end it "requires either guest path or name" do subject.synced_folder(".", name: nil, guestpath: nil) subject.finalize! assert_invalid end it "keeps nil guest path if not provided" do subject.synced_folder(".", name: "my-vagrant-folder") sf = subject.synced_folders expect(sf["my-vagrant-folder"][:guestpath]).to be_nil end context "WSL host paths" do let(:valid_path){ "/mnt/c/path" } let(:invalid_path){ "/home/vagrant/path" } let(:synced_folder_impl){ double("synced_folder_impl", new: double("synced_folder_inst", usable?: true, _initialize: true)) } let(:fs_config){ double("fs_config", vm: double("fs_vm", allowed_synced_folder_types: nil)) } let(:plugin){ double("plugin", manager: manager) } let(:manager){ double("manager", synced_folders: {sf_impl: [synced_folder_impl, 1]}) } let(:stub_pathname){ double("stub_pathname", directory?: true, relative?: false) } before do allow(Pathname).to receive(:new).and_return(stub_pathname) allow(stub_pathname).to receive(:expand_path).and_return(stub_pathname) allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(true) allow(Vagrant::Util::Platform).to receive(:wsl_drvfs_path?).with(valid_path).and_return(true) allow(Vagrant::Util::Platform).to receive(:wsl_drvfs_path?).with(invalid_path).and_return(false) allow(machine).to receive(:config).and_return(fs_config) allow(Vagrant).to receive(:plugin).with("2").and_return(plugin) subject.synced_folder(".", "/vagrant", disabled: true) end it "is valid when located on DrvFs" do subject.synced_folder(valid_path, "/guest/path") subject.finalize! assert_valid end it "is invalid when not located on DrvFs" do subject.synced_folder(invalid_path, "/guest/path") subject.finalize! assert_invalid end context "when synced folder defines support for non-DrvFs" do let(:support_nondrvfs){ true } before do allow(synced_folder_impl).to receive(:respond_to?).with(:wsl_allow_non_drvfs?).and_return(true) allow(synced_folder_impl).to receive(:wsl_allow_non_drvfs?).and_return(support_nondrvfs) end context "and is supported" do it "is valid when located on DrvFs" do subject.synced_folder(valid_path, "/guest/path") subject.finalize! assert_valid end it "is valid when not located on DrvFs" do subject.synced_folder(invalid_path, "/guest/path") subject.finalize! assert_valid end end context "and is not supported" do let(:support_nondrvfs){ false } it "is valid when located on DrvFs" do subject.synced_folder(valid_path, "/guest/path") subject.finalize! assert_valid end it "is invalid when not located on DrvFs" do subject.synced_folder(invalid_path, "/guest/path") subject.finalize! assert_invalid end end end end end describe "#usable_port_range" do it "defaults properly" do subject.finalize! expect(subject.usable_port_range).to eq( Range.new(2200, 2250)) end end end ================================================ FILE: test/unit/plugins/kernel_v2/config/vm_trigger_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/vm_trigger") describe VagrantPlugins::Kernel_V2::VagrantConfigTrigger do include_context "unit" let(:command) { :up } subject { described_class.new(command) } let(:machine) { double("machine") } def assert_invalid errors = subject.validate(machine) if errors.empty? raise "No errors: #{errors.inspect}" end end def assert_valid errors = subject.validate(machine) if !errors.empty? raise "Errors: #{errors.inspect}" end end before do env = double("env") allow(env).to receive(:root_path).and_return(nil) allow(machine).to receive(:env).and_return(env) allow(machine).to receive(:provider_config).and_return(nil) allow(machine).to receive(:provider_options).and_return({}) subject.name = "foo" subject.info = "Hello there" subject.warn = "Warning!!" subject.ignore = :up subject.only_on = "guest" subject.ruby do |env,machine| var = 'test' math = 1+1 end subject.run = {inline: "apt-get update"} subject.run_remote = {inline: "apt-get update", env: {"VAR"=>"VAL"}} end describe "with defaults" do it "is valid with test defaults" do subject.finalize! assert_valid end it "sets a command" do subject.finalize! expect(subject.command).to eq(command) end it "uses default error behavior" do subject.finalize! expect(subject.on_error).to eq(:halt) end end describe "defining a new config that needs to match internal restraints" do let(:cmd) { :destroy } let(:cfg) { described_class.new(cmd) } let(:arr_cfg) { described_class.new(cmd) } before do cfg.only_on = :guest cfg.ignore = "up" cfg.abort = true cfg.type = "action" cfg.ruby do var = 1+1 end arr_cfg.only_on = ["guest", /other/] arr_cfg.ignore = ["up", "destroy"] end it "ensures only_on is an array" do cfg.finalize! arr_cfg.finalize! expect(cfg.only_on).to be_a(Array) expect(arr_cfg.only_on).to be_a(Array) end it "ensures ignore is an array of symbols" do cfg.finalize! arr_cfg.finalize! expect(cfg.ignore).to be_a(Array) expect(arr_cfg.ignore).to be_a(Array) cfg.ignore.each do |a| expect(a).to be_a(Symbol) end arr_cfg.ignore.each do |a| expect(a).to be_a(Symbol) end end it "ensures ruby is a proc" do cfg.finalize! expect(cfg.ruby_block).to be_a(Proc) end it "converts aborts true to exit code 0" do cfg.finalize! expect(cfg.abort).to eq(1) end it "converts types to symbols" do cfg.finalize! expect(cfg.type).to eq(:action) end end describe "defining a basic trigger config" do let(:cmd) { :up } let(:cfg) { described_class.new(cmd) } before do cfg.info = "Hello there" cfg.warn = "Warning!!" cfg.on_error = :continue cfg.ignore = :up cfg.abort = 3 cfg.only_on = "guest" cfg.ruby = proc{ var = 1+1 } cfg.run = {inline: "apt-get update"} cfg.run_remote = {inline: "apt-get update", env: {"VAR"=>"VAL"}} end it "sets the options" do cfg.finalize! expect(cfg.info).to eq("Hello there") expect(cfg.warn).to eq("Warning!!") expect(cfg.on_error).to eq(:continue) expect(cfg.ignore).to eq([:up]) expect(cfg.only_on).to eq(["guest"]) expect(cfg.run).to be_a(VagrantPlugins::Shell::Config) expect(cfg.run_remote).to be_a(VagrantPlugins::Shell::Config) expect(cfg.abort).to eq(3) expect(cfg.ruby_block).to be_a(Proc) end end end ================================================ FILE: test/unit/plugins/providers/docker/action/compare_synced_folders_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/providers/docker/action/compare_synced_folders" describe VagrantPlugins::DockerProvider::Action::CompareSyncedFolders do include_context "unit" include_context "virtualbox" let(:sandbox) { isolated_environment } let(:iso_env) do # We have to create a Vagrantfile so there is a root path sandbox.vagrantfile("") sandbox.create_vagrant_env end let(:machine) do iso_env.machine(iso_env.machine_names[0], :virtualbox).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) end end let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new(".") }} let(:app) { lambda { |*args| }} let(:driver) { double("driver") } subject { described_class.new(app, env) } after do sandbox.close end describe "#call" do let(:cached) { {:docker=>{"/vagrant"=>{:guestpath=>"/vagrant", :hostpath=>"/home/hashicorp/code/vagrant-sandbox", :disabled=>false, :__vagrantfile=>true}}} } let(:fresh) { {:docker=>{"/vagrant"=>{:guestpath=>"/vagrant", :hostpath=>".", :disabled=>false, :__vagrantfile=>true}}} } let(:existing) { {"/vagrant"=>"/home/hashicorp/code/vagrant-sandbox"} } it "calls the next action in the chain" do allow(machine.provider).to receive(:host_vm?).and_return(false) called = false app = ->(*args) { called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end context "invalid or existing entries" do let(:cached) { {:docker=>{"/vagrant"=>{:guestpath=>"/not-real", :hostpath=>"/home/hashicorp/code/vagrant-sandbox", :disabled=>false, :__vagrantfile=>true}}} } let(:fresh) { {:docker=>{"/vagrant"=>{:guestpath=>"/vagrant", :hostpath=>".", :disabled=>false, :__vagrantfile=>true}}} } it "shows a warning" do allow(machine.provider).to receive(:host_vm?).and_return(false) called = false app = ->(*args) { called = true } action = described_class.new(app, env) expect(action).to receive(:synced_folders). with(machine, cached: true).and_return(cached) expect(action).to receive(:synced_folders). with(machine).and_return(fresh) expect(machine.ui).to receive(:warn) action.call(env) expect(called).to eq(true) end end it "shows no warning comparing synced folders" do allow(machine.provider).to receive(:host_vm?).and_return(false) called = false app = ->(*args) { called = true } action = described_class.new(app, env) expect(action).to receive(:synced_folders). with(machine, cached: true).and_return(cached) expect(action).to receive(:synced_folders). with(machine).and_return(fresh) action.call(env) expect(machine.ui).not_to receive(:warn) expect(called).to eq(true) end end end ================================================ FILE: test/unit/plugins/providers/docker/action/connect_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/providers/docker/action/connect_networks" describe VagrantPlugins::DockerProvider::Action::ConnectNetworks do include_context "unit" let(:sandbox) { isolated_environment } let(:iso_env) do # We have to create a Vagrantfile so there is a root path sandbox.vagrantfile("") sandbox.create_vagrant_env end let(:vm_config) { double("machine_vm_config") } let(:machine_config) do double("machine_config").tap do |top_config| allow(top_config).to receive(:vm).and_return(vm_config) end end let(:machine) do iso_env.machine(iso_env.machine_names[0], :docker).tap do |m| allow(m).to receive(:vagrantfile).and_return(vagrantfile) allow(m).to receive(:config).and_return(machine_config) allow(m).to receive(:id).and_return("12345") allow(m.provider).to receive(:driver).and_return(driver) allow(m.provider).to receive(:host_vm?).and_return(false) allow(m.config.vm).to receive(:networks).and_return(networks) end end let(:docker_connects) { {0=>"vagrant_network_172.20.0.0/16", 1=>"vagrant_network_public_wlp4s0", 2=>"vagrant_network_2a02:6b8:b010:9020:1::/80"} } let(:vagrantfile) { double("vagrantfile") } let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new("."), docker_connects: docker_connects, vagrantfile: vagrantfile }} let(:app) { lambda { |*args| }} let(:driver) { double("driver", create: "abcd1234") } let(:networks) { [[:private_network, {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"}], [:public_network, {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"}], [:private_network, {:type=>"dhcp", :ipv6=>"true", :subnet=>"2a02:6b8:b010:9020:1::/80", :protocol=>"tcp", :id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}], [:forwarded_port, {:guest=>22, :host=>2200, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}]] } subject { described_class.new(app, env) } let(:subprocess_result) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(0) allow(result).to receive(:stdout).and_return("") allow(result).to receive(:stderr).and_return("") end end before do allow(Vagrant::Util::Subprocess).to receive(:execute).with("docker", "version", an_instance_of(Hash)).and_return(subprocess_result) end after do sandbox.close end describe "#call" do it "calls the next action in the chain" do allow(driver).to receive(:host_vm?).and_return(false) allow(driver).to receive(:connect_network).and_return(true) called = false app = ->(*args) { called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end it "connects all of the available networks to a container" do expect(driver).to receive(:connect_network).with("vagrant_network_172.20.0.0/16", "12345", ["--ip", "172.20.128.2", "--alias", "mynetwork"]) expect(driver).to receive(:connect_network).with("vagrant_network_public_wlp4s0", "12345", ["--ip", "172.30.130.2"]) expect(driver).to receive(:connect_network).with("vagrant_network_2a02:6b8:b010:9020:1::/80", "12345", []) subject.call(env) end context "with missing env values" do it "raises an error if the network name is missing" do env[:docker_connects] = {} expect{subject.call(env)}.to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkNameMissing) end end end describe "#generate_connect_cli_arguments" do let(:network_options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} } let(:false_network_options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"false", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} } it "removes false values" do cli_args = subject.generate_connect_cli_arguments(false_network_options) expect(cli_args).to eq(["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) end it "removes true and leaves flag value in arguments" do cli_args = subject.generate_connect_cli_arguments(network_options) expect(cli_args).to eq(["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--internal", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) end it "takes options and generates cli flags" do cli_args = subject.generate_connect_cli_arguments(network_options) expect(cli_args).to eq(["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--internal", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) end end end ================================================ FILE: test/unit/plugins/providers/docker/action/create_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/providers/docker/action/create" describe VagrantPlugins::DockerProvider::Action::Create do include_context "unit" include_context "virtualbox" let(:sandbox) { isolated_environment } let(:iso_env) do # We have to create a Vagrantfile so there is a root path sandbox.vagrantfile("") sandbox.create_vagrant_env end let(:machine) do iso_env.machine(iso_env.machine_names[0], :virtualbox).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) end end let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new(".") }} let(:app) { lambda { |*args| }} let(:driver) { double("driver", create: "abcd1234") } subject { described_class.new(app, env) } after do sandbox.close end describe "#call" do it "calls the next action in the chain" do called = false app = ->(*args) { called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end end describe "#forwarded_ports" do it "does not clobber ports with different protocols" do subject.instance_variable_set(:@machine, machine) machine.config.vm.network "forwarded_port", guest: 8125, host: 8125, protocol: "tcp" machine.config.vm.network "forwarded_port", guest: 8125, host: 8125, protocol: "udp" result = subject.forwarded_ports(false) expect(result).to eq(["8125:8125", "8125:8125/udp"]) end end describe "#generate_container_name" do let(:env) {{root_path: root_path}} let(:root_path) {Pathname.new("/path/to/root")} before do subject.instance_variable_set(:@env, env) subject.instance_variable_set(:@machine, machine) end it "should not remove any characters" do expect(subject.generate_container_name).to start_with("root") end context "when root path starts with underscores" do let(:root_path) {Pathname.new("/path/to/__root")} it "should remove underscores" do expect(subject.generate_container_name).to start_with("root") end end context "when root path starts with dashes" do let(:root_path) {Pathname.new("/path/to/--root")} it "should remove dashes" do expect(subject.generate_container_name).to start_with("root") end end context "when root path starts with combination of invalid starting characters" do let(:root_path) {Pathname.new("/path/to/_.-_-root")} it "should remove invalid starting characters" do expect(subject.generate_container_name).to start_with("root") end end context "when root path ends with underscores" do let(:root_path) {Pathname.new("/path/to/root__")} it "should not remove trailing underscroes" do expect(subject.generate_container_name).to start_with("root_") end end context "when root path ends with invalid characters" do let(:root_path) {Pathname.new("/path/to/root_.")} it "should remove invalid trailing characters" do expect(subject.generate_container_name).to start_with("root_") end end context "when root path contains invalid characters" do let(:root_path) {Pathname.new("/path/to/root-.path_.")} it "should only remove invalid characters" do expect(subject.generate_container_name).to start_with("root-path_") end end end end ================================================ FILE: test/unit/plugins/providers/docker/action/destroy_network_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/providers/docker/action/destroy_network" describe VagrantPlugins::DockerProvider::Action::DestroyNetwork do include_context "unit" let(:sandbox) { isolated_environment } let(:iso_env) do # We have to create a Vagrantfile so there is a root path sandbox.vagrantfile("") sandbox.create_vagrant_env end let(:vm_config) { double("machine_vm_config") } let(:machine_config) do double("machine_config").tap do |top_config| allow(top_config).to receive(:vm).and_return(vm_config) end end let(:machine) do iso_env.machine(iso_env.machine_names[0], :docker).tap do |m| allow(m).to receive(:vagrantfile).and_return(vagrantfile) allow(m).to receive(:config).and_return(machine_config) allow(m.provider).to receive(:driver).and_return(driver) allow(m.config.vm).to receive(:networks).and_return(networks) end end let(:vagrantfile) { double("vagrantfile") } let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new("."), vagrantfile: vagrantfile }} let(:app) { lambda { |*args| }} let(:driver) { double("driver", create: "abcd1234") } let(:networks) { [[:private_network, {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"}], [:private_network, {:type=>"dhcp", :ipv6=>"true", :subnet=>"2a02:6b8:b010:9020:1::/80", :protocol=>"tcp", :id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}], [:forwarded_port, {:guest=>22, :host=>2200, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}]] } subject { described_class.new(app, env) } let(:subprocess_result) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(0) allow(result).to receive(:stdout).and_return("") allow(result).to receive(:stderr).and_return("") end end before do allow(Vagrant::Util::Subprocess).to receive(:execute).with("docker", "version", an_instance_of(Hash)).and_return(subprocess_result) end after do sandbox.close end describe "#call" do let(:network_names) { ["vagrant_network_172.20.0.0/16", "vagrant_network_2a02:6b8:b010:9020:1::/80"] } it "calls the next action in the chain" do allow(driver).to receive(:host_vm?).and_return(false) allow(driver).to receive(:existing_network?).and_return(true) allow(driver).to receive(:network_used?).and_return(true) allow(driver).to receive(:list_network_names).and_return([]) called = false app = ->(*args) { called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end it "calls the proper driver method to destroy the network" do allow(driver).to receive(:list_network_names).and_return(network_names) allow(driver).to receive(:host_vm?).and_return(false) allow(driver).to receive(:existing_named_network?).with("vagrant_network_172.20.0.0/16"). and_return(true) allow(driver).to receive(:network_used?).with("vagrant_network_172.20.0.0/16"). and_return(false) allow(driver).to receive(:existing_named_network?).with("vagrant_network_2a02:6b8:b010:9020:1::/80"). and_return(true) allow(driver).to receive(:network_used?).with("vagrant_network_2a02:6b8:b010:9020:1::/80"). and_return(false) expect(driver).to receive(:rm_network).with("vagrant_network_172.20.0.0/16").twice expect(driver).to receive(:rm_network).with("vagrant_network_2a02:6b8:b010:9020:1::/80").twice subject.call(env) end it "doesn't destroy the network if another container is still using it" do allow(driver).to receive(:host_vm?).and_return(false) allow(driver).to receive(:list_network_names).and_return(network_names) allow(driver).to receive(:existing_named_network?).with("vagrant_network_172.20.0.0/16"). and_return(true) allow(driver).to receive(:network_used?).with("vagrant_network_172.20.0.0/16"). and_return(true) allow(driver).to receive(:existing_named_network?).with("vagrant_network_2a02:6b8:b010:9020:1::/80"). and_return(true) allow(driver).to receive(:network_used?).with("vagrant_network_2a02:6b8:b010:9020:1::/80"). and_return(true) expect(driver).not_to receive(:rm_network).with("vagrant_network_172.20.0.0/16") expect(driver).not_to receive(:rm_network).with("vagrant_network_2a02:6b8:b010:9020:1::/80") subject.call(env) end end end ================================================ FILE: test/unit/plugins/providers/docker/action/host_machine_sync_folders_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/providers/docker/action/host_machine_sync_folders" describe VagrantPlugins::DockerProvider::Action::HostMachineSyncFolders do include_context "unit" include_context "virtualbox" let(:sandbox) { isolated_environment } let(:iso_env) do # We have to create a Vagrantfile so there is a root path sandbox.vagrantfile("") sandbox.create_vagrant_env end let(:machine) do iso_env.machine(iso_env.machine_names[0], :virtualbox).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) end end let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new(".") }} let(:app) { lambda { |*args| }} let(:driver) { double("driver") } subject { described_class.new(app, env) } after do sandbox.close end describe "#call" do it "calls the next action in the chain" do allow(machine.provider).to receive(:host_vm?).and_return(false) called = false app = ->(*args) { called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end context "with a host vm" do it "calls the next action in the chain" do allow(machine.provider).to receive(:host_vm?).and_return(true) allow(machine.provider).to receive(:host_vm).and_return(machine) called = false app = ->(*args) { called = true } expect(machine.provider).to receive(:host_vm_lock).and_return(true) action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end end end describe "#setup_synced_folders" do it "syncs folders on the guest machine with a given id" do allow(Digest::MD5).to receive(:hexdigest).and_return("4e9414d72abee585b3d6263e50248e37") expect(machine).to receive(:action).with(:sync_folders, {:synced_folders_config => anything}) expect(env[:machine].config.vm).to receive(:synced_folder). with("/var/lib/docker/docker_4e9414d72abee585b3d6263e50248e37", "/vagrant", anything) subject.send(:setup_synced_folders, machine, env) end end end ================================================ FILE: test/unit/plugins/providers/docker/action/login_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/providers/docker/action/login" describe VagrantPlugins::DockerProvider::Action::Login do include_context "unit" let(:sandbox) { isolated_environment } let(:iso_env) do # We have to create a Vagrantfile so there is a root path sandbox.vagrantfile("") sandbox.create_vagrant_env end let(:provider_config) { double("provider_config", username: "docker", password: "") } let(:vm_config) { double("machine_vm_config") } let(:machine_config) do double("machine_config").tap do |top_config| allow(top_config).to receive(:vm).and_return(vm_config) end end let(:machine) do iso_env.machine(iso_env.machine_names[0], :docker).tap do |m| allow(m).to receive(:id).and_return("12345") allow(m).to receive(:config).and_return(machine_config) allow(m).to receive(:provider_config).and_return(provider_config) allow(m).to receive(:vagrantfile).and_return(vagrantfile) allow(m.provider).to receive(:driver).and_return(driver) allow(m.provider).to receive(:host_vm?).and_return(false) end end let(:vagrantfile) { double("vagrantfile") } let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new("."), vagrantfile: vagrantfile }} let(:app) { lambda { |*args| }} let(:driver) { double("driver", create: "abcd1234") } subject { described_class.new(app, env) } let(:subprocess_result) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(0) allow(result).to receive(:stdout).and_return("") allow(result).to receive(:stderr).and_return("") end end before do allow(Vagrant::Util::Subprocess).to receive(:execute).with("docker", "version", an_instance_of(Hash)).and_return(subprocess_result) end after do sandbox.close end describe "#call" do it "calls the next action in the chain" do allow(driver).to receive(:host_vm?).and_return(false) called = false app = ->(*args) { called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end it "uses a host vm lock if host_vm is true and password is set" do allow(driver).to receive(:host_vm?).and_return(true) allow(driver).to receive(:login).and_return(true) allow(driver).to receive(:logout).and_return(true) allow(machine.provider).to receive(:host_vm?).and_return(true) allow(machine.provider).to receive(:host_vm_lock) { |&block| block.call } allow(provider_config).to receive(:password).and_return("docker") allow(provider_config).to receive(:email).and_return("docker") allow(provider_config).to receive(:auth_server).and_return("docker") called = false app = ->(*args) { called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end it "doesn't use the host vm if not set" do allow(driver).to receive(:host_vm?).and_return(false) allow(driver).to receive(:login).and_return(true) allow(driver).to receive(:logout).and_return(true) allow(machine.provider).to receive(:host_vm?).and_return(false) allow(provider_config).to receive(:password).and_return("docker") allow(provider_config).to receive(:email).and_return("docker") allow(provider_config).to receive(:auth_server).and_return("docker") called = false app = ->(*args) { called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end end end ================================================ FILE: test/unit/plugins/providers/docker/action/prepare_networks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/providers/docker/action/prepare_networks" describe VagrantPlugins::DockerProvider::Action::PrepareNetworks do include_context "unit" let(:sandbox) { isolated_environment } let(:iso_env) do # We have to create a Vagrantfile so there is a root path sandbox.vagrantfile("") sandbox.create_vagrant_env end let(:vm_config) { double("machine_vm_config") } let(:machine_config) do double("machine_config").tap do |top_config| allow(top_config).to receive(:vm).and_return(vm_config) end end let(:machine) do iso_env.machine(iso_env.machine_names[0], :docker).tap do |m| allow(m).to receive(:vagrantfile).and_return(vagrantfile) allow(m).to receive(:config).and_return(machine_config) allow(m.provider).to receive(:driver).and_return(driver) allow(m.config.vm).to receive(:networks).and_return(networks) end end let(:vagrantfile) { double("vagrantfile") } let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new("."), vagrantfile: vagrantfile }} let(:app) { lambda { |*args| }} let(:driver) { double("driver", create: "abcd1234") } let(:networks) { [[:private_network, {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"}], [:public_network, {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"}], [:private_network, {:type=>"dhcp", :ipv6=>"true", :subnet=>"2a02:6b8:b010:9020:1::/80", :protocol=>"tcp", :id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}], [:forwarded_port, {:guest=>22, :host=>2200, :host_ip=>"127.0.0.1", :id=>"ssh", :auto_correct=>true, :protocol=>"tcp"}]] } let(:invalid_network) { [[:private_network, {:ipv6=>"true", :protocol=>"tcp", :id=>"b8f23054-38d5-45c3-99ea-d33fc5d1b9f2"}]] } subject { described_class.new(app, env) } let(:subprocess_result) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(0) allow(result).to receive(:stdout).and_return("") allow(result).to receive(:stderr).and_return("") end end before do allow(Vagrant::Util::Subprocess).to receive(:execute).with("docker", "version", an_instance_of(Hash)).and_return(subprocess_result) end after do sandbox.close end describe "#call" do it "calls the next action in the chain" do allow(driver).to receive(:host_vm?).and_return(false) allow(driver).to receive(:existing_named_network?).and_return(false) allow(driver).to receive(:create_network).and_return(true) called = false app = ->(*args) { called = true } action = described_class.new(app, env) allow(action).to receive(:process_public_network).and_return(["name", {}]) allow(action).to receive(:process_private_network).and_return(["name", {}]) action.call(env) expect(called).to eq(true) end it "calls the proper driver methods to setup a network" do allow(driver).to receive(:host_vm?).and_return(false) allow(driver).to receive(:existing_named_network?).and_return(false) allow(driver).to receive(:network_containing_address). with("172.20.128.2").and_return(nil) allow(driver).to receive(:network_containing_address). with("192.168.1.1").and_return(nil) allow(driver).to receive(:network_defined?).with("172.20.128.0/24"). and_return(false) allow(driver).to receive(:network_defined?).with("172.30.128.0/24"). and_return(false) allow(driver).to receive(:network_defined?).with("2a02:6b8:b010:9020:1::/80"). and_return(false) allow(subject).to receive(:request_public_gateway).and_return("1234") allow(subject).to receive(:request_public_iprange).and_return("1234") expect(subject).to receive(:process_private_network).with(networks[0][1], {}, env). and_return(["vagrant_network_172.20.128.0/24", {:ipv6=>false, :subnet=>"172.20.128.0/24"}]) expect(subject).to receive(:process_public_network).with(networks[1][1], {}, env). and_return(["vagrant_network_public_wlp4s0", {"opt"=>"parent=wlp4s0", "subnet"=>"192.168.1.0/24", "driver"=>"macvlan", "gateway"=>"1234", "ipv6"=>false, "ip_range"=>"1234"}]) expect(subject).to receive(:process_private_network).with(networks[2][1], {}, env). and_return(["vagrant_network_2a02:6b8:b010:9020:1::/80", {:ipv6=>true, :subnet=>"2a02:6b8:b010:9020:1::/80"}]) allow(machine.ui).to receive(:ask).and_return("1") expect(driver).to receive(:create_network). with("vagrant_network_172.20.128.0/24", ["--subnet", "172.20.128.0/24"]) expect(driver).to receive(:create_network). with("vagrant_network_public_wlp4s0", ["--opt", "parent=wlp4s0", "--subnet", "192.168.1.0/24", "--driver", "macvlan", "--gateway", "1234", "--ip-range", "1234"]) expect(driver).to receive(:create_network). with("vagrant_network_2a02:6b8:b010:9020:1::/80", ["--ipv6", "--subnet", "2a02:6b8:b010:9020:1::/80"]) subject.call(env) expect(env[:docker_connects]).to eq({0=>"vagrant_network_172.20.128.0/24", 1=>"vagrant_network_public_wlp4s0", 2=>"vagrant_network_2a02:6b8:b010:9020:1::/80"}) end it "uses an existing network if a matching subnet is found" do allow(driver).to receive(:host_vm?).and_return(false) allow(driver).to receive(:network_containing_address). with("172.20.128.2").and_return(nil) allow(driver).to receive(:network_containing_address). with("192.168.1.1").and_return(nil) allow(driver).to receive(:network_defined?).with("172.20.128.0/24"). and_return("vagrant_network_172.20.128.0/24") allow(driver).to receive(:network_defined?).with("172.30.128.0/24"). and_return("vagrant_network_public_wlp4s0") allow(driver).to receive(:network_defined?).with("2a02:6b8:b010:9020:1::/80"). and_return("vagrant_network_2a02:6b8:b010:9020:1::/80") allow(machine.ui).to receive(:ask).and_return("1") expect(driver).to receive(:existing_named_network?). with("vagrant_network_172.20.128.0/24").and_return(true) expect(driver).to receive(:existing_named_network?). with("vagrant_network_public_wlp4s0").and_return(true) expect(driver).to receive(:existing_named_network?). with("vagrant_network_2a02:6b8:b010:9020:1::/80").and_return(true) expect(subject).to receive(:process_private_network).with(networks[0][1], {}, env). and_return(["vagrant_network_172.20.128.0/24", {:ipv6=>false, :subnet=>"172.20.128.0/24"}]) expect(subject).to receive(:process_public_network).with(networks[1][1], {}, env). and_return(["vagrant_network_public_wlp4s0", {"opt"=>"parent=wlp4s0", "subnet"=>"192.168.1.0/24", "driver"=>"macvlan", "gateway"=>"1234", "ipv6"=>false, "ip_range"=>"1234"}]) expect(subject).to receive(:process_private_network).with(networks[2][1], {}, env). and_return(["vagrant_network_2a02:6b8:b010:9020:1::/80", {:ipv6=>true, :subnet=>"2a02:6b8:b010:9020:1::/80"}]) expect(driver).not_to receive(:create_network) expect(subject).to receive(:validate_network_configuration!). with("vagrant_network_172.20.128.0/24", networks[0][1], {:ipv6=>false, :subnet=>"172.20.128.0/24"}, driver) expect(subject).to receive(:validate_network_configuration!). with("vagrant_network_public_wlp4s0", networks[1][1], {"opt"=>"parent=wlp4s0", "subnet"=>"192.168.1.0/24", "driver"=>"macvlan", "gateway"=>"1234", "ipv6"=>false, "ip_range"=>"1234"}, driver) expect(subject).to receive(:validate_network_configuration!). with("vagrant_network_2a02:6b8:b010:9020:1::/80", networks[2][1], {:ipv6=>true, :subnet=>"2a02:6b8:b010:9020:1::/80"}, driver) subject.call(env) end it "raises an error if an inproper network configuration is given" do allow(machine.config.vm).to receive(:networks).and_return(invalid_network) allow(driver).to receive(:host_vm?).and_return(false) allow(driver).to receive(:existing_network?).and_return(false) expect{ subject.call(env) }.to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkIPAddressRequired) end end describe "#list_interfaces" do let(:interfaces){ ["192.168.1.2", "192.168.10.10"] } it "returns an array of interfaces to use" do allow(Socket).to receive(:getifaddrs). and_return(interfaces.map{|i| double(:socket, addr: Addrinfo.ip(i))}) interfaces = subject.list_interfaces expect(subject.list_interfaces.size).to eq(2) end it "does not include an interface with the address is nil" do allow(Socket).to receive(:getifaddrs). and_return(interfaces.map{|i| double(:socket, addr: nil)}) expect(subject.list_interfaces.size).to eq(0) end end describe "#generate_create_cli_arguments" do let(:network_options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} } let(:false_network_options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"false", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58"} } it "returns an array of cli arguments" do cli_args = subject.generate_create_cli_arguments(network_options) expect(cli_args).to eq( ["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--internal", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) end it "removes option if set to false" do cli_args = subject.generate_create_cli_arguments(false_network_options) expect(cli_args).to eq( ["--ip", "172.20.128.2", "--subnet", "172.20.0.0/16", "--driver", "bridge", "--alias", "mynetwork", "--protocol", "tcp", "--id", "80e017d5-388f-4a2f-a3de-f8dce8156a58"]) end end describe "#validate_network_name!" do let(:netname) { "vagrant_network" } it "returns true if name exists" do allow(driver).to receive(:existing_named_network?).with(netname). and_return(true) expect(subject.validate_network_name!(netname, env)).to be_truthy end it "raises an error if name does not exist" do allow(driver).to receive(:existing_named_network?).with(netname). and_return(false) expect{subject.validate_network_name!(netname, env)}.to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkNameUndefined) end end describe "#validate_network_configuration!" do let(:netname) { "vagrant_network_172.20.128.0/24" } let(:options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58", :netmask=>24} } let(:network_options) { {:ipv6=>false, :subnet=>"172.20.128.0/24"} } it "returns true if all options are valid" do allow(driver).to receive(:network_containing_address).with(options[:ip]). and_return(netname) allow(driver).to receive(:network_containing_address).with(network_options[:subnet]). and_return(netname) expect(subject.validate_network_configuration!(netname, options, network_options, driver)). to be_truthy end it "raises an error of the address is invalid" do allow(driver).to receive(:network_containing_address).with(options[:ip]). and_return("fakename") expect{subject.validate_network_configuration!(netname, options, network_options, driver)}. to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkAddressInvalid) end it "raises an error of the subnet is invalid" do allow(driver).to receive(:network_containing_address).with(options[:ip]). and_return(netname) allow(driver).to receive(:network_containing_address).with(network_options[:subnet]). and_return("fakename") expect{subject.validate_network_configuration!(netname, options, network_options, driver)}. to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkSubnetInvalid) end end describe "#process_private_network" do let(:options) { {:ip=>"172.20.128.2", :subnet=>"172.20.0.0/16", :driver=>"bridge", :internal=>"true", :alias=>"mynetwork", :protocol=>"tcp", :id=>"80e017d5-388f-4a2f-a3de-f8dce8156a58", :netmask=>24} } let(:dhcp_options) { {type: "dhcp"} } let(:bad_options) { {driver: "bridge"} } it "generates a network name and config for a dhcp private network" do network_name, network_options = subject.process_private_network(dhcp_options, {}, env) expect(network_name).to eq("vagrant_network") expect(network_options).to eq({}) end it "generates a network name and options for a static ip" do allow(driver).to receive(:network_defined?).and_return(nil) network_name, network_options = subject.process_private_network(options, {}, env) expect(network_name).to eq("vagrant_network_172.20.0.0/16") expect(network_options).to eq({:ipv6=>false, :subnet=>"172.20.0.0/16"}) end it "raises an error if no ip address or type `dhcp` was given" do expect{subject.process_private_network(bad_options, {}, env)}. to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkIPAddressRequired) end end describe "#process_public_network" do let(:options) { {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"} } let(:addr) { double("addr", ip: true, ip_address: "192.168.1.139") } let(:netmask) { double("netmask", ip_unpack: ["255.255.255.0"]) } let(:ipaddr) { double("ipaddr", prefix: 22, succ: "10.1.10.2", ipv4?: true, ipv6?: false, to_i: 4294967040, name: "ens20u1u2", addr: addr, netmask: netmask) } it "raises an error if there are no network interfaces" do expect(subject).to receive(:list_interfaces).and_return([]) expect{subject.process_public_network(options, {}, env)}. to raise_error(VagrantPlugins::DockerProvider::Errors::NetworkNoInterfaces) end it "generates a network name and configuration" do allow(machine.ui).to receive(:ask).and_return("1") allow(subject).to receive(:request_public_gateway).and_return("1234") allow(subject).to receive(:request_public_iprange).and_return("1234") allow(IPAddr).to receive(:new).and_return(ipaddr) allow(driver).to receive(:existing_named_network?).and_return(false) allow(driver).to receive(:network_containing_address). with("10.1.10.2").and_return("vagrant_network_public") # mock the call to PrepareNetworks.list_interfaces so that we don't depend # on the current network interfaces allow(subject).to receive(:list_interfaces). and_return([ipaddr]) network_name, _network_options = subject.process_public_network(options, {}, env) expect(network_name).to eq("vagrant_network_public") end end describe "#request_public_gateway" do let(:options) { {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"} } let(:ipaddr) { double("ipaddr", to_s: "172.30.130.2", prefix: 22, succ: "172.30.130.3", ipv4?: true, ipv6?: false) } it "requests a gateway" do allow(IPAddr).to receive(:new).and_return(ipaddr) allow(ipaddr).to receive(:include?).and_return(false) allow(machine.ui).to receive(:ask).and_return("1") addr = subject.request_public_gateway(options, "bridge", env) expect(addr).to eq("172.30.130.2") end end describe "#request_public_iprange" do let(:options) { {:ip=>"172.30.130.2", :subnet=>"172.30.0.0/16", :driver=>"bridge", :id=>"30e017d5-488f-5a2f-a3ke-k8dce8246b60"} } let(:ipaddr) { double("ipaddr", to_s: "172.30.100.2", prefix: 22, succ: "172.30.100.3", ipv4?: true, ipv6?: false) } let(:subnet) { double("ipaddr", to_s: "172.30.130.2", prefix: 22, succ: "172.30.130.3", ipv6?: false) } let(:ipaddr_prefix) { double("ipaddr_prefix", to_s: "255.255.255.255/255.255.255.0", to_i: 4294967040 ) } let(:netmask) { double("netmask", ip_unpack: ["255.255.255.0", 0]) } let(:interface) { double("interface", name: "bridge", netmask: netmask) } it "requests a public ip range" do allow(IPAddr).to receive(:new).with(options[:subnet]).and_return(subnet) allow(IPAddr).to receive(:new).with("172.30.130.2").and_return(ipaddr) allow(IPAddr).to receive(:new).with("255.255.255.255/255.255.255.0").and_return(ipaddr_prefix) allow(subnet).to receive(:include?).and_return(true) allow(machine.ui).to receive(:ask).and_return(options[:ip]) addr = subject.request_public_iprange(options, interface, env) end end end ================================================ FILE: test/unit/plugins/providers/docker/command/exec_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/providers/docker/command/exec" describe VagrantPlugins::DockerProvider::Command::Exec do include_context "unit" include_context "command plugin helpers" let(:env) { { action_runner: action_runner, machine: machine, ui: Vagrant::UI::Silent.new, } } subject { described_class.new(app, env) } let(:argv) { [] } let(:iso_env) do # We have to create a Vagrantfile so there is a root path isolated_environment.tap do |env| env.vagrantfile("") end end let(:iso_vagrant_env) { iso_env.create_vagrant_env } let(:action_runner) { double("action_runner") } let(:box) do box_dir = iso_env.box3("foo", "1.0", :virtualbox) Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir) end let(:machine) { iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy) } subject { described_class.new(argv, env) } before(:all) do I18n.load_path << Vagrant.source_root.join("templates/locales/providers_docker.yml") I18n.reload! end before do allow(Vagrant.plugin("2").manager).to receive(:commands).and_return({}) end describe "#exec_command" do describe "with -t option" do let(:command) { ["/bin/bash"] } let(:options) { {pty: "true"} } it "calls Safe Exec" do allow(Kernel).to receive(:exec).and_return(true) expect(Vagrant::Util::SafeExec).to receive(:exec).with("docker", "exec", "-t", anything, "/bin/bash") subject.exec_command(machine, command, options) end end describe "with options" do let(:command) { ["ls"] } let(:options) { {etc: "something"} } it "passes the options successfully" do driver = instance_double("Driver") expect(driver).to receive(:execute).with("docker", "exec", nil, "ls", {etc: "something"}) allow(machine.provider).to receive(:driver) { driver } subject.exec_command(machine, command, options) end end describe "without a command" do let(:argv) { [] } it "raises an error" do expect { subject.execute }.to raise_error(VagrantPlugins::DockerProvider::Errors::ExecCommandRequired) end end end end ================================================ FILE: test/unit/plugins/providers/docker/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/platform" require Vagrant.source_root.join("plugins/providers/docker/config") describe VagrantPlugins::DockerProvider::Config do include_context "unit" let(:machine) { double("machine") } let(:build_dir) do Dir.mktmpdir("vagrant-test-docker-provider-build-dir").tap do |dir| File.open(File.join(dir, "Dockerfile"), "wb+") do |f| f.write("Hello") end end end after do FileUtils.rm_rf(build_dir) end def assert_invalid errors = subject.validate(machine) if !errors.values.any? { |v| !v.empty? } raise "No errors: #{errors.inspect}" end end def assert_valid errors = subject.validate(machine) if !errors.values.all? { |v| v.empty? } raise "Errors: #{errors.inspect}" end end def valid_defaults subject.image = "foo" end describe "defaults" do before { subject.finalize! } its(:build_dir) { should be_nil } its(:git_repo) { should be_nil } its(:expose) { should eq([]) } its(:cmd) { should eq([]) } its(:env) { should eq({}) } its(:force_host_vm) { should be(false) } its(:host_vm_build_dir_options) { should be_nil } its(:image) { should be_nil } its(:name) { should be_nil } its(:privileged) { should be(false) } its(:stop_timeout) { should eq(1) } its(:vagrant_machine) { should be_nil } its(:vagrant_vagrantfile) { should be_nil } its(:auth_server) { should be_nil } its(:email) { should eq("") } its(:username) { should eq("") } its(:password) { should eq("") } end before do # By default lets be Linux for validations allow(Vagrant::Util::Platform).to receive(:linux).and_return(true) allow(Vagrant::Util::Platform).to receive(:linux?).and_return(true) end describe "should be invalid if any two or more of build dir, git repo and image are set" do it "build dir and image" do subject.build_dir = build_dir subject.image = "foo" subject.git_repo = nil subject.finalize! assert_invalid end it "build dir and git repo" do subject.build_dir = build_dir subject.git_repo = "http://example.com/something.git#branch:dir" subject.image = nil subject.finalize! assert_invalid end it "git repo dir and image" do subject.build_dir = nil subject.git_repo = "http://example.com/something.git#branch:dir" subject.image = "foo" subject.finalize! assert_invalid end it "build dir, git repo and image" do subject.build_dir = build_dir subject.git_repo = "http://example.com/something.git#branch:dir" subject.image = "foo" subject.finalize! assert_invalid end end describe "#build_dir" do it "should be valid if not set with image or git repo" do subject.build_dir = nil subject.git_repo = nil subject.image = "foo" subject.finalize! assert_valid end it "should be valid with a valid directory" do subject.build_dir = build_dir subject.finalize! assert_valid end end describe "#git_repo" do it "should be valid if not set with image or build dir" do subject.build_dir = nil subject.git_repo = "http://example.com/something.git#branch:dir" subject.image = nil subject.finalize! assert_valid end it "should be valid with a http git url" do subject.git_repo = "http://example.com/something.git#branch:dir" subject.finalize! assert_valid end it "should be valid with a git@ url" do subject.git_repo = "git@example.com:somebody/something" subject.finalize! assert_valid end it "should be valid with a git:// url" do subject.git_repo = "git://example.com/something" subject.finalize! assert_valid end it "should be valid with a short url beginning with github.com url" do subject.git_repo = "github.com/somebody/something" subject.finalize! assert_valid end it "should be invalid with an non-git url" do subject.git_repo = "http://foo.bar.com" subject.finalize! assert_invalid end it "should be invalid with an non url" do subject.git_repo = "http||://foo.bar.com sdfs" subject.finalize! assert_invalid end end describe "#compose" do before do valid_defaults end it "should be valid when enabled" do subject.compose = true subject.finalize! assert_valid end it "should be invalid when force_host_vm is enabled" do subject.compose = true subject.force_host_vm = true subject.finalize! assert_invalid end end describe "#create_args" do before do valid_defaults end it "is invalid if it isn't an array" do subject.create_args = "foo" subject.finalize! assert_invalid end end describe "#expose" do before do valid_defaults end it "uniqs the ports" do subject.expose = [1, 1, 4, 5] subject.finalize! assert_valid expect(subject.expose).to eq([1, 4, 5]) end end describe "#image" do it "should be valid if set" do subject.image = "foo" subject.finalize! assert_valid end it "should be invalid if not set" do subject.image = nil subject.finalize! assert_invalid end end describe "#link" do before do valid_defaults end it "should be valid with good links" do subject.link "foo:bar" subject.link "db:blah" subject.finalize! assert_valid end it "should be invalid if not name:alias" do subject.link "foo" subject.finalize! assert_invalid end it "should be invalid if too many colons" do subject.link "foo:bar:baz" subject.finalize! assert_invalid end end describe "#merge" do let(:one) { described_class.new } let(:two) { described_class.new } subject { one.merge(two) } context "#build_dir, #git_repo and #image" do it "overrides image if build_dir is set previously" do one.build_dir = "foo" two.image = "bar" expect(subject.build_dir).to be_nil expect(subject.image).to eq("bar") end it "overrides image if git_repo is set previously" do one.git_repo = "foo" two.image = "bar" expect(subject.image).to eq("bar") expect(subject.git_repo).to be_nil end it "overrides build_dir if image is set previously" do one.image = "foo" two.build_dir = "bar" expect(subject.build_dir).to eq("bar") expect(subject.image).to be_nil end it "overrides build_dir if git_repo is set previously" do one.git_repo = "foo" two.build_dir = "bar" expect(subject.build_dir).to eq("bar") expect(subject.git_repo).to be_nil end it "overrides git_repo if build_dir is set previously" do one.build_dir = "foo" two.git_repo = "bar" expect(subject.build_dir).to be_nil expect(subject.git_repo).to eq("bar") end it "overrides git_repo if image is set previously" do one.image = "foo" two.git_repo = "bar" expect(subject.image).to be_nil expect(subject.git_repo).to eq("bar") end it "preserves if both image and build_dir are set" do one.image = "foo" two.image = "baz" two.build_dir = "bar" expect(subject.build_dir).to eq("bar") expect(subject.image).to eq("baz") end it "preserves if both image and git_repo are set" do one.image = "foo" two.image = "baz" two.git_repo = "bar" expect(subject.image).to eq("baz") expect(subject.git_repo).to eq("bar") end it "preserves if both build_dir and git_repo are set" do one.build_dir = "foo" two.build_dir = "baz" two.git_repo = "bar" expect(subject.build_dir).to eq("baz") expect(subject.git_repo).to eq("bar") end end context "env vars" do it "should merge the values" do one.env["foo"] = "bar" two.env["bar"] = "baz" expect(subject.env).to eq({ "foo" => "bar", "bar" => "baz", }) end end context "exposed ports" do it "merges the exposed ports" do one.expose << 1234 two.expose = [42, 54] expect(subject.expose).to eq([ 1234, 42, 54]) end end context "links" do it "should merge the links" do one.link "foo" two.link "bar" expect(subject._links).to eq([ "foo", "bar"]) end end end describe "#vagrant_machine" do before { valid_defaults } it "should convert to a symbol" do subject.vagrant_machine = "foo" subject.finalize! assert_valid expect(subject.vagrant_machine).to eq(:foo) end end describe "#vagrant_vagrantfile" do before { valid_defaults } it "should be valid if set to a file" do subject.vagrant_vagrantfile = temporary_file.to_s subject.finalize! assert_valid end it "should not be valid if set to a non-existent place" do subject.vagrant_vagrantfile = "/i/shouldnt/exist" subject.finalize! assert_invalid end end end ================================================ FILE: test/unit/plugins/providers/docker/driver_compose_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "yaml" require_relative "../../../base" require Vagrant.source_root.join("lib/vagrant/util/deep_merge") require Vagrant.source_root.join("plugins/providers/docker/driver") describe VagrantPlugins::DockerProvider::Driver::Compose do let(:cmd_executed) { @cmd } let(:execute_result) { double("execute_result", exit_code: exit_code, stderr: stderr, stdout: stdout ) } let(:exit_code) { 0 } let(:stderr) { "" } let(:stdout) { "" } let(:cid) { 'side-1-song-10' } let(:docker_yml){ double("docker-yml", path: "/tmp-file") } let(:machine){ double("machine", env: env, name: :docker_1, id: :docker_id, provider_config: provider_config) } let(:compose_configuration){ {} } let(:provider_config) do double("provider-config", compose: true, compose_configuration: compose_configuration ) end let(:env) do double("env", cwd: Pathname.new("/compose/cwd"), local_data_path: local_data_path ) end let(:composition_content){ "--- {}\n" } let(:composition_path) do double("composition-path", to_s: "docker-compose.yml", exist?: true, read: composition_content, delete: true ) end let(:data_directory){ double("data-directory", join: composition_path) } let(:local_data_path){ double("local-data-path") } let(:compose_execute_up){ ["docker-compose", "-f", "docker-compose.yml", "-p", "cwd", "up", "--remove-orphans", "-d", any_args] } let(:compose_execute_up_regex) { /docker-compose -f docker-compose.yml -p cwd up --remove-orphans -d/ } subject{ described_class.new(machine) } before do @cmd = [] allow(Vagrant::Util::Subprocess).to receive(:execute) { |*args| if args.last.is_a?(Hash) args = args[0, args.size - 1] end invalid = args.detect { |a| !a.is_a?(String) } if invalid raise TypeError, "Vagrant::Util::Subprocess#execute only accepts signle option Hash and String arguments, received `#{invalid.class}'" end @cmd << args.join(" ") }.and_return(execute_result) allow_any_instance_of(Vagrant::Errors::VagrantError). to receive(:translate_error) { |*args| args.join(" ") } allow(Vagrant::Util::Which).to receive(:which).and_return("/dev/null/docker-compose") allow(env).to receive(:lock).and_yield allow(Pathname).to receive(:new).with(local_data_path).and_return(local_data_path) allow(Pathname).to receive(:new).with('/host/path').and_call_original allow(local_data_path).to receive(:join).and_return(data_directory) allow(data_directory).to receive(:mkpath) allow(FileUtils).to receive(:mv) allow(Tempfile).to receive(:new).with("vagrant-docker-compose").and_return(docker_yml) allow(docker_yml).to receive(:write) allow(docker_yml).to receive(:close) end describe '#build' do it 'creates a compose config with no extra options' do expect(subject).to receive(:update_composition) subject.build(composition_path) end it 'creates a compose config when given an array for build-arg' do expect(subject).to receive(:update_composition) subject.build(composition_path, extra_args: ["foo", "bar"]) end it 'creates a compose config when given a hash for build-arg' do expect(subject).to receive(:update_composition) subject.build(composition_path, extra_args: {"foo"=>"bar"}) end end describe '#create' do let(:params) { { image: 'jimi/hendrix:electric-ladyland', cmd: ['play', 'voodoo-chile'], ports: '8080:80', volumes: '/host/path:guest/path', detach: true, links: [[:janis, 'joplin'], [:janis, 'janis']], env: {key: 'value'}, name: cid, hostname: 'jimi-hendrix', privileged: true } } after { subject.create(params) expect(cmd_executed.first).to match(compose_execute_up_regex) } it 'sets container name' do expect(docker_yml).to receive(:write).with(/#{machine.name}/) end it 'forwards ports' do expect(docker_yml).to receive(:write).with(/#{params[:ports]}/) end it 'shares folders' do expect(docker_yml).to receive(:write).with(/#{params[:volumes]}/) end context 'when links are provided as strings' do before{ params[:links] = ["linkl1:linkr1", "linkl2:linkr2"] } it 'links containers' do params[:links].flatten.map{|l| l.split(':')}.each do |link| expect(docker_yml).to receive(:write).with(/#{link}/) end subject.create(params) end end context 'with relative path in share folders' do before do params[:volumes] = './path:guest/path' allow(Pathname).to receive(:new).with('./path').and_call_original allow(Pathname).to receive(:new).with('/compose/cwd/path').and_call_original end it 'should expand the relative host directory' do expect(docker_yml).to receive(:write).with(%r{/compose/cwd/path}) end end context 'with a volumes key in use for mounting' do let(:compose_config) { {"volumes"=>{"my_volume_key"=>"data"}} } before do params[:volumes] = 'my_volume_key:my/guest/path' allow(Pathname).to receive(:new).with('./path').and_call_original allow(Pathname).to receive(:new).with('my_volume_key').and_call_original allow(Pathname).to receive(:new).with('/compose/cwd/my_volume_key').and_call_original allow(subject).to receive(:get_composition).and_return(compose_config) end it 'should not expand the relative host directory' do expect(docker_yml).to receive(:write).with(%r{my_volume_key}) end end it 'links containers' do params[:links].each do |link| expect(docker_yml).to receive(:write).with(/#{link}/) end subject.create(params) end it 'sets environmental variables' do expect(docker_yml).to receive(:write).with(/key.*value/) end it 'is able to run a privileged container' do expect(docker_yml).to receive(:write).with(/privileged/) end it 'sets the hostname if specified' do expect(docker_yml).to receive(:write).with(/#{params[:hostname]}/) end it 'executes the provided command' do expect(docker_yml).to receive(:write).with(/#{params[:image]}/) end end describe '#created?' do let(:result) { subject.created?(cid) } it 'performs the check on all containers list' do subject.created?(cid) expect(cmd_executed.first).to match(/docker ps \-a \-q/) end context 'when container exists' do let(:stdout) { "foo\n#{cid}\nbar" } it { expect(result).to be_truthy } end context 'when container does not exist' do let(:stdout) { "foo\n#{cid}extra\nbar" } it { expect(result).to be_falsey } end end describe '#pull' do it 'should pull images' do subject.pull('foo') expect(cmd_executed.first).to eq("docker pull foo") end end describe '#running?' do let(:result) { subject.running?(cid) } it 'performs the check on the running containers list' do subject.running?(cid) expect(cmd_executed.first).to match(/docker ps \-q/) expect(cmd_executed.first).to_not include('-a') end context 'when container exists' do let(:stdout) { "foo\n#{cid}\nbar" } it { expect(result).to be_truthy } end context 'when container does not exist' do let(:stdout) { "foo\n#{cid}extra\nbar" } it { expect(result).to be_falsey } end end describe '#privileged?' do it 'identifies privileged containers' do allow(subject).to receive(:inspect_container) .and_return({'HostConfig' => {"Privileged" => true}}) expect(subject).to be_privileged(cid) end it 'identifies unprivileged containers' do allow(subject).to receive(:inspect_container) .and_return({'HostConfig' => {"Privileged" => false}}) expect(subject).to_not be_privileged(cid) end end describe '#start' do context 'when container is running' do before { allow(subject).to receive(:running?).and_return(true) } it 'does not start the container' do subject.start(cid) expect(cmd_executed).to be_empty end end context 'when container is not running' do before { allow(subject).to receive(:running?).and_return(false) } it 'starts the container' do subject.start(cid) expect(cmd_executed.first).to eq("docker start #{cid}") end end end describe '#stop' do context 'when container is running' do before { allow(subject).to receive(:running?).and_return(true) } it 'stops the container' do subject.stop(cid, 1) expect(cmd_executed.first).to eq("docker stop -t 1 #{cid}") end it "stops the container with the set timeout" do subject.stop(cid, 5) expect(cmd_executed.first).to eq("docker stop -t 5 #{cid}") end end context 'when container is not running' do before { allow(subject).to receive(:running?).and_return(false) } it 'does not stop container' do expect(subject).not_to receive(:execute).with('docker', 'stop', '-t', '1', cid) subject.stop(cid, 1) expect(cmd_executed).to be_empty end end end describe '#rm' do context 'when container has been created' do before { allow(subject).to receive(:created?).and_return(true) } it 'removes the container' do subject.rm(cid) expect(cmd_executed.first).to match(/docker-compose -f docker-compose.yml -p cwd rm -f docker_1/) end end context 'when container has not been created' do before { allow(subject).to receive(:created?).and_return(false) } it 'does not attempt to remove the container' do subject.rm(cid) expect(cmd_executed).to be_empty end end end describe '#inspect_container' do let(:stdout) { '[{"json": "value"}]' } it 'inspects the container' do subject.inspect_container(cid) expect(cmd_executed.first).to eq("docker inspect #{cid}") end it 'parses the json output' do expect(subject.inspect_container(cid)).to eq('json' => 'value') end end describe '#all_containers' do let(:stdout) { "container1\ncontainer2" } it 'returns an array of all known containers' do expect(subject.all_containers).to eq(['container1', 'container2']) expect(cmd_executed.first).to eq("docker ps -a -q --no-trunc") end end describe '#docker_bridge_ip' do let(:stdout) { " inet 123.456.789.012/16 " } it 'returns the bridge ip' do expect(subject.docker_bridge_ip).to eq('123.456.789.012') expect(cmd_executed.first).to eq("docker network inspect bridge") expect(cmd_executed.last).to eq("ip -4 addr show scope global docker0") end end end ================================================ FILE: test/unit/plugins/providers/docker/driver_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/providers/docker/driver") describe VagrantPlugins::DockerProvider::Driver do let(:cmd_executed) { @cmd } let(:cid) { 'side-1-song-10' } let(:execute_result) { double("execute_result", exit_code: exit_code, stderr: stderr, stdout: stdout ) } let(:exit_code) { 0 } let(:stderr) { "" } let(:stdout) { "" } before do allow(Vagrant::Util::Subprocess).to receive(:execute) { |*args| if args.last.is_a?(Hash) args = args[0, args.size - 1] end invalid = args.detect { |a| !a.is_a?(String) } if invalid raise TypeError, "Vagrant::Util::Subprocess#execute only accepts signle option Hash and String arguments, received `#{invalid.class}'" end @cmd = args.join(" ") }.and_return(execute_result) allow_any_instance_of(Vagrant::Errors::VagrantError). to receive(:translate_error) { |*args| args.join(" ") } end let(:docker_network_struct) { [ { "Name": "bridge", "Id": "ae74f6cc18bbcde86326937797070b814cc71bfc4a6d8e3e8cf3b2cc5c7f4a7d", "Created": "2019-03-20T14:10:06.313314662-07:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": nil, "Config": [ { "Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": { "Name": "vagrant-sandbox_docker-1_1553116237", "EndpointID": "fc1b0ed6e4f700cf88bb26a98a0722655191542e90df3e3492461f4d1f3c0cae", "MacAddress": "02:42:ac:11:00:02", "IPv4Address": "172.17.0.2/16", "IPv6Address": "" } }, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1500" }, "Labels": {} }, { "Name": "host", "Id": "2a2845e77550e33bf3e97bda8b71477ac7d3ccf78bc9102585fdb6056fb84cbf", "Created": "2018-09-28T10:54:08.633543196-07:00", "Scope": "local", "Driver": "host", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": nil, "Config": [] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": {}, "Options": {}, "Labels": {} }, { "Name": "vagrant_network", "Id": "93385d4fd3cf7083a36e62fa72a0ad0a21203d0ddf48409c32b550cd8462b3ba", "Created": "2019-03-20T14:10:36.828235585-07:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "172.18.0.0/16", "Gateway": "172.18.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": { "Name": "vagrant-sandbox_docker-1_1553116237", "EndpointID": "9502cd9d37ae6815e3ffeb0bc2de9b84f79e7223e8a1f8f4ccc79459e96c7914", "MacAddress": "02:42:ac:12:00:02", "IPv4Address": "172.18.0.2/16", "IPv6Address": "" } }, "Options": {}, "Labels": {} }, { "Name": "vagrant_network_172.20.0.0/16", "Id": "649f0ab3ef0eef6f2a025c0d0398bd7b9b4d05ec88b0d7bd573b44153d903cfb", "Created": "2019-03-20T14:10:37.088885647-07:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "172.20.0.0/16" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": { "Name": "vagrant-sandbox_docker-1_1553116237", "EndpointID": "e19156f8018f283468227fa97c145f4ea0eaba652fb7e977a0c759b1c3ec168a", "MacAddress": "02:42:ac:14:80:02", "IPv4Address": "172.20.0.2/16", "IPv6Address": "" } }, "Options": {}, "Labels": {} } ].to_json } describe '#build' do let(:stdout) { "Successfully built other_package\nSuccessfully built 1a2b3c4d" } let(:cid) { "1a2b3c4d" } it "builds a container with standard docker" do container_id = subject.build("/tmp/fakedir") expect(container_id).to eq(cid) end context "using buildkit" do let(:stdout) { "writing image sha256:1a2b3c4d 0.0s done" } it "builds a container with buildkit docker" do container_id = subject.build("/tmp/fakedir") expect(container_id).to eq(cid) end end context "using buildkit with old output" do let(:stdout) { "writing image sha256:1a2b3c4d done" } it "builds a container with buildkit docker (old output)" do container_id = subject.build("/tmp/fakedir") expect(container_id).to eq(cid) end end context "using buildkit with containerd backend output" do let(:stdout) { "exporting manifest list sha256:1a2b3c4d done" } it "builds a container with buildkit docker (containerd)" do container_id = subject.build("/tmp/fakedir") expect(container_id).to eq(cid) end end context "using podman emulating docker CLI" do let(:stdout) { "1a2b3c4d5e6f7g8h9i10j11k12l13m14n16o17p18q19r20s21t22u23v24w25x2" } it "builds a container with podman emulating docker CLI" do allow(subject).to receive(:podman?).and_return(true) container_id = subject.build("/tmp/fakedir") expect(container_id).to eq(cid) end context "if output contains extra trailing information" do let(:stdout) { "1a2b3c4d5e6f7g8h9i10j11k12l13m14n16o17p18q19r20s21t22u23v24w25x2\nextra content\n" } it "builds a container with podman emulating docker CLI" do allow(subject).to receive(:podman?).and_return(true) container_id = subject.build("/tmp/fakedir") expect(container_id).to eq(cid) end end end end describe '#podman?' do context "when docker is used" do let(:stdout) { "Docker version 1.8.1, build d12ea79" } it 'returns false' do expect(subject.podman?).to be false end end context "when podman is used" do let(:stdout) { "podman version 1.7.1-dev" } it 'returns true' do expect(subject.podman?).to be true end end end describe '#create' do let(:params) { { image: 'jimi/hendrix:electric-ladyland', cmd: ['play', 'voodoo-chile'], ports: '8080:80', volumes: '/host/path:guest/path', detach: true, links: [[:janis, 'joplin'], [:janis, 'janis']], env: {key: 'value'}, name: cid, hostname: 'jimi-hendrix', privileged: true } } before { subject.create(params) } it 'runs a detached docker image' do expect(cmd_executed).to match(/^docker run .+ -d .+ #{Regexp.escape params[:image]}/) end it 'sets container name' do expect(cmd_executed).to match(/--name #{Regexp.escape params[:name]}/) end it 'forwards ports' do expect(cmd_executed).to match(/-p #{params[:ports]} .+ #{Regexp.escape params[:image]}/) end it 'shares folders' do expect(cmd_executed).to match(/-v #{params[:volumes]} .+ #{Regexp.escape params[:image]}/) end it 'links containers' do params[:links].each do |link| expect(cmd_executed).to match(/--link #{link.join(':')} .+ #{Regexp.escape params[:image]}/) end end it 'sets environmental variables' do expect(cmd_executed).to match(/-e key=value .+ #{Regexp.escape params[:image]}/) end it 'is able to run a privileged container' do expect(cmd_executed).to match(/--privileged .+ #{Regexp.escape params[:image]}/) end it 'sets the hostname if specified' do expect(cmd_executed).to match(/-h #{params[:hostname]} #{Regexp.escape params[:image]}/) end it 'executes the provided command' do expect(cmd_executed).to match(/#{Regexp.escape params[:image]} #{Regexp.escape params[:cmd].join(' ')}/) end end describe '#create windows' do let(:params) { { image: 'jimi/hendrix:eletric-ladyland', cmd: ['play', 'voodoo-chile'], ports: '8080:80', volumes: 'C:/Users/BobDylan/AllAlong:/The/Watchtower', detach: true, links: [[:janis, 'joplin'], [:janis, 'janis']], env: {key: 'value'}, name: cid, hostname: 'jimi-hendrix', privileged: true } } let(:translated_path) { "//c/Users/BobDylan/AllAlong:/The/Watchtower" } before do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) subject.create(params) end it 'shares folders' do expect(cmd_executed).to match(/-v #{translated_path} .+ #{Regexp.escape params[:image]}/) end end describe '#created?' do let(:result) { subject.created?(cid) } it 'performs the check on all containers list' do subject.created?(cid) expect(cmd_executed).to match(/docker ps \-a \-q/) end context 'when container exists' do let(:stdout) { "foo\n#{cid}\nbar" } it { expect(result).to be_truthy } end context 'when container does not exist' do let(:stdout) { "foo\n#{cid}extra\nbar" } it { expect(result).to be_falsey } end end describe '#image?' do let(:result) { subject.image?(cid) } it 'performs the check on all images list' do subject.image?(cid) expect(cmd_executed).to match(/docker images \-q \--no-trunc/) end context 'when image id exists' do let(:stdout) { "foo\n#{cid}\nbar" } it { expect(result).to be_truthy } end context 'when sha265 image id exists' do let(:stdout) { "sha256:foo\nsha256:#{cid}\nsha256:bar" } it { expect(result).to be_truthy } end context 'when image does not exist' do let(:stdout) { "foo\n#{cid}extra\nbar" } it { expect(result).to be_falsey } end end describe '#pull' do it 'should pull images' do subject.pull('foo') expect(cmd_executed).to match(/docker pull foo/) end end describe '#read_used_ports' do let(:all_containers) { ["container1\ncontainer2"] } let(:container_info) { {"Name"=>"/container", "State"=>{"Running"=>true}, "HostConfig"=>{"PortBindings"=>{}}} } let(:empty_used_ports) { {} } context "with existing port forwards" do let(:container_info) { {"Name"=>"/container","State"=>{"Running"=>true}, "HostConfig"=>{"PortBindings"=>{"22/tcp"=>[{"HostIp"=>"127.0.0.1","HostPort"=>"2222"}] }}} } let(:used_ports_set) { {"2222"=>Set["127.0.0.1"]} } context "with active containers" do it 'should read all port bindings and return a hash of sets' do allow(subject).to receive(:all_containers).and_return(all_containers) allow(subject).to receive(:inspect_container).and_return(container_info) used_ports = subject.read_used_ports expect(used_ports).to eq(used_ports_set) end end context "with inactive containers" do let(:container_info) { {"Name"=>"/container", "State"=>{"Running"=>false}, "HostConfig"=>{"PortBindings"=>{"22/tcp"=>[{"HostIp"=>"127.0.0.1","HostPort"=>"2222"}] }}} } it 'returns empty' do allow(subject).to receive(:all_containers).and_return(all_containers) allow(subject).to receive(:inspect_container).and_return(container_info) used_ports = subject.read_used_ports expect(used_ports).to eq(empty_used_ports) end end end it 'returns empty if no ports are already bound' do allow(subject).to receive(:all_containers).and_return(all_containers) allow(subject).to receive(:inspect_container).and_return(container_info) used_ports = subject.read_used_ports expect(used_ports).to eq(empty_used_ports) end end describe '#running?' do let(:result) { subject.running?(cid) } it 'performs the check on the running containers list' do subject.running?(cid) expect(cmd_executed).to match(/docker ps \-q/) expect(cmd_executed).to_not include('-a') end context 'when container exists' do let(:stdout) { "foo\n#{cid}\nbar" } it { expect(result).to be_truthy } end context 'when container does not exist' do let(:stdout) { "foo\n#{cid}extra\nbar" } it { expect(result).to be_falsey } end end describe '#privileged?' do it 'identifies privileged containers' do allow(subject).to receive(:inspect_container).and_return({'HostConfig' => {"Privileged" => true}}) expect(subject).to be_privileged(cid) end it 'identifies unprivileged containers' do allow(subject).to receive(:inspect_container).and_return({'HostConfig' => {"Privileged" => false}}) expect(subject).to_not be_privileged(cid) end end describe '#start' do context 'when container is running' do before { allow(subject).to receive(:running?).and_return(true) } it 'does not start the container' do subject.start(cid) expect(cmd_executed).to be_nil end end context 'when container is not running' do before { allow(subject).to receive(:running?).and_return(false) } it 'starts the container' do subject.start(cid) expect(cmd_executed).to eq("docker start #{cid}") end end end describe '#stop' do context 'when container is running' do before { allow(subject).to receive(:running?).and_return(true) } it 'stops the container' do subject.stop(cid, 1) expect(cmd_executed).to eq("docker stop -t 1 #{cid}") end it "stops the container with the set timeout" do subject.stop(cid, 5) expect(cmd_executed).to eq("docker stop -t 5 #{cid}") end end context 'when container is not running' do before { allow(subject).to receive(:running?).and_return(false) } it 'does not stop container' do subject.stop(cid, 1) expect(cmd_executed).to be_nil end end end describe '#rm' do context 'when container has been created' do before { allow(subject).to receive(:created?).and_return(true) } it 'removes the container' do subject.rm(cid) expect(cmd_executed).to eq("docker rm -f -v #{cid}") end end context 'when container has not been created' do before { allow(subject).to receive(:created?).and_return(false) } it 'does not attempt to remove the container' do subject.rm(cid) expect(cmd_executed).to be_nil end end end describe '#rmi' do let(:id) { 'asdg21ew' } context 'image exists' do it "removes the image" do subject.rmi(id) expect(cmd_executed).to eq("docker rmi #{id}") end end context 'image is being used by running container' do before { allow(subject).to receive(:execute).and_raise("image is being used by running container") } it 'does not remove the image' do expect(subject.rmi(id)).to eq(false) subject.rmi(id) end end context 'image is being used by stopped container' do before { allow(subject).to receive(:execute).and_raise("image is being used by stopped container") } it 'does not remove the image' do expect(subject.rmi(id)).to eq(false) subject.rmi(id) end end context 'container is using it' do before { allow(subject).to receive(:execute).and_raise("container is using it") } it 'does not remove the image' do expect(subject.rmi(id)).to eq(false) subject.rmi(id) end end context 'image does not exist' do before { allow(subject).to receive(:execute).and_raise("No such image") } it 'raises an error' do expect(subject.rmi(id)).to eq(nil) subject.rmi(id) end end context 'image is in use by a container' do before { allow(subject).to receive(:execute).and_raise("image is in use by a container") } it 'does not remove the image' do expect(subject.rmi(id)).to eq(false) subject.rmi(id) end end end describe '#inspect_container' do let(:stdout) { '[{"json": "value"}]' } it 'inspects the container' do subject.inspect_container(cid) expect(cmd_executed).to eq("docker inspect #{cid}") end it 'parses the json output' do expect(subject.inspect_container(cid)).to eq('json' => 'value') end end describe '#all_containers' do let(:stdout) { "container1\ncontainer2" } it 'returns an array of all known containers' do expect(subject.all_containers).to eq(['container1', 'container2']) expect(cmd_executed).to eq("docker ps -a -q --no-trunc") end end describe '#docker_bridge_ip' do context 'from docker network command' do let(:network_struct) { [{ "Name": "bridge", "Id": "ae74f6cc18bbcde86326937797070b814cc71bfc4a6d8e3e8cf3b2cc5c7f4a7d", "Created": "2019-03-20T14:10:06.313314662-07:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": nil, "Config": [ { "Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": { "Name": "vagrant-sandbox_docker-1_1553116237", "EndpointID": "fc1b0ed6e4f700cf88bb26a98a0722655191542e90df3e3492461f4d1f3c0cae", "MacAddress": "02:42:ac:11:00:02", "IPv4Address": "172.17.0.2/16", "IPv6Address": "" } }, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1500" }, "Labels": {} }].to_json } it "returns the bridge gateway ip" do allow(subject).to receive(:inspect_network).and_return(JSON.load(network_struct)) expect(subject.docker_bridge_ip).to eq('172.17.0.1') end end context 'when falling back to ip' do let(:stdout) { " inet 123.456.789.012/16 " } it 'returns the bridge ip' do expect(subject.docker_bridge_ip).to eq('123.456.789.012') expect(cmd_executed).to eq("ip -4 addr show scope global docker0") end end end describe '#docker_connect_network' do let(:opts) { ["--ip", "172.20.128.2"] } it 'connects a network to a container' do subject.connect_network("vagrant_network", cid, opts) expect(cmd_executed).to eq("docker network connect vagrant_network #{cid} --ip 172.20.128.2") end end describe '#docker_create_network' do let(:opts) { ["--subnet", "172.20.0.0/16"] } it 'creates a network' do subject.create_network("vagrant_network", opts) expect(cmd_executed).to eq("docker network create vagrant_network --subnet 172.20.0.0/16") end end describe '#docker_disconnet_network' do it 'disconnects a network from a container' do subject.disconnect_network("vagrant_network", cid) expect(cmd_executed).to eq("docker network disconnect vagrant_network #{cid} --force") end end describe '#docker_inspect_network' do it 'gets info about a network' do subject.inspect_network("vagrant_network") expect(cmd_executed).to eq("docker network inspect vagrant_network") end end describe '#docker_list_network' do it 'lists docker networks' do subject.list_network() expect(cmd_executed).to eq("docker network ls") end end describe '#docker_rm_network' do it 'deletes a docker network' do subject.rm_network("vagrant_network") expect(cmd_executed).to eq("docker network rm vagrant_network") end end describe '#network_defined?' do let(:subnet_string) { "172.20.0.0/16" } let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] } before do allow(subject).to receive(:list_network_names).and_return(network_names) allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct)) end it "returns network name if defined" do network_name = subject.network_defined?(subnet_string) expect(network_name).to eq("vagrant_network_172.20.0.0/16") end it "returns nil name if not defined" do network_name = subject.network_defined?("120.20.0.0/24") expect(network_name).to eq(nil) end context "when config information is missing" do let(:docker_network_struct) do [ { "Name": "bridge", "Id": "ae74f6cc18bbcde86326937797070b814cc71bfc4a6d8e3e8cf3b2cc5c7f4a7d", "Created": "2019-03-20T14:10:06.313314662-07:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": nil, }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": { "Name": "vagrant-sandbox_docker-1_1553116237", "EndpointID": "fc1b0ed6e4f700cf88bb26a98a0722655191542e90df3e3492461f4d1f3c0cae", "MacAddress": "02:42:ac:11:00:02", "IPv4Address": "172.17.0.2/16", "IPv6Address": "" }, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1500" }, "Labels": {} }, } ].to_json end it "should not raise an error" do expect { subject.network_defined?(subnet_string) }.not_to raise_error end end context "when IPAM information is missing" do let(:docker_network_struct) do [ { "Name": "bridge", "Id": "ae74f6cc18bbcde86326937797070b814cc71bfc4a6d8e3e8cf3b2cc5c7f4a7d", "Created": "2019-03-20T14:10:06.313314662-07:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "a1ee9b12bcea8268495b1f43e8d1285df1925b7174a695075f6140adb9415d87": { "Name": "vagrant-sandbox_docker-1_1553116237", "EndpointID": "fc1b0ed6e4f700cf88bb26a98a0722655191542e90df3e3492461f4d1f3c0cae", "MacAddress": "02:42:ac:11:00:02", "IPv4Address": "172.17.0.2/16", "IPv6Address": "" }, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1500" }, "Labels": {} }, } ].to_json end it "should not raise an error" do expect { subject.network_defined?(subnet_string) }.not_to raise_error end end end describe '#network_containing_address' do let(:address) { "172.20.128.2" } let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] } it "returns the network name if it contains the requested address" do allow(subject).to receive(:list_network_names).and_return(network_names) allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct)) network_name = subject.network_containing_address(address) expect(network_name).to eq("vagrant_network_172.20.0.0/16") end it "returns nil if no networks contain the requested address" do allow(subject).to receive(:list_network_names).and_return(network_names) allow(subject).to receive(:inspect_network).and_return(JSON.load(docker_network_struct)) network_name = subject.network_containing_address("127.0.0.1") expect(network_name).to eq(nil) end end describe '#existing_named_network?' do let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] } it "returns true if the network exists" do allow(subject).to receive(:list_network_names).and_return(network_names) expect(subject.existing_named_network?("vagrant_network_172.20.0.0/16")).to be_truthy end it "returns false if the network does not exist" do allow(subject).to receive(:list_network_names).and_return(network_names) expect(subject.existing_named_network?("vagrant_network_17.0.0/16")).to be_falsey end end describe '#list_network_names' do let(:unparsed_network_names) { "vagrant_network_172.20.0.0/16\nbridge\nnull" } let(:network_names) { ["vagrant_network_172.20.0.0/16", "bridge", "null" ] } it "lists the network names" do allow(subject).to receive(:list_network).with("--format={{.Name}}"). and_return(unparsed_network_names) expect(subject.list_network_names).to eq(network_names) end end describe '#network_used?' do let(:network_name) { "vagrant_network_172.20.0.0/16" } it "returns nil if no networks" do allow(subject).to receive(:inspect_network).with(network_name).and_return(nil) expect(subject.network_used?(network_name)).to eq(nil) end it "returns true if network has containers in use" do allow(subject).to receive(:inspect_network).with(network_name).and_return([JSON.load(docker_network_struct).last]) expect(subject.network_used?(network_name)).to be_truthy end it "returns false if network has containers in use" do allow(subject).to receive(:inspect_network).with("host").and_return([JSON.load(docker_network_struct)[1]]) expect(subject.network_used?("host")).to be_falsey end end end ================================================ FILE: test/unit/plugins/providers/docker/provider_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/providers/docker/provider") describe VagrantPlugins::DockerProvider::Provider do let(:driver_obj){ double("driver") } let(:provider){ double("provider", driver: driver_obj) } let(:provider_config){ double("provider_config", force_host_vm: false) } let(:ssh) { double("ssh", guest_port: 22) } let(:config) { double("config", ssh: ssh) } let(:machine){ double("machine", provider: provider, provider_config: provider_config, config: config) } let(:platform) { double("platform") } subject { described_class.new(machine) } before do stub_const("Vagrant::Util::Platform", platform) allow(machine).to receive(:id).and_return("foo") end describe ".usable?" do subject { described_class } it "returns true if usable" do allow(VagrantPlugins::DockerProvider::Driver).to receive(:new).and_return(driver_obj) allow(provider_config).to receive(:compose).and_return(false) allow(driver_obj).to receive(:execute).with("docker", "version").and_return(true) expect(subject).to be_usable end it "raises an exception if docker is not available" do allow(VagrantPlugins::DockerProvider::Driver).to receive(:new).and_return(driver_obj) allow(provider_config).to receive(:compose).and_return(false) allow(platform).to receive(:windows?).and_return(false) allow(platform).to receive(:darwin?).and_return(false) allow(driver_obj).to receive(:execute).with("docker", "version"). and_raise(Vagrant::Errors::CommandUnavailable, file: "docker") expect { subject.usable?(true) }. to raise_error(Vagrant::Errors::CommandUnavailable) end end describe "#driver" do it "is initialized" do allow(provider_config).to receive(:compose).and_return(false) allow(platform).to receive(:windows?).and_return(false) allow(platform).to receive(:darwin?).and_return(false) expect(subject.driver).to be_kind_of(VagrantPlugins::DockerProvider::Driver) end end describe "#state" do before { allow(subject).to receive(:driver).and_return(driver_obj) } it "returns not_created if no ID" do allow(machine).to receive(:id).and_return(nil) expect(subject.state.id).to eq(:not_created) end it "calls an action to determine the ID" do allow(provider_config).to receive(:compose).and_return(false) allow(platform).to receive(:windows?).and_return(false) allow(platform).to receive(:darwin?).and_return(false) expect(machine).to receive(:id).and_return("foo") expect(driver_obj).to receive(:created?).with("foo").and_return(false) expect(subject.state.id).to eq(:not_created) end end describe "#host_vm" do let(:host_env) { double("host_env", root_path: "/vagrant.d", default_provider: :virtualbox) } it "returns the host machine object" do allow(machine.provider_config).to receive(:vagrant_vagrantfile).and_return("/path/to/Vagrantfile") allow(machine.provider_config).to receive(:vagrant_machine).and_return(:default) allow(machine).to receive(:env).and_return(double("env")) allow(machine.env).to receive(:root_path).and_return("/.vagrant.d") allow(machine.env).to receive(:home_path).and_return("/path/to") allow(machine.env).to receive(:ui_class).and_return(true) expect(Vagrant::Environment).to receive(:new).and_return(host_env) allow(host_env).to receive(:machine).and_return(true) subject.host_vm end end describe "#ssh_info" do let(:result) { "127.0.0.1" } let(:exit_code) { 0 } let(:ssh_info) {{:host=>result,:port=>22}} let(:network_settings) { {"NetworkSettings" => {"Bridge"=>"", "SandboxID"=>"randomid", "HairpinMode"=>false, "LinkLocalIPv6Address"=>"", "LinkLocalIPv6PrefixLen"=>0, "Ports"=>{"443/tcp"=>nil, "80/tcp"=>nil}, "SandboxKey"=>"/var/run/docker/netns/158b7024a9e4", "SecondaryIPAddresses"=>nil, "SecondaryIPv6Addresses"=>nil, "EndpointID"=>"randomEndpointID", "Gateway"=>"172.17.0.1", "GlobalIPv6Address"=>"", "GlobalIPv6PrefixLen"=>0, "IPAddress"=>"127.0.0.1", "IPPrefixLen"=>16, "IPv6Gateway"=>"", "MacAddress"=>"02:42:ac:11:00:02", "Networks"=>{"bridge"=>{"IPAMConfig"=>nil, "Links"=>nil, "Aliases"=>nil, "NetworkID"=>"networkIDVar", "EndpointID"=>"endpointIDVar", "Gateway"=>"127.0.0.1", "IPAddress"=>"127.0.0.1", "IPPrefixLen"=>16, "IPv6Gateway"=>"", "GlobalIPv6Address"=>"", "GlobalIPv6PrefixLen"=>0, "MacAddress"=>"02:42:ac:11:00:02", "DriverOpts"=>nil}}}} } let(:empty_network_settings) { {"NetworkSettings" => {"Bridge"=>"", "SandboxID"=>"randomid", "HairpinMode"=>false, "LinkLocalIPv6Address"=>"", "LinkLocalIPv6PrefixLen"=>0, "Ports"=>"", "SandboxKey"=>"/var/run/docker/netns/158b7024a9e4", "SecondaryIPAddresses"=>nil, "SecondaryIPv6Addresses"=>nil, "EndpointID"=>"randomEndpointID", "Gateway"=>"172.17.0.1", "GlobalIPv6Address"=>"", "GlobalIPv6PrefixLen"=>0, "IPAddress"=>"", "IPPrefixLen"=>16, "IPv6Gateway"=>"", "MacAddress"=>"02:42:ac:11:00:02", "Networks"=>{"bridge"=>{"IPAMConfig"=>nil, "Links"=>nil, "Aliases"=>nil, "NetworkID"=>"networkIDVar", "EndpointID"=>"endpointIDVar", "Gateway"=>"127.0.0.1", "IPAddress"=>"127.0.0.1", "IPPrefixLen"=>16, "IPv6Gateway"=>"", "GlobalIPv6Address"=>"", "GlobalIPv6PrefixLen"=>0, "MacAddress"=>"02:42:ac:11:00:02", "DriverOpts"=>nil}}}} } before do allow(VagrantPlugins::DockerProvider::Driver).to receive(:new).and_return(driver_obj) allow(machine).to receive(:action).with(:read_state).and_return(machine_state_id: :running) end it "returns nil if a port info is nil from the driver" do allow(provider_config).to receive(:compose).and_return(false) allow(platform).to receive(:windows?).and_return(false) allow(platform).to receive(:darwin?).and_return(false) allow(driver_obj).to receive(:created?).and_return(true) allow(driver_obj).to receive(:state).and_return(:running) allow(driver_obj).to receive(:inspect_container).and_return(empty_network_settings) expect(subject.ssh_info).to eq(nil) end it "should receive a valid address" do allow(provider_config).to receive(:compose).and_return(false) allow(platform).to receive(:windows?).and_return(false) allow(platform).to receive(:darwin?).and_return(false) allow(driver_obj).to receive(:created?).and_return(true) allow(driver_obj).to receive(:state).and_return(:running) allow(driver_obj).to receive(:execute).with(:get_network_config).and_return(result) allow(driver_obj).to receive(:inspect_container).and_return(network_settings) expect(subject.ssh_info).to eq(ssh_info) end end end ================================================ FILE: test/unit/plugins/providers/docker/synced_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/providers/docker/synced_folder") describe VagrantPlugins::DockerProvider::SyncedFolder do subject { described_class.new } let(:provider_config) { double("provider_config", volumes: []) } let(:machine) { double("machine") } before do allow(machine).to receive(:provider_name).and_return(:docker) allow(machine).to receive(:provider_config).and_return(provider_config) end describe "#usable?" do it "is usable" do expect(subject).to be_usable(machine) end it "is not usable if provider isn't docker" do allow(machine).to receive(:provider_name).and_return(:virtualbox) expect(subject).to_not be_usable(machine) end it "raises an error if bad provider if specified" do allow(machine).to receive(:provider_name).and_return(:virtualbox) expect { subject.usable?(machine, true) }. to raise_error(VagrantPlugins::DockerProvider::Errors::SyncedFolderNonDocker) end end describe "#prepare" do let(:folders) {{"/guest/dir1"=> {:guestpath=>"/guest/dir1", :hostpath=>"/Users/brian/code/vagrant-sandbox", :disabled=>false, :__vagrantfile=>true}, "/dev/vagrant"=> {:guestpath=>"/dev/vagrant", :hostpath=>"/Users/brian/code/vagrant", :disabled=>false, :__vagrantfile=>true}}} let(:consistency_folders) {{"/guest/dir1"=> {:docker_consistency=>"cached", :guestpath=>"/guest/dir1", :hostpath=>"/Users/brian/code/vagrant-sandbox", :disabled=>false, :__vagrantfile=>true}, "/dev/vagrant"=> {:docker_consistency=>"delegated", :guestpath=>"/dev/vagrant", :hostpath=>"/Users/brian/code/vagrant", :disabled=>false, :__vagrantfile=>true}}} let(:options) { {} } let(:volumes) { ["/Users/brian/code/vagrant-sandbox:/guest/dir1", "/Users/brian/code/vagrant:/dev/vagrant"] } let(:consistency_volumes) { ["/Users/brian/code/vagrant-sandbox:/guest/dir1:cached", "/Users/brian/code/vagrant:/dev/vagrant:delegated"] } it "prepares folders to mount" do subject.prepare(machine, folders, options) expect(machine.provider_config.volumes).to eq(volumes) end it "sets volume consistency if specified" do subject.prepare(machine, consistency_folders, options) expect(machine.provider_config.volumes).to eq(consistency_volumes) end end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/check_enabled_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/check_enabled") describe VagrantPlugins::HyperV::Action::CheckEnabled do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider) } let(:subject){ described_class.new(app, env) } it "should continue when Hyper-V is enabled" do expect(driver).to receive(:execute).and_return("result" => true) expect(app).to receive(:call) subject.call(env) end it "should raise error when Hyper-V is not enabled" do expect(driver).to receive(:execute).and_return("result" => false) expect(app).not_to receive(:call) expect{ subject.call(env) }.to raise_error(VagrantPlugins::HyperV::Errors::PowerShellFeaturesDisabled) end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/configure_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/configure") describe VagrantPlugins::HyperV::Action::Configure do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider, config: config, provider_config: provider_config, data_dir: data_dir, id: "machineID") } let(:data_dir){ double("data_dir") } let(:config){ double("config", vm: vm) } let(:vm){ double("vm", networks: networks) } let(:networks){ [] } let(:switches){ [ {"Name" => "Switch1", "Id" => "ID1"}, {"Name" => "Switch2", "Id" => "ID2"} ]} let(:sentinel){ double("sentinel") } let(:provider_config){ double("provider_config", memory: "1024", maxmemory: "1024", cpus: 1, auto_start_action: "Nothing", auto_stop_action: "Save", enable_checkpoints: false, enable_automatic_checkpoints: true, enable_virtualization_extensions: false, vm_integration_services: vm_integration_services, enable_enhanced_session_mode: enable_enhanced_session_mode ) } let(:vm_integration_services){ {} } let(:enable_enhanced_session_mode){ false } let(:subject){ described_class.new(app, env) } before do allow(driver).to receive(:execute) allow(app).to receive(:call) expect(driver).to receive(:execute).with(:get_switches).and_return(switches) allow(ui).to receive(:ask).and_return("1") allow(data_dir).to receive(:join).and_return(sentinel) allow(sentinel).to receive(:file?).and_return(false) allow(sentinel).to receive(:open) allow(driver).to receive(:set_enhanced_session_transport_type).with("VMBus") end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end context "with missing switch sentinel file" do it "should prompt for switch to use" do expect(ui).to receive(:ask) subject.call(env) end it "should write sentinel file" do expect(sentinel).to receive(:open) subject.call(env) end end context "with existing switch sentinel file" do before{ allow(sentinel).to receive(:file?).twice.and_return(true) } it "should not prompt for switch to use" do expect(ui).not_to receive(:ask) subject.call(env) end it "should not write sentinel file" do expect(sentinel).not_to receive(:open) subject.call(env) end end context "with bridge defined in networks" do context "with valid bridge switch name" do let(:networks){ [[:public_network, {bridge: "Switch1"}]] } it "should not prompt for switch" do expect(ui).not_to receive(:ask) subject.call(env) end end context "with valid bridge switch ID" do let(:networks){ [[:public_network, {bridge: "ID1"}]] } it "should not prompt for switch" do expect(ui).not_to receive(:ask) subject.call(env) end end context "with invalid bridge switch name" do let(:networks){ [[:public_network, {bridge: "UNKNOWN"}]] } it "should prompt for switch" do expect(ui).to receive(:ask) subject.call(env) end end end context "with integration services enabled" do let(:vm_integration_services){ {service: true} } it "should call the driver to set the services" do expect(driver).to receive(:set_vm_integration_services) subject.call(env) end end context "without enhanced session transport type" do it "should call the driver to set enhanced session transport type back to default" do expect(driver).to receive(:set_enhanced_session_transport_type).with("VMBus") subject.call(env) end end context "with enhanced session transport type" do let(:enable_enhanced_session_mode) { true } it "should call the driver to set enhanced session transport type" do expect(driver).to receive(:set_enhanced_session_transport_type).with("HvSocket") subject.call(env) end end context "without available switches" do let(:switches){ [] } it "should raise an error" do expect{ subject.call(env) }.to raise_error(VagrantPlugins::HyperV::Errors::NoSwitches) end end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/delete_vm_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/delete_vm") describe VagrantPlugins::HyperV::Action::DeleteVM do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider, data_dir: "/dev/null") } let(:subject){ described_class.new(app, env) } before do allow(app).to receive(:call) allow(driver).to receive(:delete_vm) allow(FileUtils).to receive(:rm_rf) allow(FileUtils).to receive(:mkdir_p) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end it "should call the driver to delete the vm" do expect(driver).to receive(:delete_vm) subject.call(env) end it "should delete the data directory" do expect(FileUtils).to receive(:rm_rf).with(machine.data_dir) subject.call(env) end it "should recreate the data directory" do expect(FileUtils).to receive(:mkdir_p).with(machine.data_dir) subject.call(env) end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/export_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/export") describe VagrantPlugins::HyperV::Action::Export do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider, state: state) } let(:state){ double("state", id: machine_state) } let(:machine_state){ :off } let(:subject){ described_class.new(app, env) } before do allow(app).to receive(:call) allow(driver).to receive(:export) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end it "should call the driver to perform the export" do expect(driver).to receive(:export) subject.call(env) end context "with invalid machine state" do let(:machine_state){ :on } it "should raise an error" do expect{ subject.call(env) }.to raise_error(Vagrant::Errors::VMPowerOffToPackage) end end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/import_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/import") describe VagrantPlugins::HyperV::Action::Import do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider, provider_config: provider_config, box: box, data_dir: data_dir, name: "machname") } let(:provider_config){ double("provider_config", linked_clone: false, vmname: "VMNAME", cpus: nil, memory: nil, maxmemory: nil, ) } let(:box){ double("box", directory: box_directory) } let(:box_directory){ double("box_directory") } let(:data_dir){ double("data_dir") } let(:vm_dir){ double("vm_dir") } let(:hd_dir){ double("hd_dir") } let(:subject){ described_class.new(app, env) } before do allow(app).to receive(:call) allow(box_directory).to receive(:join).with("Virtual Machines").and_return(vm_dir) allow(box_directory).to receive(:join).with("Virtual Hard Disks").and_return(hd_dir) allow(vm_dir).to receive(:directory?).and_return(true) allow(vm_dir).to receive(:each_child).and_yield(Pathname.new("file.txt")) allow(hd_dir).to receive(:directory?).and_return(true) allow(hd_dir).to receive(:each_child).and_yield(Pathname.new("file.txt")) allow(driver).to receive(:has_vmcx_support?).and_return(true) allow(data_dir).to receive(:join).and_return(data_dir) allow(data_dir).to receive(:to_s).and_return("DATA_DIR_PATH") allow(driver).to receive(:import).and_return("id" => "VMID") allow(machine).to receive(:id=) end context "with missing virtual machines directory" do before{ expect(vm_dir).to receive(:directory?).and_return(false) } it "should raise an error" do expect{ subject.call(env) }.to raise_error(VagrantPlugins::HyperV::Errors::BoxInvalid) end end context "with missing hard disks directory" do before{ expect(hd_dir).to receive(:directory?).and_return(false) } it "should raise an error" do expect{ subject.call(env) }.to raise_error(VagrantPlugins::HyperV::Errors::BoxInvalid) end end context "with missing configuration file" do before do allow(hd_dir).to receive(:each_child).and_yield(Pathname.new("image.vhd")) end it "should raise an error" do expect{ subject.call(env) }.to raise_error(VagrantPlugins::HyperV::Errors::BoxInvalid) end end context "with missing image file" do before do allow(vm_dir).to receive(:each_child).and_yield(Pathname.new("config.xml")) end it "should raise an error" do expect{ subject.call(env) }.to raise_error(VagrantPlugins::HyperV::Errors::BoxInvalid) end end context "with image and config files" do before do allow(vm_dir).to receive(:each_child).and_yield(Pathname.new("config.xml")) allow(hd_dir).to receive(:each_child).and_yield(Pathname.new("image.vhd")) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end it "should request import via the driver" do expect(driver).to receive(:import).and_return("id" => "VMID") subject.call(env) end it "should set the machine ID after import" do expect(machine).to receive(:id=).with("VMID") subject.call(env) end context "VM ID result is Array" do before do expect(driver).to receive(:import).and_return("id" => "VMID") end it "should properly set the machine ID" do expect(machine).to receive(:id=).with("VMID") subject.call(env) end end context "with no vmcx support" do before do expect(driver).to receive(:has_vmcx_support?).and_return(false) end it "should match XML config file" do subject.call(env) end it "should not match VMCX config file" do expect(vm_dir).to receive(:each_child).and_yield(Pathname.new("config.vmcx")) expect{ subject.call(env) }.to raise_error(VagrantPlugins::HyperV::Errors::BoxInvalid) end end context "with vmcx support" do before do expect(driver).to receive(:has_vmcx_support?).and_return(true) end it "should match XML config file" do subject.call(env) end it "should match VMCX config file" do expect(vm_dir).to receive(:each_child).and_yield(Pathname.new("config.vmcx")) subject.call(env) end end end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/is_windows_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/is_windows") describe VagrantPlugins::HyperV::Action::IsWindows do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider, config: config) } let(:config){ double("config", vm: vm) } let(:vm){ double("vm", guest: :windows) } let(:subject){ described_class.new(app, env) } before do allow(app).to receive(:call) allow(env).to receive(:[]=) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end it "should update the env with the result" do expect(env).to receive(:[]=).with(:result, true) subject.call(env) end it "should set the result to false when not windows" do expect(vm).to receive(:guest).and_return(:linux) expect(env).to receive(:[]=).with(:result, false) subject.call(env) end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/net_set_mac_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/net_set_mac") describe VagrantPlugins::HyperV::Action::NetSetMac do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider, provider_config: provider_config) } let(:provider_config){ double("provider_config", mac: mac) } let(:mac){ "ADDRESS" } let(:subject){ described_class.new(app, env) } before do allow(driver).to receive(:net_set_mac) allow(app).to receive(:call) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end it "should call the driver to set the MAC address" do expect(driver).to receive(:net_set_mac).with(mac) subject.call(env) end context "with no MAC address provided" do let(:mac){ nil } it "should not call driver to set the MAC address" do expect(driver).not_to receive(:net_set_mac) subject.call(env) end end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/net_set_vlan_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/net_set_vlan") describe VagrantPlugins::HyperV::Action::NetSetVLan do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider, provider_config: provider_config) } let(:provider_config){ double("provider_config", vlan_id: vlan_id) } let(:vlan_id){ "VID" } let(:subject){ described_class.new(app, env) } before do allow(driver).to receive(:net_set_vlan) allow(app).to receive(:call) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end it "should call the driver to set the vlan id" do expect(driver).to receive(:net_set_vlan).with(vlan_id) subject.call(env) end context "with no vlan id provided" do let(:vlan_id){ nil } it "should not call driver to set the vlan id" do expect(driver).not_to receive(:net_set_vlan) subject.call(env) end end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/read_guest_ip_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/read_guest_ip") describe VagrantPlugins::HyperV::Action::ReadGuestIP do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider) } let(:subject){ described_class.new(app, env) } before do allow(app).to receive(:call) allow(env).to receive(:[]=) allow(machine).to receive(:id) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end context "with machine ID set" do before{ allow(machine).to receive(:id).and_return("VMID") } it "should request guest IP from the driver" do expect(driver).to receive(:read_guest_ip).and_return("ip" => "ADDRESS") subject.call(env) end it "should set the host information into the env" do expect(env).to receive(:[]=).with(:machine_ssh_info, { host: "ADDRESS" }) expect(driver).to receive(:read_guest_ip).and_return("ip" => "ADDRESS") subject.call(env) end end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/read_state_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/read_state") describe VagrantPlugins::HyperV::Action::ReadState do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine, machine_state_id: state_id} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider) } let(:state_id){ nil } let(:subject){ described_class.new(app, env) } before do allow(app).to receive(:call) allow(env).to receive(:[]=) allow(machine).to receive(:id) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end it "should set machine state into the env as not created" do expect(env).to receive(:[]=).with(:machine_state_id, :not_created) subject.call(env) end context "with machine ID set" do before{ allow(machine).to receive(:id).and_return("VMID") } it "should request machine state from the driver" do expect(driver).to receive(:get_current_state).and_return("state" => "running") subject.call(env) end it "should set machine state into the env" do expect(driver).to receive(:get_current_state).and_return("state" => "running") expect(env).to receive(:[]=).with(:machine_state_id, :running) subject.call(env) end context "with machine state ID as not_created" do let(:state_id){ :not_created } it "should clear the machine ID" do expect(driver).to receive(:get_current_state).and_return("state" => "not_created") expect(machine).to receive(:id=).with(nil) subject.call(env) end end end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/set_name_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/set_name") describe VagrantPlugins::HyperV::Action::SetName do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine, root_path: Pathname.new("path")} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider, provider_config: provider_config, data_dir: data_dir, name: "machname") } let(:data_dir){ double("data_dir") } let(:provider_config){ double("provider_config", vmname: vmname) } let(:vmname){ "VMNAME" } let(:sentinel){ double("sentinel") } let(:subject){ described_class.new(app, env) } before do allow(driver).to receive(:set_name) allow(app).to receive(:call) allow(data_dir).to receive(:join).and_return(sentinel) allow(sentinel).to receive(:file?).and_return(false) allow(sentinel).to receive(:open) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end it "should call the driver to set the name" do expect(driver).to receive(:set_name) subject.call(env) end it "should use the configured name when setting" do expect(driver).to receive(:set_name).with(vmname) subject.call(env) end it "should write sentinel after name is set" do expect(sentinel).to receive(:open) subject.call(env) end context "when no name is provided in the config" do let(:vmname){ nil } it "should generate a name based on path and machine" do expect(driver).to receive(:set_name).with(/^#{env[:root_path].to_s}_#{machine.name}_.+/) subject.call(env) end it "should not set name if sentinel exists" do expect(sentinel).to receive(:file?).and_return(true) subject.call(env) end end end ================================================ FILE: test/unit/plugins/providers/hyperv/action/wait_for_ip_address_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/action/wait_for_ip_address") describe VagrantPlugins::HyperV::Action::WaitForIPAddress do let(:app){ double("app") } let(:env){ {ui: ui, machine: machine} } let(:ui){ Vagrant::UI::Silent.new } let(:provider){ double("provider", driver: driver) } let(:driver){ double("driver") } let(:machine){ double("machine", provider: provider, provider_config: provider_config) } let(:provider_config){ double("provider_config", ip_address_timeout: ip_address_timeout) } let(:ip_address_timeout){ 1 } let(:subject){ described_class.new(app, env) } before do allow(driver).to receive(:read_guest_ip).and_return("ip" => "127.0.0.1") allow(app).to receive(:call) end it "should call the app on success" do expect(app).to receive(:call) subject.call(env) end it "should set a timeout for waiting" do expect(Timeout).to receive(:timeout).with(ip_address_timeout) subject.call(env) end it "should retry until it receives a valid address" do expect(driver).to receive(:read_guest_ip).and_return("ip" => "ADDRESS") expect(driver).to receive(:read_guest_ip).and_return("ip" => "127.0.0.1") expect(subject).to receive(:sleep) subject.call(env) end end ================================================ FILE: test/unit/plugins/providers/hyperv/cap/cleanup_disks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/cap/cleanup_disks") describe VagrantPlugins::HyperV::Cap::CleanupDisks 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(:driver) { double("driver") } let(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) allow(m).to receive(:state).and_return(state) end end let(:state) do double(:state) end let(:subject) { described_class } let(:disk_meta_file) { {disk: [], floppy: [], dvd: []} } let(:defined_disks) { {} } context "#cleanup_disks" do it "returns if there's no data in meta file" do subject.cleanup_disks(machine, defined_disks, disk_meta_file) expect(subject).not_to receive(:handle_cleanup_disk) end describe "with disks to clean up" do let(:disk_meta_file) do { "disk" => [ { "UUID" => "1234", "Path" => "c:\\users\\vagrant\\storage.vhdx", "Name" => "storage" } ], "floppy" => [], "dvd" => [] } end before { allow(driver).to receive(:read_scsi_controllers).and_return([]) } it "calls the cleanup method if a disk_meta file is defined" do expect(subject).to receive(:handle_cleanup_disk). with(machine, defined_disks, disk_meta_file["disk"]). and_return(true) subject.cleanup_disks(machine, defined_disks, disk_meta_file) end context "with dvd to clean up" do let(:disk_meta_file) do { "disk" => [], "floppy" => [], "dvd" => [ { "Path" => "test.iso" } ] } it "calls the cleamup method if a disk_meta file is defined" do expect(subject).to receive(:handle_cleanup_dvd). with(machine, defined_disks, disk_meta_file["dvd"]). and_return(true) subject.cleanup_disks(machine, defined_disks, disk_meta_file) end end end end end context "handle_cleanup_dvd" do let(:disk_meta_file) do { "disk" => [], "floppy" => [], "dvd" => [] } end let(:scsi_controllers) do [ { "ControllerNumber" => 0, "Name" => "SCSI Controller", "Drives" => drives } ] end let(:drives) do [ { "DvdMediaType" => 1, "Path" => "test.iso", "ControllerLocation" => 1, "ControllerNumber" => 0, "ControllerType" => 1 } ] end let(:defined_disks) { [] } before do allow(driver).to receive(:read_scsi_controllers).and_return(scsi_controllers) end it "should not remove disk" do expect(driver).not_to receive(:detach_dvd) subject.handle_cleanup_dvd(machine, defined_disks, disk_meta_file["dvd"]) end context "when disk is defined in meta file" do let(:disk_meta_file) do { "disk" => [], "floppy" => [], "dvd" => [ "Path" => "test.iso", "ControllerLocation" => 1, "ControllerNumber" => 0, "ControllerType" => 1 ] } end it "should remove the disk" do expect(driver).to receive(:detach_dvd).with(1, 0) subject.handle_cleanup_dvd(machine, defined_disks, disk_meta_file["dvd"]) end context "when disk is defined in defined disks" do let(:defined_disks) do [ double("dvd", name: "test-dvd", type: :dvd, file: "test.iso") ] end it "should not remove disk" do expect(driver).not_to receive(:detach_dvd) subject.handle_cleanup_dvd(machine, defined_disks, disk_meta_file["dvd"]) end end end end context "#handle_cleanup_disk" do let(:disk_meta_file) do { "disk" => [ { "UUID" => "1234", "Path" => "c:\\users\\vagrant\\storage.vhdx", "Name" => "storage" } ], "floppy" => [], "dvd" => [] } end let(:defined_disks) { [] } let(:all_disks) do [ { "UUID" => "1234", "Path" => "c:\\users\\vagrant\\storage.vhdx", "Name"=>"storage", "ControllerType" => "IDE", "ControllerNumber" => 1, "ControllerLocation" => 0 } ] end let(:path) { "C:\\Users\\vagrant\\storage.vhdx" } it "removes and closes medium from guest" do expect(driver).to receive(:list_hdds).and_return(all_disks) expect(driver).to receive(:remove_disk).with("IDE", 1, 0, "c:\\users\\vagrant\\storage.vhdx").and_return(true) subject.handle_cleanup_disk(machine, defined_disks, disk_meta_file["disk"]) end it "displays a warning if the disk could not be determined" do expect(driver).to receive(:list_hdds).and_return(all_disks) expect(File).to receive(:realdirpath).and_return(path) expect(File).to receive(:realdirpath).and_return("") expect(driver).not_to receive(:remove_disk) expect(machine.ui).to receive(:warn).twice subject.handle_cleanup_disk(machine, defined_disks, disk_meta_file["disk"]) end describe "when windows paths mix cases" do let(:disk_meta_file) do { "disk" => [ { "UUID" => "1234", "Path" => "c:\\users\\vagrant\\storage.vhdx", "Name" => "storage" } ], "floppy" => [], "dvd" => [] } end let(:defined_disks) { [] } let(:all_disks) do [ { "UUID" => "1234", "Path" => "C:\\Users\\vagrant\\storage.vhdx", "Name" => "storage", "ControllerType" => "IDE", "ControllerNumber" => 1, "ControllerLocation" => 0 } ] end let(:path) { "C:\\Users\\vagrant\\storage.vhdx" } it "still removes and closes the medium from the guest" do expect(driver).to receive(:list_hdds).and_return(all_disks) expect(File).to receive(:realdirpath).twice.and_return(path) expect(driver).to receive(:remove_disk).with("IDE", 1, 0, path).and_return(true) subject.handle_cleanup_disk(machine, defined_disks, disk_meta_file["disk"]) end end end end ================================================ FILE: test/unit/plugins/providers/hyperv/cap/configure_disks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/cap/configure_disks") describe VagrantPlugins::HyperV::Cap::ConfigureDisks 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(:driver) { double("driver") } let(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) allow(m).to receive(:state).and_return(state) end end let(:state) do double(:state) end let(:defined_disks) do [ double("disk", name: "vagrant_primary", size: "5GB", primary: true, type: :disk), double("disk", name: "disk-0", size: "5GB", primary: false, type: :disk), double("disk", name: "disk-1", size: "5GB", primary: false, type: :disk), double("disk", name: "disk-2", size: "5GB", primary: false, type: :disk) ] end let(:subject) { described_class } let(:all_disks) do [ { "UUID"=>"12345", "Path"=>"C:/Users/vagrant/disks/ubuntu-18.04-amd64-disk001.vhdx", "ControllerLocation"=>0, "ControllerNumber"=>0 }, { "UUID"=>"67890", "Name"=>"disk-0", "Path"=>"C:/Users/vagrant/disks/disk-0.vhdx", "ControllerLocation"=>1, "ControllerNumber"=>0 }, { "UUID"=>"324bbb53-d5ad-45f8-9bfa-1f2468b199a8", "Path"=>"C:/Users/vagrant/disks/disk-1.vhdx", "Name"=>"disk-1", "ControllerLocation"=>2, "ControllerNumber"=>0 } ] end context "#configure_disks" do let(:dsk_data) do { "UUID"=>"1234", "Name"=>"disk", "Path"=> "C:/Users/vagrant/storage.vhdx" } end it "configures disks and returns the disks defined" do allow(driver).to receive(:list_hdds).and_return([]) expect(subject).to receive(:handle_configure_disk).exactly(4).and_return(dsk_data) subject.configure_disks(machine, defined_disks) end describe "with no disks to configure" do let(:defined_disks) { {} } it "returns empty hash if no disks to configure" do expect(subject.configure_disks(machine, defined_disks)).to eq({}) end end context "with dvd" do before do defined_disks.push( double("dvd", name: "test-dvd", type: :dvd, file: "test.iso") ) end it "should configure the dvd disk" do allow(driver).to receive(:list_hdds).and_return([]) allow(subject).to receive(:handle_configure_disk).and_return({}) expect(subject).to receive(:handle_configure_dvd).and_return({}) subject.configure_disks(machine, defined_disks) end end end context "#get_current_disk" do it "gets primary disk uuid if disk to configure is primary" do expect(driver).to receive(:get_disk).with(all_disks.first["Path"]).and_return(all_disks.first) primary_disk = subject.get_current_disk(machine, defined_disks.first, all_disks) expect(primary_disk).to eq(all_disks.first) end it "finds the disk to configure" do disk = subject.get_current_disk(machine, defined_disks[1], all_disks) expect(disk).to eq(all_disks[1]) end it "returns nil if disk is not found" do disk = subject.get_current_disk(machine, defined_disks[3], all_disks) expect(disk).to be_nil end context "when primary disk is not located at 0 0" do let(:all_disks) do [ { "UUID"=>"12345", "Path"=>"C:/Users/vagrant/disks/ubuntu-18.04-amd64-disk001.vhdx", "ControllerLocation"=>1, "ControllerNumber"=>0 }, { "UUID"=>"67890", "Name"=>"disk-0", "Path"=>"C:/Users/vagrant/disks/disk-0.vhdx", "ControllerLocation"=>2, "ControllerNumber"=>0 }, { "UUID"=>"324bbb53-d5ad-45f8-9bfa-1f2468b199a8", "Path"=>"C:/Users/vagrant/disks/disk-1.vhdx", "Name"=>"disk-1", "ControllerLocation"=>3, "ControllerNumber"=>0 } ] end it "should return the primary disk" do expect(driver).to receive(:get_disk).with(all_disks.first["Path"]).and_return(all_disks.first) primary_disk = subject.get_current_disk(machine, defined_disks.first, all_disks) expect(primary_disk).to eq(all_disks.first) end context "when disks are unsorted" do let(:all_disks) do [ { "UUID"=>"67890", "Name"=>"disk-0", "Path"=>"C:/Users/vagrant/disks/disk-0.vhdx", "ControllerLocation"=>2, "ControllerNumber"=>0 }, { "UUID"=>"12345", "Path"=>"C:/Users/vagrant/disks/ubuntu-18.04-amd64-disk001.vhdx", "ControllerLocation"=>1, "ControllerNumber"=>0 }, { "UUID"=>"324bbb53-d5ad-45f8-9bfa-1f2468b199a8", "Path"=>"C:/Users/vagrant/disks/disk-1.vhdx", "Name"=>"disk-1", "ControllerLocation"=>3, "ControllerNumber"=>0 } ] end it "should return the primary disk" do expect(driver).to receive(:get_disk).with(all_disks[1]["Path"]).and_return(all_disks[1]) primary_disk = subject.get_current_disk(machine, defined_disks.first, all_disks) expect(primary_disk).to eq(all_disks[1]) end end end end context "#handle_configure_dvd" do let(:scsi_controllers_current) do [ { "ControllerNumber" => 0, "Name" => "SCSI Controller", "Drives" => drives_current } ] end let(:drives_current) { [] } let(:scsi_controllers_updated) do [ { "ControllerNumber" => 0, "Name" => "SCSI Controller", "Drives" => drives_updated } ] end let(:drives_updated) do [ { "DvdMediaType" => 1, "Path" => "test.iso", "ControllerLocation" => 1, "ControllerNumber" => 0, "ControllerType" => 1 } ] end let(:defined_disk) do double("dvd", name: "test-dvd", type: :dvd, file: "test.iso") end it "should add disk to guest" do expect(driver).to receive(:read_scsi_controllers).and_return(scsi_controllers_current) expect(driver).to receive(:read_scsi_controllers).and_return(scsi_controllers_updated) expect(driver).to receive(:attach_dvd).with(/test.iso$/) subject.handle_configure_dvd(machine, defined_disk) end context "when disk is already attached" do let(:drives_current) do [ { "DvdMediaType" => 1, "Path" => "test.iso", "ControllerLocation" => 1, "ControllerNumber" => 0, "ControllerType" => 1 } ] end it "should not add disk to guest" do expect(driver).to receive(:read_scsi_controllers).and_return(scsi_controllers_current) expect(driver).not_to receive(:attach_dvd) subject.handle_configure_dvd(machine, defined_disk) end context "when additional disk is defined" do let(:defined_disk) do double("dvd", name: "other-dvd", type: :dvd, file: "other-test.iso") end let(:drives_updated) do [ { "DvdMediaType" => 1, "Path" => "test.iso", "ControllerLocation" => 1, "ControllerNumber" => 0, "ControllerType" => 1 }, { "DvdMediaType" => 1, "Path" => "other-test.iso", "ControllerLocation" => 2, "ControllerNumber" => 0, "ControllerType" => 1 } ] end it "should add disk to guest" do expect(driver).to receive(:read_scsi_controllers).and_return(scsi_controllers_current) expect(driver).to receive(:read_scsi_controllers).and_return(scsi_controllers_updated) expect(driver).to receive(:attach_dvd).with(/other-test.iso$/) subject.handle_configure_dvd(machine, defined_disk) end end end end context "#handle_configure_disk" do describe "when creating a new disk" do let(:all_disks) do [ { "UUID"=>"12345", "Path"=>"C:/Users/vagrant/disks/ubuntu-18.04-amd64-disk001.vhdx", "ControllerLocation"=>0, "ControllerNumber"=>0 } ] end let(:disk_meta) do { "UUID" => "12345", "Name" => "vagrant_primary", "Path" => "C:/Users/vagrant/disks/ubuntu-18.04-amd64-disk001.vhdx" } end it "creates a new disk if it doesn't yet exist" do expect(subject).to receive(:create_disk).with(machine, defined_disks[1]) .and_return(disk_meta) subject.handle_configure_disk(machine, defined_disks[1], all_disks) end end describe "when a disk needs to be resized" do let(:all_disks) do [ {"UUID"=>"12345", "Path"=>"C:/Users/vagrant/disks/ubuntu-18.04-amd64-disk001.vhdx", "ControllerLocation"=>0, "ControllerNumber"=>0 }, { "UUID"=>"67890", "Name"=>"disk-0", "Path"=>"C:/Users/vagrant/disks/disk-0.vhdx", "ControllerLocation"=>1, "ControllerNumber"=>0 }, { "UUID"=>"324bbb53-d5ad-45f8-9bfa-1f2468b199a8", "Path"=>"C:/Users/vagrant/disks/disk-1.vhdx", "Name"=>"disk-1", "ControllerLocation"=>2, "ControllerNumber"=>0 } ] end it "resizes a disk" do expect(subject).to receive(:get_current_disk). with(machine, defined_disks[1], all_disks).and_return(all_disks[1]) expect(subject).to receive(:compare_disk_size). with(machine, defined_disks[1], all_disks[1]).and_return(true) expect(subject).to receive(:resize_disk). with(machine, defined_disks[1], all_disks[1]).and_return(true) subject.handle_configure_disk(machine, defined_disks[1], all_disks) end end describe "if no additional disk configuration is required" do let(:all_disks) do [ { "UUID"=>"12345", "Path"=>"C:/Users/vagrant/disks/ubuntu-18.04-amd64-disk001.vhdx", "ControllerLocation"=>0, "ControllerNumber"=>0 }, { "UUID"=>"67890", "Name"=>"disk-0", "Path"=>"C:/Users/vagrant/disks/disk-0.vhdx", "ControllerLocation"=>1, "ControllerNumber"=>0 }, { "UUID"=>"324bbb53-d5ad-45f8-9bfa-1f2468b199a8", "Path"=>"C:/Users/vagrant/disks/disk-1.vhdx", "Name"=>"disk-1", "ControllerLocation"=>2, "ControllerNumber"=>0 } ] end it "does nothing if all disks are properly configured" do expect(subject).to receive(:get_current_disk). with(machine, defined_disks[1], all_disks).and_return(all_disks[1]) expect(subject).to receive(:compare_disk_size). with(machine, defined_disks[1], all_disks[1]).and_return(false) subject.handle_configure_disk(machine, defined_disks[1], all_disks) end end end context "#compare_disk_size" do let(:disk_config_small) do double("disk", name: "disk-0", size: 41824.0, primary: false, type: :disk ) end let(:disk_config_large) do double("disk", name: "disk-0", size: 123568719476736.0, primary: false, type: :disk ) end let(:disk_large) do [ { "UUID" => "12345", "Path" => "C:/Users/vagrant/disks/ubuntu-18.04-amd64-disk001.vhdx", "ControllerLocation" => 0, "ControllerNumber" => 0 } ] end let(:disk_small) do { "UUID" => "67890", "Path" => "C:/Users/vagrant/disks/small_disk.vhd", "Size" => 1073741824.0, "ControllerLocation" => 1, "ControllerNumber" => 0 } end it "shows a warning if user attempts to shrink size of a vhd disk" do expect(machine.ui).to receive(:warn) expect(driver).to receive(:get_disk).with(all_disks[1]["Path"]).and_return(disk_small) expect(subject.compare_disk_size(machine, disk_config_small, all_disks[1])).to be_falsey end it "returns true if requested size is bigger than current size" do expect(driver).to receive(:get_disk).with(all_disks[2]["Path"]).and_return(disk_small) expect(subject.compare_disk_size(machine, disk_config_large, all_disks[2])).to be_truthy end end context "#create_disk" do let(:disk_provider_config) { {} } let(:disk_config) do double("disk", name: "disk-0", size: 1073741824.0, primary: false, type: :disk, disk_ext: "vhdx", provider_config: disk_provider_config, file: nil ) end let(:disk_file) { "C:/Users/vagrant/disks/Virtual Hard Disks/disk-0.vhdx" } let(:data_dir) { Pathname.new("C:/Users/vagrant/disks") } let(:disk) do { "DiskIdentifier" => "12345", "Path" => "C:/Users/vagrant/disks/Virtual Hard Disks/disk-0.vhdx", "ControllerLocation" => 1, "ControllerNumber" => 0 } end it "creates a disk and attaches it to a guest" do expect(machine).to receive(:data_dir).and_return(data_dir) expect(driver).to receive(:create_disk).with(disk_file, disk_config.size) expect(driver).to receive(:get_disk).with(disk_file).and_return(disk) expect(driver).to receive(:attach_disk).with(disk_file) subject.create_disk(machine, disk_config) end end context "#convert_size_vars!" do let(:disk_provider_config) do { BlockSizeBytes: "128MB", LogicalSectorSizeBytes: 512, PhysicalSectorSizeBytes: 4096 } end it "converts certain powershell arguments into something usable" do updated_config = subject.convert_size_vars!(disk_provider_config) expect(updated_config[:BlockSizeBytes]).to eq(134217728) expect(updated_config[:LogicalSectorSizeBytes]).to eq(512) expect(updated_config[:PhysicalSectorSizeBytes]).to eq(4096) end end context "#resize_disk" do let(:disk_config) do double("disk", name: "disk-0", size: 1073741824.0, primary: false, type: :disk, disk_ext: "vhdx", provider_config: nil, file: nil ) end let(:disk) do { "DiskIdentifier" => "12345", "Path" => "C:/Users/vagrant/disks/disk-0.vhdx", "ControllerLocation" => 1, "ControllerNumber" => 0 } end let(:disk_file) { "C:/Users/vagrant/disks/disk-0.vhdx" } it "resizes the disk" do expect(driver).to receive(:get_disk).with(disk_file).and_return(disk) expect(driver).to receive(:resize_disk).with(disk_file, disk_config.size.to_i).and_return(true) subject.resize_disk(machine, disk_config, all_disks[1]) end end end val =<<-EOF { "ControllerNumber": 0, "IsTemplate": false, "Drives": [ { "Path": "C:\\Users\\vagrant\\project\\.vagrant\\machines\\default\\hyperv\\Virtual Hard Disks\\ubuntu-18.04-amd64.vhdx", "DiskNumber": null, "MaximumIOPS": 0, "MinimumIOPS": 0, "QoSPolicyID": "00000000-0000-0000-0000-000000000000", "SupportPersistentReservations": false, "WriteHardeningMethod": 0, "ControllerLocation": 0, "ControllerNumber": 0, "ControllerType": 1, "Name": "Hard Drive on SCSI controller number 0 at location 0", "PoolName": "Primordial", "Id": "Microsoft:6F225311-B793-49CF-98A3-0A32108E49BB\\6AEC67E1-3135-401C-BB23-9FE1C4E34560\\0\\0\\D", "VMId": "6f225311-b793-49cf-98a3-0a32108e49bb", "VMName": "project_default_1744059721263_2993", "VMSnapshotId": "00000000-0000-0000-0000-000000000000", "VMSnapshotName": "", "CimSession": { "ComputerName": null, "InstanceId": "899e8c1f-5c4f-4ba4-86a4-f72dc887885f" }, "ComputerName": "DESKTOP-GICAJ17", "IsDeleted": false }, { "DvdMediaType": 1, "Path": "C:\\Users\\Vagrant\\deb2.iso", "ControllerLocation": 2, "ControllerNumber": 0, "ControllerType": 1, "Name": "DVD Drive on SCSI controller number 0 at location 2", "PoolName": "Primordial", "Id": "Microsoft:6F225311-B793-49CF-98A3-0A32108E49BB\\6AEC67E1-3135-401C-BB23-9FE1C4E34560\\0\\2\\D", "VMId": "6f225311-b793-49cf-98a3-0a32108e49bb", "VMName": "project_default_1744059721263_2993", "VMSnapshotId": "00000000-0000-0000-0000-000000000000", "VMSnapshotName": "", "CimSession": { "ComputerName": null, "InstanceId": "899e8c1f-5c4f-4ba4-86a4-f72dc887885f" }, "ComputerName": "DESKTOP-GICAJ17", "IsDeleted": false } ], "Name": "SCSI Controller", "Id": "Microsoft:6F225311-B793-49CF-98A3-0A32108E49BB\\6AEC67E1-3135-401C-BB23-9FE1C4E34560\\0", "VMId": "6f225311-b793-49cf-98a3-0a32108e49bb", "VMName": "project_default_1744059721263_2993", "VMSnapshotId": "00000000-0000-0000-0000-000000000000", "VMSnapshotName": "", "CimSession": { "ComputerName": null, "InstanceId": "899e8c1f-5c4f-4ba4-86a4-f72dc887885f" }, "ComputerName": "DESKTOP-GICAJ17", "IsDeleted": false, "VMCheckpointId": "00000000-0000-0000-0000-000000000000", "VMCheckpointName": "" } EOF ================================================ FILE: test/unit/plugins/providers/hyperv/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/config") describe VagrantPlugins::HyperV::Config do let(:machine){ double("machine", ui: ui) } let(:ui){ Vagrant::UI::Silent.new } describe "#ip_address_timeout" do it "can be set" do subject.ip_address_timeout = 180 subject.finalize! expect(subject.ip_address_timeout).to eq(180) end it "defaults to a number" do subject.finalize! expect(subject.ip_address_timeout).to eq(120) end end describe "#vlan_id" do it "can be set" do subject.vlan_id = 100 subject.finalize! expect(subject.vlan_id).to eq(100) end end describe "#mac" do it "can be set" do subject.mac = "001122334455" subject.finalize! expect(subject.mac).to eq("001122334455") end end describe "#vmname" do it "can be set" do subject.vmname = "test" subject.finalize! expect(subject.vmname).to eq("test") end end describe "#memory" do it "can be set" do subject.memory = 512 subject.finalize! expect(subject.memory).to eq(512) end end describe "#maxmemory" do it "can be set" do subject.maxmemory = 1024 subject.finalize! expect(subject.maxmemory).to eq(1024) end end describe "#cpus" do it "can be set" do subject.cpus = 2 subject.finalize! expect(subject.cpus).to eq(2) end end describe "#vmname" do it "can be set" do subject.vmname = "custom" subject.finalize! expect(subject.vmname).to eq("custom") end end describe "#differencing_disk" do it "is false by default" do subject.finalize! expect(subject.differencing_disk).to eq(false) end it "can be set" do subject.differencing_disk = true subject.finalize! expect(subject.differencing_disk).to eq(true) end it "should set linked_clone" do subject.differencing_disk = true subject.finalize! expect(subject.differencing_disk).to eq(true) expect(subject.linked_clone).to eq(true) end it "should provide a deprecation warning when set" do expect(ui).to receive(:warn) subject.differencing_disk = true subject.finalize! subject.validate(machine) end end describe "#linked_clone" do it "is false by default" do subject.finalize! expect(subject.linked_clone).to eq(false) end it "can be set" do subject.linked_clone = true subject.finalize! expect(subject.linked_clone).to eq(true) end it "should set differencing_disk" do subject.linked_clone = true subject.finalize! expect(subject.linked_clone).to eq(true) expect(subject.differencing_disk).to eq(true) end end describe "#auto_start_action" do it "should be Nothing by default" do subject.finalize! expect(subject.auto_start_action).to eq("Nothing") end it "can be set" do subject.auto_start_action = "Start" subject.finalize! expect(subject.auto_start_action).to eq("Start") end it "does not accept invalid values" do subject.auto_start_action = "Invalid" subject.finalize! result = subject.validate(machine) expect(result["Hyper-V"]).not_to be_empty end end describe "#auto_stop_action" do it "should be ShutDown by default" do subject.finalize! expect(subject.auto_stop_action).to eq("ShutDown") end it "can be set" do subject.auto_stop_action = "Save" subject.finalize! expect(subject.auto_stop_action).to eq("Save") end it "does not accept invalid values" do subject.auto_stop_action = "Invalid" subject.finalize! result = subject.validate(machine) expect(result["Hyper-V"]).not_to be_empty end end describe "#enable_checkpoints" do it "is true by default" do subject.finalize! expect(subject.enable_checkpoints).to eq(true) end it "can be set" do subject.enable_checkpoints = false subject.finalize! expect(subject.enable_checkpoints).to eq(false) end it "is enabled automatically when enable_automatic_checkpoints is enabled" do subject.enable_checkpoints = false subject.enable_automatic_checkpoints = true subject.finalize! expect(subject.enable_checkpoints).to eq(true) end end describe "#enable_automatic_checkpoints" do it "is false by default" do subject.finalize! expect(subject.enable_automatic_checkpoints).to eq(false) end it "can be set" do subject.enable_checkpoints = true subject.finalize! expect(subject.enable_checkpoints).to eq(true) end end describe "#enable_virtualization_extensions" do it "is false by default" do subject.finalize! expect(subject.enable_virtualization_extensions).to eq(false) end it "can be set" do subject.enable_virtualization_extensions = true subject.finalize! expect(subject.enable_virtualization_extensions).to eq(true) end end describe "#vm_integration_services" do it "is empty by default" do subject.finalize! expect(subject.vm_integration_services).to be_empty end it "accepts new entries" do subject.vm_integration_services["entry"] = "value" subject.finalize! expect(subject.vm_integration_services["entry"]).to eq("value") end it "does not accept non-Hash types" do subject.vm_integration_services = "value" subject.finalize! result = subject.validate(machine) expect(result["Hyper-V"]).not_to be_empty end it "accepts boolean values within Hash" do subject.vm_integration_services["custom"] = true subject.finalize! result = subject.validate(machine) expect(result["Hyper-V"]).to be_empty end it "does not accept non-boolean values within Hash" do subject.vm_integration_services["custom"] = "value" subject.finalize! result = subject.validate(machine) expect(result["Hyper-V"]).not_to be_empty end end describe "#enable_enhanced_session_mode" do it "is false by default" do subject.finalize! expect(subject.enable_enhanced_session_mode).to eq(false) end it "can be set" do subject.enable_enhanced_session_mode = true subject.finalize! expect(subject.enable_enhanced_session_mode).to eq(true) end end end ================================================ FILE: test/unit/plugins/providers/hyperv/driver_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/driver") describe VagrantPlugins::HyperV::Driver do def generate_result(obj) "===Begin-Output===\n" + JSON.dump(obj) + "\n===End-Output===" end def generate_error(msg) "===Begin-Error===\n#{JSON.dump(error: msg)}\n===End-Error===\n" end let(:result){ Vagrant::Util::Subprocess::Result.new( result_exit, result_stdout, result_stderr) } let(:subject){ described_class.new(vm_id) } let(:vm_id){ 1 } let(:result_stdout){ "" } let(:result_stderr){ "" } let(:result_exit){ 0 } context "public methods" do before{ allow(subject).to receive(:execute_powershell).and_return(result) } describe "#execute" do it "should convert symbol into path string" do expect(subject).to receive(:execute_powershell).with(kind_of(String), any_args) .and_return(result) subject.execute(:thing) end it "should append extension when converting symbol" do expect(subject).to receive(:execute_powershell).with("thing.ps1", any_args) .and_return(result) subject.execute(:thing) end context "when command returns non-zero exit code" do let(:result_exit){ 1 } it "should raise an error" do expect{ subject.execute(:thing) }.to raise_error(VagrantPlugins::HyperV::Errors::PowerShellError) end end context "when command stdout matches error pattern" do let(:result_stdout){ generate_error("Error Message") } it "should raise an error" do expect{ subject.execute(:thing) }.to raise_error(VagrantPlugins::HyperV::Errors::PowerShellError) end end context "with valid JSON output" do let(:result_stdout){ generate_result(:custom => "value") } it "should return parsed JSON data" do expect(subject.execute(:thing)).to eq("custom" => "value") end end context "with invalid JSON output" do let(:result_stdout){ "value" } it "should return nil" do expect(subject.execute(:thing)).to be_nil end end end describe "#has_vmcx_support?" do context "when support is available" do let(:result_stdout){ generate_result(:result => true) } it "should be true" do expect(subject.has_vmcx_support?).to eq(true) end end context "when support is not available" do let(:result_stdout){ generate_result(:result => false) } it "should be false" do expect(subject.has_vmcx_support?).to eq(false) end end end describe "#set_vm_integration_services" do it "should map known integration services names automatically" do expect(subject).to receive(:execute) do |name, args| expect(args[:Id]).to eq(VagrantPlugins::HyperV::Driver::INTEGRATION_SERVICES_MAP[:shutdown]) end subject.set_vm_integration_services(shutdown: true) end it "should set enable when value is true" do expect(subject).to receive(:execute) do |name, args| expect(args[:Enable]).to eq(true) end subject.set_vm_integration_services(shutdown: true) end it "should not set enable when value is false" do expect(subject).to receive(:execute) do |name, args| expect(args[:Enable]).to be_nil end subject.set_vm_integration_services(shutdown: false) end it "should pass unknown key names directly through" do expect(subject).to receive(:execute) do |name, args| expect(args[:Id]).to eq("CustomKey") end subject.set_vm_integration_services(CustomKey: true) end end end describe "#execute_powershell" do before{ allow(Vagrant::Util::PowerShell).to receive(:execute) } it "should call the PowerShell module to execute" do expect(Vagrant::Util::PowerShell).to receive(:execute) subject.send(:execute_powershell, "path", {}) end it "should modify the path separators" do expect(Vagrant::Util::PowerShell).to receive(:execute) .with("\\path\\to\\script.ps1", any_args) subject.send(:execute_powershell, "/path/to/script.ps1", {}) end it "should include ErrorAction option as Stop" do expect(Vagrant::Util::PowerShell).to receive(:execute) do |path, *args| expect(args).to include("-ErrorAction") expect(args).to include("Stop") end subject.send(:execute_powershell, "path", {}) end it "should automatically include module path" do expect(Vagrant::Util::PowerShell).to receive(:execute) do |path, *args| opts = args.detect{|i| i.is_a?(Hash)} expect(opts[:module_path]).not_to be_nil end subject.send(:execute_powershell, "path", {}) end it "should covert hash options into arguments" do expect(Vagrant::Util::PowerShell).to receive(:execute) do |path, *args| expect(args).to include("-Custom") expect(args).to include("'Value'") end subject.send(:execute_powershell, "path", "Custom" => "Value") end it "should treat keys with `true` value as switches" do expect(Vagrant::Util::PowerShell).to receive(:execute) do |path, *args| expect(args).to include("-Custom") expect(args).not_to include("'true'") end subject.send(:execute_powershell, "path", "Custom" => true) end it "should not include keys with `false` value" do expect(Vagrant::Util::PowerShell).to receive(:execute) do |path, *args| expect(args).not_to include("-Custom") end subject.send(:execute_powershell, "path", "Custom" => false) end end end ================================================ FILE: test/unit/plugins/providers/hyperv/provider_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/provider") describe VagrantPlugins::HyperV::Provider do let(:driver){ double("driver") } let(:provider){ double("provider", driver: driver) } let(:provider_config){ double("provider_config", ip_address_timeout: ip_address_timeout) } let(:ip_address_timeout){ 1 } let(:machine){ double("machine", provider: provider, provider_config: provider_config) } let(:platform) { double("platform") } let(:powershell) { double("powershell") } subject { described_class.new(machine) } before do stub_const("Vagrant::Util::Platform", platform) stub_const("Vagrant::Util::PowerShell", powershell) allow(machine).to receive(:id).and_return("foo") allow(platform).to receive(:windows?).and_return(true) allow(platform).to receive(:wsl?).and_return(false) allow(platform).to receive(:windows_admin?).and_return(true) allow(platform).to receive(:windows_hyperv_admin?).and_return(true) allow(powershell).to receive(:available?).and_return(true) end describe ".usable?" do subject { described_class } it "returns false if not windows" do allow(platform).to receive(:windows?).and_return(false) expect(subject).to_not be_usable end it "returns true if within WSL" do expect(platform).to receive(:windows?).and_return(false) expect(platform).to receive(:wsl?).and_return(true) expect(subject).to be_usable end it "returns false if neither an admin nor a hyper-v admin" do allow(platform).to receive(:windows_admin?).and_return(false) allow(platform).to receive(:windows_hyperv_admin?).and_return(false) expect(subject).to_not be_usable end it "returns true if not an admin but is a hyper-v admin" do allow(platform).to receive(:windows_admin?).and_return(false) allow(platform).to receive(:windows_hyperv_admin?).and_return(true) expect(subject).to be_usable end it "returns false if powershell is not available" do allow(powershell).to receive(:available?).and_return(false) expect(subject).to_not be_usable end it "raises an exception if not windows" do allow(platform).to receive(:windows?).and_return(false) expect { subject.usable?(true) }. to raise_error(VagrantPlugins::HyperV::Errors::WindowsRequired) end it "raises an exception if neither an admin nor a hyper-v admin" do allow(platform).to receive(:windows_admin?).and_return(false) allow(platform).to receive(:windows_hyperv_admin?).and_return(false) expect { subject.usable?(true) }. to raise_error(VagrantPlugins::HyperV::Errors::AdminRequired) end it "raises an exception if neither an admin nor a hyper-v admin" do allow(platform).to receive(:windows_admin?).and_return(false) allow(platform).to receive(:windows_hyperv_admin?).and_return(false) expect { subject.usable?(true) }. to raise_error(VagrantPlugins::HyperV::Errors::AdminRequired) end it "raises an exception if powershell is not available" do allow(powershell).to receive(:available?).and_return(false) expect { subject.usable?(true) }. to raise_error(VagrantPlugins::HyperV::Errors::PowerShellRequired) end end describe "#driver" do it "is initialized" do expect(subject.driver).to be_kind_of(VagrantPlugins::HyperV::Driver) end end describe "#state" do it "returns not_created if no ID" do allow(machine).to receive(:id).and_return(nil) expect(subject.state.id).to eq(:not_created) end it "calls an action to determine the ID" do allow(machine).to receive(:id).and_return("foo") expect(machine).to receive(:action).with(:read_state). and_return({ machine_state_id: :bar }) expect(subject.state.id).to eq(:bar) end end describe "#ssh_info" do let(:result) { "127.0.0.1" } let(:exit_code) { 0 } let(:ssh_info) {{:host=>result,:port=>22}} before do allow(VagrantPlugins::HyperV::Driver).to receive(:new).and_return(driver) allow(machine).to receive(:action).with(:read_state).and_return(machine_state_id: :running) end it "returns nil if a PowerShellError is returned from the driver" do allow(driver).to receive(:read_guest_ip) .and_raise(VagrantPlugins::HyperV::Errors::PowerShellError, script: anything, stderr: anything) expect(subject.ssh_info).to eq(nil) end it "should receive a valid address" do allow(driver).to receive(:execute).with(:get_network_config).and_return(result) allow(driver).to receive(:read_guest_ip).and_return({"ip" => "127.0.0.1"}) expect(subject.ssh_info).to eq(ssh_info) end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/action/clean_machine_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../base' describe VagrantPlugins::ProviderVirtualBox::Action::CleanMachineFolder do let(:app) { double("app") } let(:driver) { double("driver") } let(:machine) { double("machine", provider: double("provider", driver: driver), name: "") } let(:env) { { machine: machine } } let(:subject) { described_class.new(app, env) } before do allow(driver).to receive(:read_machine_folder) end context "machine folder is not accessible" do before do allow(subject).to receive(:clean_machine_folder).and_raise(Errno::EPERM) end it "raises an error" do expect { subject.call(env) }.to raise_error(Vagrant::Errors::MachineFolderNotAccessible) end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/action/import_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative '../base' describe VagrantPlugins::ProviderVirtualBox::Action::Import do let(:app) { double("app") } let(:state) { :test_state } let(:machine) { double("machine", state: double("state", id: state)) } let(:action_runner) { double("action_runner") } let(:env) { { machine: machine, action_runner: action_runner, destroy_on_error: destroy_on_error, } } subject { described_class.new(app, {}) } describe "#recover" do context "when destroy_on_error is false" do let(:destroy_on_error) { false } it "does nothing" do expect(action_runner).to_not receive(:run) subject.recover(env) end end context "when destroy_on_error is true" do let(:destroy_on_error) { true } context "and machine is not_created" do let(:state) { Vagrant::MachineState::NOT_CREATED_ID } it "does nothing" do expect(action_runner).to_not receive(:run) subject.recover(env) end end context "and machine is created" do let(:state) { :running } it "runs the destroy action with the proper environment" do destroy_stack = double("destroy_stack") allow(VagrantPlugins::ProviderVirtualBox::Action).to receive(:action_destroy) { destroy_stack } expect(action_runner).to receive(:run).with(destroy_stack, hash_including( config_validate: false, force_confirm_destroy: true, raw_action_name: :destroy, action_name: :machine_action_destroy, )) subject.recover(env) end context "but a VagrantError was raised" do before { env["vagrant.error"] = Vagrant::Errors::VagrantError.new } it "does nothing" do expect(action_runner).to_not receive(:run) subject.recover(env) end end end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/action/match_mac_address_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Action::MatchMACAddress do let(:ui) { Vagrant::UI::Silent.new } let(:machine) { double("machine", config: config, provider: double("provider", driver: driver)) } let(:driver) { double("driver") } let(:env) { {machine: machine, ui: ui} } let(:app) { double("app") } let(:config) { double("config", vm: vm) } let(:vm) { double("vm", clone: clone, base_mac: base_mac) } let(:clone) { false } let(:base_mac) { "00:00:00:00:00:00" } let(:subject) { described_class.new(app, env) } before do allow(app).to receive(:call) end after { subject.call(env) } it "should set the mac address" do expect(driver).to receive(:set_mac_address).with(base_mac) end context "when clone is true" do let(:clone) { true } it "should not set mac address" do expect(driver).not_to receive(:set_mac_address) end end context "when base_mac is falsey" do let(:base_mac) { nil } it "should set mac address" do expect(driver).to receive(:set_mac_address).with(base_mac) end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/action/network_fix_ipv6_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require 'socket' describe VagrantPlugins::ProviderVirtualBox::Action::NetworkFixIPv6 do include_context "unit" include_context "virtualbox" let(:iso_env) do env = isolated_environment env.vagrantfile("") env.create_vagrant_env end let(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) end end let(:env) {{ machine: machine }} let(:app) { lambda { |*args| }} let(:driver) { double("driver") } subject { described_class.new(app, env) } it "ignores nil IP addresses" do allow(machine.config.vm).to receive(:networks) .and_return(private_network: { ip: nil }) expect { subject.call(env) }.to_not raise_error end it "blank nil IP addresses" do allow(machine.config.vm).to receive(:networks) .and_return(private_network: { ip: "" }) expect { subject.call(env) }.to_not raise_error end context "with IPv6 interfaces" do let(:socket) { double("socket") } before do # This address is only used to trigger the fixup code. It doesn't matter # what it is. allow(machine.config.vm).to receive(:networks) .and_return(private_network: { ip: 'fe:80::' }) allow(UDPSocket).to receive(:new).with(Socket::AF_INET6) .and_return(socket) allow(socket).to receive(:connect) end it "only checks the interfaces associated with the VM" do all_networks = [{name: "vboxnet0", ipv6: "dead:beef::", ipv6_prefix: 64, status: 'Up' }, {name: "vboxnet1", ipv6: "badd:badd::", ipv6_prefix: 64, status: 'Up' } ] ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} } allow(machine.provider.driver).to receive(:read_network_interfaces) .and_return(ifaces) allow(machine.provider.driver).to receive(:read_host_only_interfaces) .and_return(all_networks) subject.call(env) expect(socket).to have_received(:connect) .with(all_networks[0][:ipv6] + (['ffff']*4).join(':'), 80) end it "correctly uses the netmask to figure out the probe address" do all_networks = [{name: "vboxnet0", ipv6: "dead:beef::", ipv6_prefix: 113, status: 'Up' } ] ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} } allow(machine.provider.driver).to receive(:read_network_interfaces) .and_return(ifaces) allow(machine.provider.driver).to receive(:read_host_only_interfaces) .and_return(all_networks) subject.call(env) expect(socket).to have_received(:connect) .with(all_networks[0][:ipv6] + '7fff', 80) end it "should ignore interfaces that are down" do all_networks = [{name: "vboxnet0", ipv6: "dead:beef::", ipv6_prefix: 64, status: 'Down' } ] ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} } allow(machine.provider.driver).to receive(:read_network_interfaces) .and_return(ifaces) allow(machine.provider.driver).to receive(:read_host_only_interfaces) .and_return(all_networks) subject.call(env) expect(socket).to_not have_received(:connect) end it "should ignore interfaces without an IPv6 address" do all_networks = [{name: "vboxnet0", ipv6: "", ipv6_prefix: 0, status: 'Up' } ] ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} } allow(machine.provider.driver).to receive(:read_network_interfaces) .and_return(ifaces) allow(machine.provider.driver).to receive(:read_host_only_interfaces) .and_return(all_networks) subject.call(env) expect(socket).to_not have_received(:connect) end it "should ignore interfaces with link-local IPv6 address" do all_networks = [{name: "vboxnet0", ipv6: "fe80::ffff:ffff:ffff:ffff", ipv6_prefix: 64, status: 'Up' } ] ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} } allow(machine.provider.driver).to receive(:read_network_interfaces) .and_return(ifaces) allow(machine.provider.driver).to receive(:read_host_only_interfaces) .and_return(all_networks) subject.call(env) expect(socket).to_not have_received(:connect) end it "should ignore nat interfaces" do all_networks = [{name: "vboxnet0", ipv6: "", ipv6_prefix: 0, status: 'Up' } ] ifaces = { 1 => {type: :nat} } allow(machine.provider.driver).to receive(:read_network_interfaces) .and_return(ifaces) allow(machine.provider.driver).to receive(:read_host_only_interfaces) .and_return(all_networks) subject.call(env) expect(socket).to_not have_received(:connect) end it "should reconfigure an interface if unreachable" do all_networks = [{name: "vboxnet0", ipv6: "dead:beef::", ipv6_prefix: 64, status: 'Up' } ] ifaces = { 1 => {type: :hostonly, hostonly: "vboxnet0"} } allow(machine.provider.driver).to receive(:read_network_interfaces) .and_return(ifaces) allow(machine.provider.driver).to receive(:read_host_only_interfaces) .and_return(all_networks) allow(socket).to receive(:connect) .with(all_networks[0][:ipv6] + (['ffff']*4).join(':'), 80) .and_raise Errno::EHOSTUNREACH allow(machine.provider.driver).to receive(:reconfig_host_only) subject.call(env) expect(machine.provider.driver).to have_received(:reconfig_host_only) .with(all_networks[0]) end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/action/network_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require "vagrant/util/platform" describe VagrantPlugins::ProviderVirtualBox::Action::Network do include_context "unit" include_context "virtualbox" 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(:machine) do iso_env.machine(iso_env.machine_names[0], :virtualbox).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) end end let(:env) {{ machine: machine, ui: machine.ui }} let(:app) { lambda { |*args| }} let(:driver) { double("driver", version: vbox_version) } let(:vbox_version) { "6.1.0" } let(:nics) { {} } subject { described_class.new(app, env) } before do allow(driver).to receive(:enable_adapters) allow(driver).to receive(:read_network_interfaces) { nics } end describe "#hostonly_config" do before do allow(subject).to receive(:hostonly_find_matching_network) allow(driver).to receive(:read_bridged_interfaces).and_return([]) subject.instance_eval do def env=(e) @env = e end end subject.env = env end let(:options) { { type: type, ip: address, } } let(:type) { :dhcp } let(:address) { nil } it "should validate the IP" do expect(subject).to receive(:validate_hostonly_ip!) subject.hostonly_config(options) end context "when address is ipv6" do let(:address) { "::1" } context "when type is static6" do let(:type) { :static6 } it "should have a static6 type" do result = subject.hostonly_config(options) expect(result[:type]).to eq(:static6) end end context "when type is static" do let(:type) { :static } it "should have static6 type" do result = subject.hostonly_config(options) expect(result[:type]).to eq(:static6) end end end context "when name is provided as interface name" do let(:options) { { type: type, ip: address, name: name } } let(:name) { "hostonly_ifname" } let(:display_name) { "HostInterfaceNetworking-hostonly_ifname" } let(:hostonly_networks) do [ { name: name, display_name: display_name } ] end before { allow(driver).to receive(:read_host_only_interfaces).and_return(hostonly_networks) } it "should lookup host only networks" do expect(driver).to receive(:read_host_only_interfaces).and_return(hostonly_networks) subject.hostonly_config(options) end it "should not change the name" do expect(subject.hostonly_config(options)[:name]) == name end context "when display name is provided in options" do let(:options) { { type: type, ip: address, name: display_name } } it "should change the name to the interface name" do expect(subject.hostonly_config(options)[:name]) == name end end end end describe "#validate_hostonly_ip!" do let(:address) { "192.168.1.2" } let(:net_conf) { [IPAddr.new(address + "/24")]} let(:vbox_version) { "6.1.28" } before do expect(subject).to receive(:validate_hostonly_ip!).and_call_original end context "when configuration file exists" do before do allow(subject).to receive(:load_net_conf).and_return(net_conf) end it "should load net configuration" do expect(subject).to receive(:load_net_conf).and_return(net_conf) subject.validate_hostonly_ip!(address, driver) end context "when address is within ranges" do it "should not error" do subject.validate_hostonly_ip!(address, driver) end end context "when address is not found within ranges" do let(:net_conf) { [IPAddr.new("127.0.0.1/20")] } it "should raise an error" do expect { subject.validate_hostonly_ip!(address, driver) }.to raise_error(Vagrant::Errors::VirtualBoxInvalidHostSubnet) end end context "when virtualbox version does not restrict range" do let(:vbox_version) { "6.1.20" } it "should not error" do subject.validate_hostonly_ip!(address, driver) end it "should not attempt to load network configuration" do expect(subject).not_to receive(:load_net_conf) subject.validate_hostonly_ip!(address, driver) end end context "when platform is windows" do before do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) end it "should not error" do subject.validate_hostonly_ip!(address, driver) end it "should not attempt to load network configuration" do expect(subject).not_to receive(:load_net_conf) subject.validate_hostonly_ip!(address, driver) end end end context "when configuration file does not exist" do before do allow(File).to receive(:exist?).with(described_class.const_get(:VBOX_NET_CONF)).and_return(false) end context "when ipv4 address is within range" do let(:address) { "192.168.59.120" } it "should not error" do subject.validate_hostonly_ip!(address, driver) end end context "when ipv4 address is not within range" do let(:address) { "192.168.33.22" } it "should raise an error" do expect { subject.validate_hostonly_ip!(address, driver) }.to raise_error(Vagrant::Errors::VirtualBoxInvalidHostSubnet) end end context "when ipv6 address is within range" do let(:address) { "fe80:77:43:99:974:222:115:20" } it "should not error" do subject.validate_hostonly_ip!(address, driver) end end context "when ipv6 address not within range" do let(:address) { "33:77:43:99:974:222:115:20" } it "should raise an error" do expect { subject.validate_hostonly_ip!(address, driver) }.to raise_error(Vagrant::Errors::VirtualBoxInvalidHostSubnet) end end end end describe "#load_net_conf" do let(:file_contents) { [""] } before do allow(File).to receive(:exist?).and_call_original allow(File).to receive(:exist?). with(described_class.const_get(:VBOX_NET_CONF)). and_return(true) allow(File).to receive(:readlines). with(described_class.const_get(:VBOX_NET_CONF)). and_return(file_contents) end it "should read the configuration file" do expect(File).to receive(:readlines). with(described_class.const_get(:VBOX_NET_CONF)). and_return(file_contents) subject.load_net_conf end context "when file has comments only" do let(:file_contents) { [ "# A comment", "# Another comment", ] } it "should return an empty array" do expect(subject.load_net_conf).to eq([]) end end context "when file has valid range entries" do let(:file_contents) { [ "* 127.0.0.1/24", "* 192.168.1.1/24", ] } it "should return an array with content" do expect(subject.load_net_conf).not_to be_empty end it "should include IPAddr instances" do subject.load_net_conf.each do |entry| expect(entry).to be_a(IPAddr) end end end context "when file has valid range entries and comments" do let(:file_contents) { [ "# Comment in file", "* 127.0.0.0/8", "random text", " * 192.168.2.0/28", ] } it "should contain two entries" do expect(subject.load_net_conf.size).to eq(2) end end context "when file has multiple entries on single line" do let(:file_contents) { [ "* 0.0.0.0/0 ::/0" ] } it "should contain two entries" do expect(subject.load_net_conf.size).to eq(2) end it "should contain an ipv4 and ipv6 range" do result = subject.load_net_conf expect(result.first).to be_ipv4 expect(result.last).to be_ipv6 end end end it "calls the next action in the chain" do called = false app = lambda { |*args| called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end it "creates a host-only interface with an IPv6 address :1" do guest = double("guest") machine.config.vm.network 'private_network', type: :static, ip: 'dead:beef::100' #allow(driver).to receive(:read_bridged_interfaces) { [] } allow(driver).to receive(:read_host_only_interfaces) { [] } #allow(driver).to receive(:read_dhcp_servers) { [] } allow(machine).to receive(:guest) { guest } allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(guest).to receive(:capability) interface_ip = 'dead:beef::1' subject.call(env) expect(driver).to have_received(:create_host_only_network).with(hash_including({ adapter_ip: interface_ip, netmask: 64, })) expect(guest).to have_received(:capability).with(:configure_networks, [{ type: :static6, adapter_ip: 'dead:beef::1', ip: 'dead:beef::100', netmask: 64, auto_config: true, interface: nil }]) end it "raises the appropriate error when provided with an invalid IP address" do machine.config.vm.network 'private_network', ip: '192.168.33.06' expect{ subject.call(env) }.to raise_error(Vagrant::Errors::NetworkAddressInvalid) end context "with a dhcp private network" do let(:bridgedifs) { [] } let(:hostonlyifs) { [] } let(:dhcpservers) { [] } let(:guest) { double("guest") } let(:network_args) {{ type: :dhcp }} before do machine.config.vm.network 'private_network', **network_args allow(driver).to receive(:read_bridged_interfaces) { bridgedifs } allow(driver).to receive(:read_host_only_interfaces) { hostonlyifs } allow(driver).to receive(:read_dhcp_servers) { dhcpservers } allow(machine).to receive(:guest) { guest } end it "tries to setup dhpc server using the ip for the specified network" do allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(driver).to receive(:create_dhcp_server) allow(guest).to receive(:capability) allow(subject).to receive(:hostonly_find_matching_network).and_return({name: "vboxnet1", ip: "192.168.55.1"}) subject.call(env) expect(driver).to have_received(:create_dhcp_server).with('vboxnet1', { adapter_ip: "192.168.55.1", auto_config: true, ip: "192.168.55.1", mac: nil, name: nil, netmask: "255.255.255.0", nic_type: nil, type: :dhcp, dhcp_ip: "192.168.55.2", dhcp_lower: "192.168.55.3", dhcp_upper: "192.168.55.254", adapter: 2 }) expect(guest).to have_received(:capability).with(:configure_networks, [{ type: :dhcp, adapter_ip: "192.168.55.1", ip: "192.168.55.1", netmask: "255.255.255.0", auto_config: true, interface: nil }]) end it "creates a host only interface and a dhcp server using default ips, then tells the guest to configure the network after boot" do allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(driver).to receive(:create_dhcp_server) allow(guest).to receive(:capability) allow(subject).to receive(:hostonly_find_matching_network).and_return(nil) subject.call(env) expect(driver).to have_received(:create_host_only_network).with(hash_including({ adapter_ip: '192.168.56.1', netmask: '255.255.255.0', })) expect(driver).to have_received(:create_dhcp_server).with('vboxnet0', { adapter_ip: "192.168.56.1", auto_config: true, ip: "192.168.56.1", mac: nil, name: nil, netmask: "255.255.255.0", nic_type: nil, type: :dhcp, dhcp_ip: "192.168.56.2", dhcp_lower: "192.168.56.3", dhcp_upper: "192.168.56.254", adapter: 2 }) expect(guest).to have_received(:capability).with(:configure_networks, [{ type: :dhcp, adapter_ip: "192.168.56.1", ip: "192.168.56.1", netmask: "255.255.255.0", auto_config: true, interface: nil }]) end context "when the default vbox dhcpserver is present from a fresh vbox install (see issue #3803)" do let(:dhcpservers) {[ { network_name: 'HostInterfaceNetworking-vboxnet0', network: 'vboxnet0', ip: '192.168.56.100', netmask: '255.255.255.0', lower: '192.168.56.101', upper: '192.168.56.254' } ]} it "removes the invalid dhcpserver so it won't collide with any host only interface" do allow(driver).to receive(:remove_dhcp_server) allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(driver).to receive(:create_dhcp_server) allow(guest).to receive(:capability) subject.call(env) expect(driver).to have_received(:remove_dhcp_server).with('HostInterfaceNetworking-vboxnet0') end context "but the user has intentionally configured their network just that way" do let (:network_args) {{ type: :dhcp, adapter_ip: '192.168.56.1', dhcp_ip: '192.168.56.100', dhcp_lower: '192.168.56.101', dhcp_upper: '192.168.56.254' }} it "does not attempt to remove the dhcpserver" do allow(driver).to receive(:remove_dhcp_server) allow(driver).to receive(:create_host_only_network) {{ name: 'vboxnet0' }} allow(driver).to receive(:create_dhcp_server) allow(guest).to receive(:capability) subject.call(env) expect(driver).not_to have_received(:remove_dhcp_server).with('HostInterfaceNetworking-vboxnet0') end end end end context 'with invalid settings' do [ { ip: 'foo'}, { ip: '1.2.3'}, { ip: 'dead::beef::'}, { ip: '192.168.56.3', netmask: 64}, { ip: '192.168.56.3', netmask: 'ffff:ffff::'}, { ip: 'dead:beef::', netmask: 'foo:bar::'}, { ip: 'dead:beef::', netmask: '255.255.255.0'} ].each do |args| it 'raises an exception' do machine.config.vm.network 'private_network', **args expect { subject.call(env) }. to raise_error(Vagrant::Errors::NetworkAddressInvalid) end end end context "without type set" do before { allow(subject).to receive(:hostonly_adapter).and_return({}) } [ { ip: "192.168.63.5" }, { ip: "192.168.63.5", netmask: "255.255.255.0" }, { ip: "dead:beef::100" }, { ip: "dead:beef::100", netmask: 96 }, ].each do |args| it "sets the type automatically" do machine.config.vm.network "private_network", **args expect(subject).to receive(:hostonly_config) do |config| expect(config).to have_key(:type) addr = IPAddr.new(args[:ip]) if addr.ipv4? expect(config[:type]).to eq(:static) else expect(config[:type]).to eq(:static6) end config end subject.call(env) end end end describe "#hostonly_find_matching_network" do let(:ip){ "192.168.55.2" } let(:config){ {ip: ip, netmask: "255.255.255.0"} } let(:interfaces){ [] } before do allow(driver).to receive(:read_host_only_interfaces).and_return(interfaces) subject.instance_variable_set(:@env, env) end context "with no defined host interfaces" do it "should return nil" do expect(subject.hostonly_find_matching_network(config)).to be_nil end end context "with matching host interface" do let(:interfaces){ [{ip: "192.168.55.1", netmask: "255.255.255.0", name: "vnet"}] } it "should return matching interface" do expect(subject.hostonly_find_matching_network(config)).to eq(interfaces.first) end context "with matching name" do let(:config){ {ip: ip, netmask: "255.255.255.0", name: "vnet"} } it "should return matching interface" do expect(subject.hostonly_find_matching_network(config)).to eq(interfaces.first) end end context "with non-matching name" do let(:config){ {ip: ip, netmask: "255.255.255.0", name: "unknown"} } it "should return nil" do expect(subject.hostonly_find_matching_network(config)).to be_nil end end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/action/prepare_nfs_settings_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require "vagrant/util/platform" describe VagrantPlugins::ProviderVirtualBox::Action::PrepareNFSSettings do include_context "unit" include_context "virtualbox" 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(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) end end let(:env) {{ machine: machine }} let(:app) { lambda { |*args| }} let(:driver) { double("driver") } let(:host) { double("host") } subject { described_class.new(app, env) } before do env[:test] = true allow(machine.env).to receive(:host) { host } allow(host).to receive(:capability?).with(:nfs_installed) { true } allow(host).to receive(:capability).with(:nfs_installed) { true } # We don't care about smb support so return not installed allow(host).to receive(:capability?).with(:smb_installed).and_return(false) end it "calls the next action in the chain" do allow(driver).to receive(:read_network_interfaces).and_return({2 => {type: :hostonly, hostonly: "vmnet2"}}) allow(driver).to receive(:read_host_only_interfaces).and_return([{name: "vmnet2", ip: "1.2.3.4"}]) allow(driver).to receive(:read_guest_ip).with(1).and_return("2.3.4.5") called = false app = lambda { |*args| called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end context "with an nfs synced folder" do let(:host_only_interfaces) { [{name: "vmnet2", ip: "1.2.3.4"}] } before do # We can't be on Windows, because NFS gets disabled on Windows allow(Vagrant::Util::Platform).to receive(:windows?).and_return(false) env[:machine].config.vm.synced_folder("/host/path", "/guest/path", type: "nfs") env[:machine].config.finalize! # Stub out the stuff so it just works by default allow(driver).to receive(:read_network_interfaces).and_return({ 2 => {type: :hostonly, hostonly: "vmnet2"}, }) allow(driver).to receive(:read_host_only_interfaces).and_return(host_only_interfaces) allow(driver).to receive(:read_guest_ip).with(1).and_return("2.3.4.5") # override sleep to 0 so test does not take seconds retry_options = subject.retry_options allow(subject).to receive(:retry_options).and_return(retry_options.merge(sleep: 0)) end context "with host interface netmask defined" do context "with machine IP included within host interface range" do let(:host_only_interfaces) { [{name: "vmnet2", ip: "2.3.4.1", netmask: "255.255.255.0"}] } it "sets nfs_host_ip and nfs_machine_ip properly" do subject.call(env) expect(env[:nfs_host_ip]).to eq("2.3.4.1") expect(env[:nfs_machine_ip]).to eq("2.3.4.5") end end context "with machine IP included within host interface range" do let(:host_only_interfaces) { [{name: "vmnet2", ip: "1.2.3.4", netmask: "255.255.255.0"}] } it "raises an error when the machine IP is not within host interface range" do expect{ subject.call(env) }.to raise_error(Vagrant::Errors::NFSNoHostonlyNetwork) end end end it "sets nfs_host_ip and nfs_machine_ip properly" do subject.call(env) expect(env[:nfs_host_ip]).to eq("1.2.3.4") expect(env[:nfs_machine_ip]).to eq("2.3.4.5") end it "raises an error when no host only adapter is configured" do allow(driver).to receive(:read_network_interfaces) {{}} expect { subject.call(env) }. to raise_error(Vagrant::Errors::NFSNoHostonlyNetwork) end it "retries through guest property not found errors" do raise_then_return = [ lambda { raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: 'stub' }, lambda { "2.3.4.5" } ] allow(driver).to receive(:read_guest_ip) { raise_then_return.shift.call } subject.call(env) expect(env[:nfs_host_ip]).to eq("1.2.3.4") expect(env[:nfs_machine_ip]).to eq("2.3.4.5") end it "raises an error informing the user of a bug when the guest IP cannot be found" do allow(driver).to receive(:read_guest_ip) { raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: 'stub' } expect { subject.call(env) }. to raise_error(Vagrant::Errors::NFSNoGuestIP) end it "allows statically configured guest IPs to work for NFS, even when guest property would fail" do env[:machine].config.vm.network :private_network, ip: "11.12.13.14" allow(driver).to receive(:read_guest_ip) { raise Vagrant::Errors::VirtualBoxGuestPropertyNotFound, guest_property: "stub" } subject.call(env) expect(env[:nfs_host_ip]).to eq("1.2.3.4") expect(env[:nfs_machine_ip]).to eq(["11.12.13.14"]) end it "allows statically configured guest IPs to co-exist with dynamic host only IPs for NFS" do env[:machine].config.vm.network :private_network, ip: "11.12.13.14" subject.call(env) expect(env[:nfs_host_ip]).to eq("1.2.3.4") expect(env[:nfs_machine_ip]).to eq(["11.12.13.14", "2.3.4.5"]) end it "allows the use of scoped hash overrides as options" do env[:machine].config.vm.network :private_network, virtualbox__ip: "11.12.13.14" subject.call(env) expect(env[:nfs_host_ip]).to eq("1.2.3.4") expect(env[:nfs_machine_ip]).to eq(["11.12.13.14", "2.3.4.5"]) end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/action/prepare_nfs_valid_ids_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Action::PrepareNFSValidIds do include_context "unit" include_context "virtualbox" 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(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) end end let(:env) {{ machine: machine }} let(:app) { lambda { |*args| }} let(:driver) { double("driver") } subject { described_class.new(app, env) } before do allow(driver).to receive(:read_vms).and_return({}) end it "calls the next action in the chain" do called = false app = lambda { |*args| called = true } action = described_class.new(app, env) action.call(env) expect(called).to eq(true) end it "sets nfs_valid_ids" do hash = {"foo" => "1", "bar" => "4"} allow(driver).to receive(:read_vms).and_return(hash) subject.call(env) expect(env[:nfs_valid_ids]).to eql(hash.values) end end ================================================ FILE: test/unit/plugins/providers/virtualbox/action/set_default_nic_type_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Action::SetDefaultNICType do include_context "unit" include_context "virtualbox" 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(:machine) do iso_env.machine(iso_env.machine_names[0], :virtualbox).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) end end let(:env) {{ machine: machine, ui: machine.ui }} let(:app) { lambda { |*args| }} let(:driver) { double("driver") } subject { described_class.new(app, env) } describe "#call" do let(:provider_config) { double("provider_config", default_nic_type: default_nic_type, network_adapters: network_adapters) } let(:default_nic_type) { nil } let(:network_adapters) { {} } let(:virtualbox_version) { "5.2.23" } before do allow(driver).to receive(:version).and_return(virtualbox_version) allow(machine).to receive(:provider_config).and_return(provider_config) end it "should call the next action" do expect(app).to receive(:call) subject.call(env) end context "when default_nic_type is set" do let(:default_nic_type) { "CUSTOM_NIC_TYPE" } context "when network adapters are defined" do let(:network_adapters) { {"1" => [:nat, {}], "2" => [:intnet, {nic_type: nil}]} } it "should set nic type if not defined" do subject.call(env) expect(network_adapters["1"].last[:nic_type]).to eq(default_nic_type) end it "should not set nic type if already defined" do subject.call(env) expect(network_adapters["2"].last[:nic_type]).to be_nil end end context "when vm networks are defined" do before do machine.config.vm.network :private_network machine.config.vm.network :public_network, nic_type: nil machine.config.vm.network :private_network, virtualbox__nic_type: "STANDARD" end it "should add namespaced nic type when not defined" do subject.call(env) networks = machine.config.vm.networks.map { |type, opts| opts if type.to_s.end_with?("_network") }.compact expect(networks.first[:virtualbox__nic_type]).to eq(default_nic_type) end it "should not add namespaced nic type when nic type defined" do subject.call(env) networks = machine.config.vm.networks.map { |type, opts| opts if type.to_s.end_with?("_network") }.compact expect(networks[1][:virtualbox__nic_type]).to be_nil end it "should not modify existing namespaced nic type" do subject.call(env) networks = machine.config.vm.networks.map { |type, opts| opts if type.to_s.end_with?("_network") }.compact expect(networks.last[:virtualbox__nic_type]).to eq("STANDARD") end end end context "when virtualbox version is has susceptible E1000" do let(:virtualbox_version) { "5.2.21" } it "should output a warning" do expect(machine.ui).to receive(:warn) subject.call(env) end context "when default_nic_type is set to E1000 type" do let(:default_nic_type) { "82540EM" } it "should output a warning" do expect(machine.ui).to receive(:warn) subject.call(env) end end context "when default_nic_type is set to non-E1000 type" do let(:default_nic_type) { "virtio" } it "should not output a warning" do expect(machine.ui).not_to receive(:warn) subject.call(env) end context "when network adapter is configured with E1000 type" do let(:network_adapters) { {"1" => [:nat, {nic_type: "82540EM" }]} } it "should output a warning" do expect(machine.ui).to receive(:warn) subject.call(env) end end context "when vm network is configured with E1000 type" do before { machine.config.vm.network :private_network, nic_type: "82540EM" } it "should output a warning" do expect(machine.ui).to receive(:warn) subject.call(env) end end context "when vm network is configured with E1000 type in namespaced argument" do before { machine.config.vm.network :private_network, virtualbox__nic_type: "82540EM" } it "should output a warning" do expect(machine.ui).to receive(:warn) subject.call(env) end end end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # base test helper for virtualbox unit tests require_relative "../../../base" require_relative "support/shared/virtualbox_driver_version_4_x_examples" require_relative "support/shared/virtualbox_driver_version_5_x_examples" require_relative "support/shared/virtualbox_driver_version_6_x_examples" require_relative "support/shared/virtualbox_driver_version_7_x_examples" ================================================ FILE: test/unit/plugins/providers/virtualbox/cap/cleanup_disks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require Vagrant.source_root.join("plugins/providers/virtualbox/cap/cleanup_disks") describe VagrantPlugins::ProviderVirtualBox::Cap::CleanupDisks 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(:driver) { double("driver") } let(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) allow(m).to receive(:state).and_return(state) end end let(:state) do double(:state) end let(:subject) { described_class } let(:disk_meta_file) { {"disk" => [], "floppy" => [], "dvd" => []} } let(:defined_disks) { {} } let(:attachments) { [{port: "0", device: "0", uuid: "12345"}, {port: "1", device: "0", uuid: "67890"}]} let(:controller) { double("controller", name: "controller", limit: 30, maxportcount: 30) } let(:storage_controllers) { double("storage controllers") } before do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).and_return(true) allow(controller).to receive(:get_attachment).with(port: "0", device: "0").and_return(attachments[0]) allow(controller).to receive(:get_attachment).with(uuid: "12345").and_return(attachments[0]) allow(controller).to receive(:get_attachment).with(uuid: "67890").and_return(attachments[1]) allow(storage_controllers).to receive(:get_controller).and_return(controller) allow(storage_controllers).to receive(:get_primary_controller).and_return(controller) allow(storage_controllers).to receive(:get_primary_attachment).and_return(attachments[0]) allow(driver).to receive(:read_storage_controllers).and_return(storage_controllers) end describe "#cleanup_disks" do it "returns if there's no data in meta file" do subject.cleanup_disks(machine, defined_disks, disk_meta_file) expect(subject).not_to receive(:handle_cleanup_disk) end context "with disks to clean up" do let(:disk_meta_file) { {"disk" => [{"uuid" => "1234", "name" => "storage"}], "floppy" => [], "dvd" => []} } it "calls the cleanup method if a disk_meta file is defined" do expect(subject).to receive(:handle_cleanup_disk). with(machine, defined_disks, disk_meta_file["disk"]). and_return(true) subject.cleanup_disks(machine, defined_disks, disk_meta_file) end it "raises an error if primary disk can't be found" do allow(storage_controllers).to receive(:get_primary_attachment).and_raise(Vagrant::Errors::VirtualBoxDisksPrimaryNotFound) expect { subject.cleanup_disks(machine, defined_disks, disk_meta_file) }. to raise_error(Vagrant::Errors::VirtualBoxDisksPrimaryNotFound) end end context "with dvd attached" do let(:disk_meta_file) { {"disk" => [], "floppy" => [], "dvd" => [{"uuid" => "12345", "name" => "iso"}] } } it "calls the cleanup method if a disk_meta file is defined" do expect(subject).to receive(:handle_cleanup_dvd). with(machine, defined_disks, disk_meta_file["dvd"]). and_return(true) subject.cleanup_disks(machine, defined_disks, disk_meta_file) end end end describe "#handle_cleanup_disk" do let(:disk_meta_file) { { disk: [{ "uuid" => "67890", "name" => "storage", "controller" => "controller", "port" => "1", "device" => "0" }], floppy: [], dvd: [] } } let(:defined_disks) { [] } it "removes and closes medium from guest" do expect(driver).to receive(:remove_disk).with("controller", "1", "0").and_return(true) expect(driver).to receive(:close_medium).with("67890").and_return(true) subject.handle_cleanup_disk(machine, defined_disks, disk_meta_file[:disk]) end context "when the disk isn't attached to a guest" do it "only closes the medium" do allow(controller).to receive(:get_attachment).with(uuid: "67890").and_return(nil) expect(driver).to receive(:close_medium).with("67890").and_return(true) subject.handle_cleanup_disk(machine, defined_disks, disk_meta_file[:disk]) end end context "when attachment is not found at the expected device" do it "removes the disk from the correct device" do allow(controller).to receive(:get_attachment).with(uuid: "67890").and_return(port: "2", device: "0") expect(driver).to receive(:remove_disk).with("controller", "2", "0").and_return(true) expect(driver).to receive(:close_medium).with("67890").and_return(true) subject.handle_cleanup_disk(machine, defined_disks, disk_meta_file[:disk]) end end end describe "#handle_cleanup_dvd" do let(:disk_meta_file) { {dvd: [{"uuid" => "1234", "name" => "iso", "port" => "0", "device" => "0", "controller" => "controller" }]} } let(:defined_disks) { [] } it "removes the medium from guest" do allow(controller).to receive(:get_attachment).with(uuid: "1234").and_return(port: "0", device: "0") expect(driver).to receive(:remove_disk).with("controller", "0", "0").and_return(true) subject.handle_cleanup_dvd(machine, defined_disks, disk_meta_file[:dvd]) end context "when attachment is not found at the expected device" do it "removes the disk from the correct device" do allow(controller).to receive(:get_attachment).with(uuid: "1234").and_return(port: "0", device: "1") expect(driver).to receive(:remove_disk).with("controller", "0", "1").and_return(true) subject.handle_cleanup_dvd(machine, defined_disks, disk_meta_file[:dvd]) end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/cap/configure_disks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require Vagrant.source_root.join("plugins/providers/virtualbox/cap/configure_disks") describe VagrantPlugins::ProviderVirtualBox::Cap::ConfigureDisks 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(:driver) { double("driver") } let(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) allow(m).to receive(:state).and_return(state) end end let(:state) do double(:state) end let(:storage_controllers) { double("storage controllers") } let(:controller) { double("controller", name: "controller", maxportcount: 30, devices_per_port: 1, limit: 30) } let(:attachments) { [{:port=>"0", :device=>"0", :uuid=>"12345", :storage_format=>"VMDK", :capacity=>"65536 MBytes", :disk_name=>"ubuntu-18.04-amd64-disk001", :location=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk"}, {:port=>"1", :device=>"0", :uuid=>"67890", :storage_format=>"VDI", :capacity=>"10240 MBytes", :disk_name=>"disk-0", :location=>"/home/vagrant/VirtualBox VMs/disk-0.vdi"}, {:port=>"2", :device=>"0", :uuid=>"10111", :storage_format=>"VDI", :capacity=>"10240 MBytes", :disk_name=>"disk-1", :location=>"/home/vagrant/VirtualBox VMs/disk-1.vdi"}] } let(:defined_disks) { [double("disk", name: "vagrant_primary", size: Vagrant::Util::Numeric::string_to_bytes("65GB"), primary: true, type: :disk), double("disk", name: "disk-0", size: Vagrant::Util::Numeric::string_to_bytes("10GB"), primary: false, type: :disk), double("disk", name: "disk-1", size: "10GB", primary: false, type: :disk), double("disk", name: "disk-2", size: "5GB", primary: false, type: :disk)] } let(:all_disks) { [{:port=>"0", :device=>"0", :uuid=>"12345", :storage_format=>"VMDK", :capacity=>"65536 MBytes", :disk_name=>"ubuntu-18.04-amd64-disk001", :location=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk"}, {:port=>"1", :device=>"0", :uuid=>"67890", :storage_format=>"VDI", :capacity=>"10240 MBytes", :disk_name=>"disk-0", :location=>"/home/vagrant/VirtualBox VMs/disk-0.vdi"}, {:port=>"2", :device=>"0", :uuid=>"10111", :storage_format=>"VDI", :capacity=>"10240 MBytes", :disk_name=>"disk-1", :location=>"/home/vagrant/VirtualBox VMs/disk-1.vdi"}] } let(:list_hdds_result) { [{"UUID"=>"12345", "Parent UUID"=>"base", "State"=>"created", "Type"=>"normal (base)", "Location"=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk", "Disk Name"=>"ubuntu-18.04-amd64-disk001", "Storage format"=>"VMDK", "Capacity"=>"65536 MBytes", "Encryption"=>"disabled"}, {"UUID"=>"67890", "Parent UUID"=>"base", "State"=>"created", "Type"=>"normal (base)", "Location"=>"/home/vagrant/VirtualBox VMs/disk-0.vdi", "Disk Name"=>"disk-0", "Storage format"=>"VDI", "Capacity"=>"10240 MBytes", "Encryption"=>"disabled"}, {"UUID"=>"324bbb53-d5ad-45f8-9bfa-1f2468b199a8", "Parent UUID"=>"base", "State"=>"created", "Type"=>"normal (base)", "Location"=>"/home/vagrant/VirtualBox VMs/disk-1.vdi", "Disk Name"=>"disk-1", "Storage format"=>"VDI", "Capacity"=>"5120 MBytes", "Encryption"=>"disabled"}] } let(:subject) { described_class } before do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).and_return(true) allow(controller).to receive(:attachments).and_return(attachments) allow(storage_controllers).to receive(:get_controller).with(controller.name).and_return(controller) allow(storage_controllers).to receive(:first).and_return(controller) allow(storage_controllers).to receive(:size).and_return(1) allow(driver).to receive(:read_storage_controllers).and_return(storage_controllers) allow(driver).to receive(:list_hdds).and_return(list_hdds_result) end describe "#configure_disks" do let(:dsk_data) { {uuid: "1234", name: "disk"} } let(:dvd) { double("dvd", type: :dvd, name: "dvd", primary: false) } before do allow(driver).to receive(:list_hdds).and_return([]) end it "configures disks and returns the disks defined" do expect(subject).to receive(:handle_configure_disk).with(machine, anything, controller.name). exactly(4).and_return(dsk_data) subject.configure_disks(machine, defined_disks) end it "configures dvd and returns the disks defined" do defined_disks = [ dvd ] expect(subject).to receive(:handle_configure_dvd).with(machine, dvd, controller.name). and_return({}) subject.configure_disks(machine, defined_disks) end context "with no disks to configure" do let(:defined_disks) { {} } it "returns empty hash if no disks to configure" do expect(subject.configure_disks(machine, defined_disks)).to eq({}) end end # NOTE: In this scenario, one slot must be reserved for the primary # disk, so the controller limit goes down by 1 when there is no primary # disk defined in the config. context "with over the disk limit for a given device" do let(:defined_disks) { (1..controller.limit).map { |i| double("disk-#{i}", type: :disk, primary: false) }.to_a } it "raises an exception if the disks defined exceed the limit" do expect{subject.configure_disks(machine, defined_disks)}. to raise_error(Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit) end end # hashicorp/bionic64 context "with more than one storage controller" do let(:controller1) { double("controller1", name: "IDE Controller", maxportcount: 2, devices_per_port: 2, limit: 4) } let(:controller2) { double("controller2", name: "SATA Controller", maxportcount: 30, devices_per_port: 1, limit: 30) } before do allow(storage_controllers).to receive(:size).and_return(2) allow(storage_controllers).to receive(:get_controller).with(controller1.name). and_return(controller1) allow(storage_controllers).to receive(:get_controller).with(controller2.name). and_return(controller2) allow(storage_controllers).to receive(:get_dvd_controller).and_return(controller1) allow(storage_controllers).to receive(:get_primary_controller).and_return(controller2) end it "attaches disks to the primary controller" do expect(subject).to receive(:handle_configure_disk).with(machine, anything, controller2.name). exactly(4).and_return(dsk_data) subject.configure_disks(machine, defined_disks) end it "attaches dvds to the secondary controller" do defined_disks = [ dvd ] expect(subject).to receive(:handle_configure_dvd).with(machine, dvd, controller1.name). and_return({}) subject.configure_disks(machine, defined_disks) end it "raises an error if there are more than 4 dvds configured" do defined_disks = [ double("dvd", name: "installer1", type: :dvd, file: "installer.iso", primary: false), double("dvd", name: "installer2", type: :dvd, file: "installer.iso", primary: false), double("dvd", name: "installer3", type: :dvd, file: "installer.iso", primary: false), double("dvd", name: "installer4", type: :dvd, file: "installer.iso", primary: false), double("dvd", name: "installer5", type: :dvd, file: "installer.iso", primary: false) ] expect { subject.configure_disks(machine, defined_disks) }. to raise_error(Vagrant::Errors::VirtualBoxDisksDefinedExceedLimit) end it "attaches multiple dvds" do defined_disks = [ double("dvd", name: "installer1", type: :dvd, file: "installer.iso", primary: false), double("dvd", name: "installer2", type: :dvd, file: "installer.iso", primary: false), double("dvd", name: "installer3", type: :dvd, file: "installer.iso", primary: false), double("dvd", name: "installer4", type: :dvd, file: "installer.iso", primary: false), ] expect(subject).to receive(:handle_configure_dvd).exactly(4).times.and_return({}) subject.configure_disks(machine, defined_disks) end end end describe "#get_current_disk" do it "gets primary disk uuid if disk to configure is primary" do allow(storage_controllers).to receive(:get_primary_attachment).and_return(attachments[0]) primary_disk = subject.get_current_disk(machine, defined_disks.first, all_disks) expect(primary_disk).to eq(all_disks.first) end it "raises an error if primary disk can't be found" do allow(storage_controllers).to receive(:get_primary_attachment).and_raise(Vagrant::Errors::VirtualBoxDisksPrimaryNotFound) expect { subject.get_current_disk(machine, defined_disks.first, all_disks) }. to raise_error(Vagrant::Errors::VirtualBoxDisksPrimaryNotFound) end it "finds the disk to configure" do disk = subject.get_current_disk(machine, defined_disks[1], all_disks) expect(disk).to eq(all_disks[1]) end it "returns nil if disk is not found" do disk = subject.get_current_disk(machine, defined_disks[3], all_disks) expect(disk).to be_nil end end describe "#handle_configure_disk" do context "when creating a new disk" do let(:all_disks) { [{:port=>"0", :device=>"0", :uuid=>"12345", :storage_format=>"VMDK", :capacity=>"65536 MBytes", :disk_name=>"ubuntu-18.04-amd64-disk001", :location=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk"}] } let(:list_hdds_result) { [{"UUID"=>"12345", "Parent UUID"=>"base", "State"=>"created", "Type"=>"normal (base)", "Location"=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk", "Disk Name"=>"ubuntu-18.04-amd64-disk001", "Storage format"=>"VMDK", "Capacity"=>"65536 MBytes", "Encryption"=>"disabled"}] } let(:disk_meta) { {uuid: "67890", name: "disk-0", controller: "controller", port: "1", device: "1"} } it "creates a new disk if it doesn't yet exist" do expect(subject).to receive(:create_disk).with(machine, defined_disks[1], controller) .and_return(disk_meta) expect(controller).to receive(:attachments).and_return(all_disks) expect(storage_controllers).to receive(:get_primary_attachment) .and_return(all_disks[0]) subject.handle_configure_disk(machine, defined_disks[1], controller.name) end it "includes disk attachment info in metadata" do expect(subject).to receive(:create_disk).with(machine, defined_disks[1], controller) .and_return(disk_meta) expect(controller).to receive(:attachments).and_return(all_disks) expect(storage_controllers).to receive(:get_primary_attachment) .and_return(all_disks[0]) disk_metadata = subject.handle_configure_disk(machine, defined_disks[1], controller.name) expect(disk_metadata).to have_key(:controller) expect(disk_metadata).to have_key(:port) expect(disk_metadata).to have_key(:device) expect(disk_metadata).to have_key(:name) end end context "when a disk needs to be resized" do let(:all_disks) { [{:port=>"0", :device=>"0", :uuid=>"12345", :storage_format=>"VMDK", :capacity=>"65536 MBytes", :disk_name=>"ubuntu-18.04-amd64-disk001", :location=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk"}, {:port=>"1", :device=>"0", :uuid=>"67890", :storage_format=>"VDI", :capacity=>"10240 MBytes", :disk_name=>"disk-0", :location=>"/home/vagrant/VirtualBox VMs/disk-0.vdi"}] } it "resizes a disk" do expect(controller).to receive(:attachments).and_return(all_disks) expect(subject).to receive(:get_current_disk). with(machine, defined_disks[1], all_disks).and_return(all_disks[1]) expect(subject).to receive(:compare_disk_size). with(machine, defined_disks[1], all_disks[1]).and_return(true) expect(subject).to receive(:resize_disk). with(machine, defined_disks[1], all_disks[1], controller).and_return({}) subject.handle_configure_disk(machine, defined_disks[1], controller.name) end end context "if no additional disk configuration is required" do let(:all_disks) { [{:port=>"0", :device=>"0", :uuid=>"12345", :storage_format=>"VMDK", :capacity=>"65536 MBytes", :disk_name=>"ubuntu-18.04-amd64-disk001", :location=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk"}, {:port=>"1", :device=>"0", :uuid=>"67890", :storage_format=>"VDI", :capacity=>"10240 MBytes", :disk_name=>"disk-0", :location=>"/home/vagrant/VirtualBox VMs/disk-0.vdi"}] } let(:disk_info) { {port: "1", device: "0"} } let(:attachments) { [{:port=>"0", :device=>"0", :uuid=>"12345", :disk_name=>"ubuntu-18.04-amd64-disk001", :location=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk"}, {:port=>"1", :device=>"0", :uuid=>"67890", :disk_name=>"disk-0", :location=>"/home/vagrant/VirtualBox VMs/disk-0.vdi"}] } it "reattaches disk if vagrant defined disk exists but is not attached to guest" do expect(controller).to receive(:attachments).and_return(all_disks) expect(subject).to receive(:get_current_disk). with(machine, defined_disks[1], all_disks).and_return(nil) expect(storage_controllers).to receive(:get_primary_attachment) .and_return(all_disks[0]) expect(driver).to receive(:attach_disk).with(controller.name, (disk_info[:port].to_i + 1).to_s, disk_info[:device], "hdd", all_disks[1][:location]) subject.handle_configure_disk(machine, defined_disks[1], controller.name) end it "does nothing if all disks are properly configured" do expect(controller).to receive(:attachments).and_return(all_disks) expect(subject).to receive(:get_current_disk). with(machine, defined_disks[1], all_disks).and_return(all_disks[1]) expect(subject).to receive(:compare_disk_size). with(machine, defined_disks[1], all_disks[1]).and_return(false) subject.handle_configure_disk(machine, defined_disks[1], controller.name) end end end describe "#compare_disk_size" do let(:disk_config_small) { double("disk", name: "disk-0", size: 1073741824.0, primary: false, type: :disk) } let(:disk_config_large) { double("disk", name: "disk-0", size: 68719476736.0, primary: false, type: :disk) } it "shows a warning if user attempts to shrink size" do expect(machine.ui).to receive(:warn) expect(subject.compare_disk_size(machine, disk_config_small, all_disks[1])).to be_falsey end it "returns true if requested size is bigger than current size" do expect(subject.compare_disk_size(machine, disk_config_large, all_disks[1])).to be_truthy end end describe "#create_disk" do let(:disk_config) { double("disk", name: "disk-0", size: 1073741824.0, primary: false, type: :disk, disk_ext: "vdi", provider_config: nil) } let(:vm_info) { {"CfgFile"=>"/home/vagrant/VirtualBox VMs/disks/"} } let(:disk_file) { "/home/vagrant/VirtualBox VMs/disk-0.vdi" } let(:disk_data) { "Medium created. UUID: 67890\n" } let(:port_and_device) { {port: "1", device: "0"} } it "creates a disk and attaches it to a guest" do expect(driver).to receive(:show_vm_info).and_return(vm_info) expect(driver).to receive(:create_disk). with(disk_file, disk_config.size, "VDI").and_return(disk_data) expect(subject).to receive(:get_next_port).with(machine, controller). and_return(port_and_device) expect(driver).to receive(:attach_disk).with(controller.name, port_and_device[:port], port_and_device[:device], "hdd", disk_file) subject.create_disk(machine, disk_config, controller) end end describe ".get_next_port" do let(:attachments) { [{:port=>"0", :device=>"0", :uuid=>"12345", :disk_name=>"ubuntu-18.04-amd64-disk001", :location=>"/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk"}, {:port=>"1", :device=>"0", :uuid=>"67890", :disk_name=>"disk-0", :location=>"/home/vagrant/VirtualBox VMs/disk-0.vdi"}] } it "determines the next available port and device to use" do dsk_info = subject.get_next_port(machine, controller) expect(dsk_info[:port]).to eq("2") expect(dsk_info[:device]).to eq("0") end context "with IDE controller" do let(:controller) { double("controller", name: "IDE", maxportcount: 2, devices_per_port: 2, limit: 4) } let(:attachments) { [] } it "attaches to port 0, device 0" do dsk_info = subject.get_next_port(machine, controller) expect(dsk_info[:port]).to eq("0") expect(dsk_info[:device]).to eq("0") end context "with 1 device" do let(:attachments) { [{port:"0", device: "0"}] } it "attaches to the next device on that port" do dsk_info = subject.get_next_port(machine, controller) expect(dsk_info[:port]).to eq("0") expect(dsk_info[:device]).to eq("1") end end end context "with SCSI controller" do let(:controller) { double("controller", name: "SCSI", maxportcount: 16, devices_per_port: 1, limit: 16) } let(:attachments) { [] } let(:vm_info) { {"SATA Controller-ImageUUID-0-0" => "12345", "SATA Controller-ImageUUID-1-0" => "67890"} } it "determines the next available port and device to use" do allow(driver).to receive(:show_vm_info).and_return(vm_info) dsk_info = subject.get_next_port(machine, controller) expect(dsk_info[:port]).to eq("0") expect(dsk_info[:device]).to eq("0") end end end describe "#resize_disk" do context "when a disk is vmdk format" do let(:disk_config) { double("disk", name: "vagrant_primary", size: 1073741824.0, primary: false, type: :disk, disk_ext: "vmdk", provider_config: nil) } let(:attach_info) { {port: "0", device: "0"} } let(:vdi_disk_file) { "/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vdi" } let(:vmdk_disk_file) { "/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk" } it "converts the disk to vdi, resizes it, and converts back to vmdk" do expect(FileUtils).to receive(:mv).with(vmdk_disk_file, "#{vmdk_disk_file}.backup"). and_return(true) expect(driver).to receive(:vmdk_to_vdi).with(all_disks[0][:location]). and_return(vdi_disk_file) expect(driver).to receive(:resize_disk).with(vdi_disk_file, disk_config.size.to_i).and_return(true) expect(driver).to receive(:remove_disk).with(controller.name, attach_info[:port], attach_info[:device]). and_return(true) expect(driver).to receive(:close_medium).with("12345") expect(driver).to receive(:vdi_to_vmdk).with(vdi_disk_file). and_return(vmdk_disk_file) expect(driver).to receive(:attach_disk). with(controller.name, attach_info[:port], attach_info[:device], "hdd", vmdk_disk_file).and_return(true) expect(driver).to receive(:close_medium).with(vdi_disk_file).and_return(true) expect(driver).to receive(:read_storage_controllers) expect(storage_controllers).to receive(:get_controller) expect(FileUtils).to receive(:remove).with("#{vmdk_disk_file}.backup", force: true). and_return(true) subject.resize_disk(machine, disk_config, all_disks[0], controller) end it "reattaches original disk if something goes wrong" do expect(FileUtils).to receive(:mv).with(vmdk_disk_file, "#{vmdk_disk_file}.backup"). and_return(true) expect(driver).to receive(:vmdk_to_vdi).with(all_disks[0][:location]). and_return(vdi_disk_file) expect(driver).to receive(:resize_disk).with(vdi_disk_file, disk_config.size.to_i).and_return(true) expect(driver).to receive(:remove_disk).with(controller.name, attach_info[:port], attach_info[:device]). and_return(true) expect(driver).to receive(:close_medium).with("12345") allow(driver).to receive(:vdi_to_vmdk).and_raise(StandardError) expect(subject).to receive(:recover_from_resize).with(machine, all_disks[0], "#{vmdk_disk_file}.backup", all_disks[0], vdi_disk_file, controller) expect{subject.resize_disk(machine, disk_config, all_disks[0], controller)}.to raise_error(Exception) end end context "when a disk is vdi format" do let(:disk_config) { double("disk", name: "disk-0", size: 1073741824.0, primary: false, type: :disk, disk_ext: "vdi", provider_config: nil) } it "resizes the disk" do expect(driver).to receive(:resize_disk).with(all_disks[1][:location], disk_config.size.to_i) subject.resize_disk(machine, disk_config, all_disks[1], controller) end end end describe "#vmdk_to_vdi" do it "converts a disk from vmdk to vdi" do end end describe "#vdi_to_vmdk" do it "converts a disk from vdi to vmdk" do end end describe ".recover_from_resize" do let(:disk_config) { double("disk", name: "vagrant_primary", size: 1073741824.0, primary: false, type: :disk, disk_ext: "vmdk", provider_config: nil) } let(:attach_info) { {port: "0", device: "0"} } let(:vdi_disk_file) { "/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vdi" } let(:vmdk_disk_file) { "/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk" } let(:vmdk_backup_file) { "/home/vagrant/VirtualBox VMs/ubuntu-18.04-amd64-disk001.vmdk.backup" } it "reattaches the original disk file and closes the cloned medium" do expect(FileUtils).to receive(:mv).with(vmdk_backup_file, vmdk_disk_file, force: true). and_return(true) expect(driver).to receive(:attach_disk). with(controller.name, attach_info[:port], attach_info[:device], "hdd", vmdk_disk_file).and_return(true) expect(driver).to receive(:close_medium).with(vdi_disk_file).and_return(true) subject.recover_from_resize(machine, attach_info, vmdk_backup_file, all_disks[0], vdi_disk_file, controller) end end describe ".handle_configure_dvd" do let(:dvd_config) { double("dvd", file: "/tmp/untitled.iso", name: "dvd1", primary: false) } before do allow(subject).to receive(:get_next_port).with(machine, controller). and_return({device: "0", port: "0"}) allow(controller).to receive(:attachments).and_return( [port: "0", device: "0", uuid: "12345"] ) end it "includes disk attachment info in metadata" do expect(driver).to receive(:attach_disk).with(controller.name, "0", "0", "dvddrive", "/tmp/untitled.iso") dvd_metadata = subject.handle_configure_dvd(machine, dvd_config, controller.name) expect(dvd_metadata[:uuid]).to eq("12345") expect(dvd_metadata).to have_key(:controller) expect(dvd_metadata).to have_key(:port) expect(dvd_metadata).to have_key(:device) expect(dvd_metadata).to have_key(:name) end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/cap/mount_options_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" describe "VagrantPlugins::ProviderVirtualBox::Cap::MountOptions" do let(:caps) do VagrantPlugins::ProviderVirtualBox::Plugin .components .synced_folder_capabilities[:virtualbox] end let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:mount_owner){ "vagrant" } let(:mount_group){ "vagrant" } let(:mount_uid){ "1000" } let(:mount_gid){ "1000" } let(:mount_name){ "vagrant" } let(:mount_guest_path){ "/vagrant" } let(:folder_options) do { owner: mount_owner, group: mount_group, hostpath: "/host/directory/path" } end let(:cap){ caps.get(:mount_options) } before do allow(machine).to receive(:communicate).and_return(comm) end after do comm.verify_expectations! end describe ".mount_options" do before do allow(comm).to receive(:sudo).with(any_args) allow(comm).to receive(:execute).with(any_args) end context "with owner user ID explicitly defined" do before do expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") end context "with user ID provided as Integer" do let(:mount_owner){ 2000 } it "generates the expected mount command using mount_owner directly" do out_mount_options, out_mount_uid, out_mount_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options) expect(out_mount_options).to eq("uid=#{mount_owner},gid=#{mount_gid},_netdev") expect(out_mount_uid).to eq(mount_owner) expect(out_mount_gid).to eq(mount_gid) end end context "with user ID provided as String" do let(:mount_owner){ "2000" } it "generates the expected mount command using mount_owner directly" do out_mount_options, out_mount_uid, out_mount_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options) expect(out_mount_options).to eq("uid=#{mount_owner},gid=#{mount_gid},_netdev") expect(out_mount_uid).to eq(mount_owner) expect(out_mount_gid).to eq(mount_gid) end end end context "with owner group ID explicitly defined" do before do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) end context "with owner group ID provided as Integer" do let(:mount_group){ 2000 } it "generates the expected mount command using mount_group directly" do out_mount_options, out_mount_uid, out_mount_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options) expect(out_mount_options).to eq("uid=#{mount_uid},gid=#{mount_group},_netdev") expect(out_mount_uid).to eq(mount_uid) expect(out_mount_gid).to eq(mount_group) end end context "with owner group ID provided as String" do let(:mount_group){ "2000" } it "generates the expected mount command using mount_group directly" do out_mount_options, out_mount_uid, out_mount_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options) expect(out_mount_options).to eq("uid=#{mount_uid},gid=#{mount_group},_netdev") expect(out_mount_uid).to eq(mount_uid) expect(out_mount_gid).to eq(mount_group) end end end context "with non-existent default owner group" do it "fetches the effective group ID of the user" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_raise(Vagrant::Errors::VirtualBoxMountFailed, {command: '', output: ''}) expect(comm).to receive(:execute).with("id -g #{mount_owner}", anything).and_yield(:stdout, "1").and_return(0) out_mount_options, out_mount_uid, out_mount_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options) expect(out_mount_options).to eq("uid=#{mount_uid},gid=1,_netdev") end end context "with non-existent owner group" do it "raises an error" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_raise(Vagrant::Errors::VirtualBoxMountFailed, {command: '', output: ''}) expect do cap.mount_options(machine, mount_name, mount_guest_path, folder_options) end.to raise_error Vagrant::Errors::VirtualBoxMountFailed end end context "with read-only option defined" do it "does not chown mounted guest directory" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") out_mount_options, out_mount_uid, out_mount_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["ro"])) expect(out_mount_options).to eq("ro,uid=#{mount_uid},gid=#{mount_gid},_netdev") expect(out_mount_uid).to eq(mount_uid) expect(out_mount_gid).to eq(mount_gid) end end context "with custom mount options" do let(:ui){ Vagrant::UI::Silent.new } before do allow(machine).to receive(:ui).and_return(ui) end context "with uid defined" do let(:options_uid){ '1234' } it "should only include uid defined within mount options" do expect(comm).not_to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") out_mount_options, out_mount_uid, out_mount_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["uid=#{options_uid}"]) ) expect(out_mount_options).to eq("uid=#{options_uid},gid=#{mount_gid},_netdev") expect(out_mount_uid).to eq(options_uid) expect(out_mount_gid).to eq(mount_gid) end end context "with gid defined" do let(:options_gid){ '1234' } it "should only include gid defined within mount options" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).not_to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") out_mount_options, out_mount_uid, out_mount_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["gid=#{options_gid}"]) ) expect(out_mount_options).to eq("uid=#{mount_uid},gid=#{options_gid},_netdev") expect(out_mount_uid).to eq(mount_uid) expect(out_mount_gid).to eq(options_gid) end end context "with uid and gid defined" do let(:options_gid){ '1234' } let(:options_uid){ '1234' } it "should only include uid and gid defined within mount options" do expect(comm).not_to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).not_to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{options_gid}:") out_mount_options, out_mount_uid, out_mount_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options.merge(mount_options: ["uid=#{options_uid}", "gid=#{options_gid}"]) ) expect(out_mount_options).to eq("uid=#{options_uid},gid=#{options_gid},_netdev") expect(out_mount_uid).to eq(options_uid) expect(out_mount_gid).to eq(options_gid) end end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/cap/public_address_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require Vagrant.source_root.join("plugins/providers/virtualbox/cap/public_address") describe VagrantPlugins::ProviderVirtualBox::Cap::PublicAddress 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(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m).to receive(:state).and_return(state) end end let(:state) do double(:state) end describe "#public_address" do it "returns nil when the machine is not running" do allow(state).to receive(:id).and_return(:not_created) expect(described_class.public_address(machine)).to be(nil) end it "returns nil when there is no ssh info" do allow(state).to receive(:id).and_return(:not_created) allow(machine).to receive(:ssh_info).and_return(nil) expect(described_class.public_address(machine)).to be(nil) end it "returns the host" do allow(state).to receive(:id).and_return(:running) allow(machine).to receive(:ssh_info).and_return(host: "test") expect(described_class.public_address(machine)).to eq("test") end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/cap_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "base" require Vagrant.source_root.join("plugins/providers/virtualbox/cap") describe VagrantPlugins::ProviderVirtualBox::Cap 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(:machine) do iso_env.machine(iso_env.machine_names[0], :dummy).tap do |m| allow(m.provider).to receive(:driver).and_return(driver) allow(m).to receive(:state).and_return(state) end end let(:driver) { double("driver") } let(:state) { double("state", id: :running) } describe "#forwarded_ports" do it "returns all the forwarded ports" do allow(driver).to receive(:read_forwarded_ports).and_return([ [nil, nil, 123, 456], [nil, nil, 245, 245], ]) expect(described_class.forwarded_ports(machine)).to eq({ 123 => 456, 245 => 245, }) end it "returns nil when the machine is not running" do allow(machine).to receive(:state).and_return(double(:state, id: :stopped)) expect(described_class.forwarded_ports(machine)).to be(nil) end end describe "#snapshot_list" do it "returns all the snapshots" do allow(machine).to receive(:id).and_return("1234") allow(driver).to receive(:list_snapshots).with(machine.id). and_return(["backup", "old"]) expect(described_class.snapshot_list(machine)).to eq(["backup", "old"]) end it "returns empty array when the machine is does not exist" do allow(machine).to receive(:id).and_return(nil) expect(described_class.snapshot_list(machine)).to eq([]) end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/providers/virtualbox/config") describe VagrantPlugins::ProviderVirtualBox::Config do let(:machine) { double("machine") } def assert_invalid errors = subject.validate(machine) if !errors.values.any? { |v| !v.empty? } raise "No errors: #{errors.inspect}" end end def assert_valid errors = subject.validate(machine) if !errors.values.all? { |v| v.empty? } raise "Errors: #{errors.inspect}" end end def valid_defaults subject.image = "foo" end before do vm_config = double("vm_config") allow(vm_config).to receive(:networks).and_return([]) config = double("config") allow(config).to receive(:vm).and_return(vm_config) allow(machine).to receive(:config).and_return(config) end its "valid by default" do subject.finalize! assert_valid end context "defaults" do before { subject.finalize! } it { expect(subject.check_guest_additions).to be(true) } it { expect(subject.gui).to be(false) } it { expect(subject.name).to be_nil } it { expect(subject.functional_vboxsf).to be(true) } it { expect(subject.default_nic_type).to be_nil } it "should have one NAT adapter" do expect(subject.network_adapters).to eql({ 1 => [:nat, {}], }) end end describe "#default_nic_type" do let(:nic_type) { "custom" } before do subject.default_nic_type = nic_type subject.finalize! end it { expect(subject.default_nic_type).to eq(nic_type) } end describe "#merge" do let(:one) { described_class.new } let(:two) { described_class.new } subject { one.merge(two) } it "merges the customizations" do one.customize ["foo"] two.customize ["bar"] expect(subject.customizations).to eq([ ["pre-boot", ["foo"]], ["pre-boot", ["bar"]]]) end end describe "#network_adapter" do it "configures additional adapters" do subject.network_adapter(2, :bridged, auto_config: true) expect(subject.network_adapters[2]).to eql( [:bridged, auto_config: true]) end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/base.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require Vagrant.source_root.join("plugins/providers/virtualbox/driver/base") describe VagrantPlugins::ProviderVirtualBox::Driver::Base do describe "#env_lang" do context "when locale command is not available" do before do allow(Vagrant::Util::Which).to receive(:which).with("locale").and_return(false) end it "should return default value" do expect(subject.send(:env_lang)).to eq({LANG: "C"}) end end context "when the locale command is available" do let(:result) { Vagrant::Util::Subprocess::Result.new(exit_code, stdout, stderr) } let(:stderr) { "" } let(:stdout) { "C.default" } let(:exit_code) { 0 } before do allow(Vagrant::Util::Which).to receive(:which).with("locale").and_return(true) allow(Vagrant::Util::Subprocess).to receive(:execute).with("locale", "-a").and_return(result) end context "when locale command errors" do let(:exit_code) { 1 } it "should return default value" do expect(subject.send(:env_lang)).to eq({LANG: "C"}) end end context "when locale command does not error" do let(:exit_code) { 0 } let(:base) { "de_AT.utf8\nde_BE.utf8\nde_CH.utf8\nde_DE.utf8\nde_IT.utf8\nde_LI.utf8\nde_LU.utf8\nen_AG\nen_AG.utf8\nen_AU.utf8\nen_BW.utf8\nen_CA.utf8\nen_DK.utf8\nen_GB.utf8\nen_HK.utf8\nen_IE.utf8\nen_IL\nen_IL.utf8\nen_IN\nen_IN.utf8\nen_NG\n" } context "when stdout includes C" do let(:stdout) { "#{base}C\n" } it "should use C for the lang" do expect(subject.send(:env_lang)).to eq({LANG: "C"}) end context "when stdout includes UTF-8 variants of C" do let(:stdout) { "#{base}C\nC.UTF-8" } it "should use the UTF-8 variant" do expect(subject.send(:env_lang)).to eq({LANG: "C.UTF-8"}) end end context "when stdout includes utf8 variants of C" do let(:stdout) { "#{base}C\nC.utf8" } it "should use the utf8 variant" do expect(subject.send(:env_lang)).to eq({LANG: "C.utf8"}) end end end context "when stdout does not include C" do context "when stdout includes C.UTF-8" do let(:stdout) { "#{base}C.UTF-8\n"} it "should use C.UTF-8 for the lang" do expect(subject.send(:env_lang)).to eq({LANG: "C.UTF-8"}) end end context "when stdout includes C.utf8" do let(:stdout) { "#{base}C.utf8\n"} it "should use C.utf8 for the lang" do expect(subject.send(:env_lang)).to eq({LANG: "C.utf8"}) end end context "when stdout includes POSIX" do let(:stdout) { "#{base}POSIX\n"} it "should use POSIX for the lang" do expect(subject.send(:env_lang)).to eq({LANG: "POSIX"}) end end context "when stdout includes en_US.UTF-8" do let(:stdout) { "#{base}en_US.UTF-8\n"} it "should use en_US.UTF-8 for the lang" do expect(subject.send(:env_lang)).to eq({LANG: "en_US.UTF-8"}) end end context "when stdout includes en_US.utf8" do let(:stdout) { "#{base}en_US.utf8\n"} it "should use en_US.utf8 for the lang" do expect(subject.send(:env_lang)).to eq({LANG: "en_US.utf8"}) end end end context "when stdout does not include any variations" do let(:stdout) { base } it "should default to C" do expect(subject.send(:env_lang)).to eq({LANG: "C"}) end end end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_4_0_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_4_0 do include_context "virtualbox" let(:vbox_version) { "4.0.0" } subject { VagrantPlugins::ProviderVirtualBox::Driver::Meta.new(uuid) } it_behaves_like "a version 4.x virtualbox driver" end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_4_1_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_4_1 do include_context "virtualbox" let(:vbox_version) { "4.1.0" } subject { VagrantPlugins::ProviderVirtualBox::Driver::Meta.new(uuid) } it_behaves_like "a version 4.x virtualbox driver" end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_4_2_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_4_2 do include_context "virtualbox" let(:vbox_version) { "4.2.0" } subject { VagrantPlugins::ProviderVirtualBox::Driver::Meta.new(uuid) } it_behaves_like "a version 4.x virtualbox driver" end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_4_3_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_4_3 do include_context "virtualbox" let(:vbox_version) { "4.3.0" } subject { VagrantPlugins::ProviderVirtualBox::Driver::Meta.new(uuid) } it_behaves_like "a version 4.x virtualbox driver" end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_5_0_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_5_0 do include_context "virtualbox" let(:vbox_version) { "5.0.0" } let(:controller_name) { "controller" } subject { VagrantPlugins::ProviderVirtualBox::Driver::Version_5_0.new(uuid) } it_behaves_like "a version 4.x virtualbox driver" it_behaves_like "a version 5.x virtualbox driver" describe "#import" do let(:ovf) { double("ovf") } let(:machine_id) { double("machine_id") } let(:output) {<<-OUTPUT 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% Interpreting /home/user/.vagrant.d/boxes/hashicorp-VAGRANTSLASH-precise64/1.1.0/virtualbox/box.ovf... OK. Disks: vmdisk1 85899345920 -1 http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized box-disk1.vmdk -1 -1 Virtual system 0: 0: Suggested OS type: "Ubuntu_64" (change with "--vsys 0 --ostype "; use "list ostypes" to list all possible values) 1: Suggested VM name "precise64" (change with "--vsys 0 --vmname ") 2: Number of CPUs: 2 (change with "--vsys 0 --cpus ") 3: Guest memory: 384 MB (change with "--vsys 0 --memory ") 4: Network adapter: orig NAT, config 3, extra slot=0;type=NAT 5: CD-ROM (disable with "--vsys 0 --unit 5 --ignore") 6: IDE controller, type PIIX4 (disable with "--vsys 0 --unit 6 --ignore") 7: IDE controller, type PIIX4 (disable with "--vsys 0 --unit 7 --ignore") 8: SATA controller, type AHCI (disable with "--vsys 0 --unit 8 --ignore") 9: Hard disk image: source image=box-disk1.vmdk, target path=/home/user/VirtualBox VMs/precise64/box-disk1.vmdk, controller=8;channel=0 (change target path with "--vsys 0 --unit 9 --disk path"; disable with "--vsys 0 --unit 9 --ignore") OUTPUT } before do allow(Vagrant::Util::Platform).to receive(:windows_path). with(ovf).and_return(ovf) allow(subject).to receive(:execute).with("import", "-n", ovf). and_return(output) allow(subject).to receive(:execute).with("import", ovf, any_args) allow(subject).to receive(:get_machine_id).and_return(machine_id) end it "should return the machine id" do expect(subject).to receive(:get_machine_id).and_return(machine_id) expect(subject.import(ovf)).to eq(machine_id) end it "should return machine id using custom name" do expect(subject).to receive(:get_machine_id).with(/.*precise64_.+/).and_return(machine_id) expect(subject.import(ovf)).to eq(machine_id) end it "should include disk image on import" do expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) expect(subject).to receive(:execute) do |*args| match = args[3, args.size].detect { |a| a.include?("disk1.vmdk") } expect(match).to include("disk1.vmdk") end expect(subject.import(ovf)).to eq(machine_id) end it "should include full path for disk image on import" do expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) expect(subject).to receive(:execute) do |*args| dpath = args[3, args.size].detect { |a| a.include?("disk1.vmdk") } expect(Pathname.new(dpath).absolute?).to be_truthy end expect(subject.import(ovf)).to eq(machine_id) end context "suggested name is not provided" do before { output.sub!(/Suggested VM name/, "") } it "should raise an error" do expect { subject.import(ovf) }.to raise_error(Vagrant::Errors::VirtualBoxNoName) end end end describe "#attach_disk" do it "attaches a device to the specified controller" do expect(subject).to receive(:execute) do |*args| storagectl = args[args.index("--storagectl") + 1] expect(storagectl).to eq(controller_name) end subject.attach_disk(controller_name, anything, anything, anything, anything) end end describe "#remove_disk" do it "removes a disk from the specified controller" do expect(subject).to receive(:execute) do |*args| storagectl = args[args.index("--storagectl") + 1] expect(storagectl).to eq(controller_name) end subject.remove_disk(controller_name, anything, anything) end end describe "#read_storage_controllers" do before do allow(subject).to receive(:show_vm_info).and_return( { "storagecontrollername0" => "SATA Controller", "storagecontrollertype0" => "IntelAhci", "storagecontrollermaxportcount0" => "30", "SATA Controller-0-0" => "/tmp/primary.vdi", "SATA Controller-ImageUUID-0-0" => "12345", "SATA Controller-1-0" => "/tmp/secondary.vdi", "SATA Controller-ImageUUID-1-0" => "67890" } ) allow(subject).to receive(:list_hdds).and_return( [ {"UUID"=>"12345", "Parent UUID"=>"base", "State"=>"created", "Type"=>"normal (base)", "Location"=>"/tmp/primary.vdi", "Disk Name"=>"primary", "Storage format"=>"VDI", "Capacity"=>"65536 MBytes", "Encryption"=>"disabled"}, {"UUID"=>"67890", "Parent UUID"=>"base", "State"=>"created", "Type"=>"normal (base)", "Location"=>"/tmp/secondary.vdi", "Disk Name"=>"primary", "Storage format"=>"VDI", "Capacity"=>"65536 MBytes", "Encryption"=>"disabled"} ] ) end let(:attachments_result) { [{:port=>"0", :device=>"0", :uuid=>"12345", :location=>"/tmp/primary.vdi", :parent_uuid=>"base", :state=>"created", :type=>"normal (base)", :disk_name=>"primary", :storage_format=>"VDI", :capacity=>"65536 MBytes", :encryption=>"disabled"}, {:port=>"1", :device=>"0", :uuid=>"67890", :location=>"/tmp/secondary.vdi", :parent_uuid=>"base", :state=>"created", :type=>"normal (base)", :disk_name=>"primary", :storage_format=>"VDI", :capacity=>"65536 MBytes", :encryption=>"disabled"}] } it "returns a list of storage controllers" do storage_controllers = subject.read_storage_controllers expect(storage_controllers.first.name).to eq("SATA Controller") expect(storage_controllers.first.type).to eq("IntelAhci") expect(storage_controllers.first.maxportcount).to eq(30) end it "includes attachments for each storage controller" do storage_controllers = subject.read_storage_controllers expect(storage_controllers.first.attachments).to eq(attachments_result) end end end VBOX_SYSTEM_PROPERTIES=%( API version: 7_0 Minimum guest RAM size: 4 Megabytes Maximum guest RAM size: 2097152 Megabytes Minimum video RAM size: 0 Megabytes Maximum video RAM size: 256 Megabytes Maximum guest monitor count: 64 Minimum guest CPU count: 1 Maximum guest CPU count: 64 Virtual disk limit (info): 2199022206976 Bytes Maximum Serial Port count: 4 Maximum Parallel Port count: 2 Maximum Boot Position: 4 Maximum PIIX3 Network Adapter count: 8 Maximum ICH9 Network Adapter count: 36 Maximum PIIX3 IDE Controllers: 1 Maximum ICH9 IDE Controllers: 1 Maximum IDE Port count: 2 Maximum Devices per IDE Port: 2 Maximum PIIX3 SATA Controllers: 1 Maximum ICH9 SATA Controllers: 8 Maximum SATA Port count: 30 Maximum Devices per SATA Port: 1 Maximum PIIX3 SCSI Controllers: 1 Maximum ICH9 SCSI Controllers: 8 Maximum SCSI Port count: 16 Maximum Devices per SCSI Port: 1 Maximum SAS PIIX3 Controllers: 1 Maximum SAS ICH9 Controllers: 8 Maximum SAS Port count: 255 Maximum Devices per SAS Port: 1 Maximum NVMe PIIX3 Controllers: 1 Maximum NVMe ICH9 Controllers: 8 Maximum NVMe Port count: 255 Maximum Devices per NVMe Port: 1 Maximum virtio-scsi PIIX3 Controllers: 1 Maximum virtio-scsi ICH9 Controllers: 8 Maximum virtio-scsi Port count: 256 Maximum Devices per virtio-scsi Port: 1 Maximum PIIX3 Floppy Controllers:1 Maximum ICH9 Floppy Controllers: 1 Maximum Floppy Port count: 1 Maximum Devices per Floppy Port: 2 Default machine folder: /home/username/VirtualBox VMs Raw-mode Supported: no Exclusive HW virtualization use: on Default hard disk format: VDI VRDE auth library: VBoxAuth Webservice auth. library: VBoxAuth Remote desktop ExtPack: VM encryption ExtPack: Log history count: 3 Default frontend: Default audio driver: ALSA Autostart database path: Default Guest Additions ISO: /usr/share/virtualbox/VBoxGuestAdditions.iso Logging Level: all Proxy Mode: System Proxy URL: User language: en_US ) ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_6_0_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_6_0 do include_context "virtualbox" let(:vbox_version) { "6.0.0" } subject { VagrantPlugins::ProviderVirtualBox::Driver::Version_6_0.new(uuid) } it_behaves_like "a version 4.x virtualbox driver" it_behaves_like "a version 5.x virtualbox driver" it_behaves_like "a version 6.x virtualbox driver" describe "#import" do let(:ovf) { double("ovf") } let(:machine_id) { double("machine_id") } let(:output) {<<-OUTPUT 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% Interpreting /home/user/.vagrant.d/boxes/hashicorp-VAGRANTSLASH-precise64/1.1.0/virtualbox/box.ovf... OK. Disks: vmdisk1 85899345920 -1 http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized box-disk1.vmdk -1 -1 Virtual system 0: 0: Suggested OS type: "Ubuntu_64" (change with "--vsys 0 --ostype "; use "list ostypes" to list all possible values) 1: Suggested VM name "precise64" (change with "--vsys 0 --vmname ") 2: Suggested VM group "/" (change with "--vsys 0 --group ") 3: Suggested VM settings file name "/home/user/VirtualBox VMs/precise64/precise64.vbox" (change with "--vsys 0 --settingsfile ") 4: Suggested VM base folder "/home/vagrant/VirtualBox VMs" (change with "--vsys 0 --basefolder ") 5: Number of CPUs: 2 (change with "--vsys 0 --cpus ") 6: Guest memory: 384 MB (change with "--vsys 0 --memory ") 7: Network adapter: orig NAT, config 3, extra slot=0;type=NAT 8: CD-ROM (disable with "--vsys 0 --unit 8 --ignore") 9: IDE controller, type PIIX4 (disable with "--vsys 0 --unit 9 --ignore") 10: IDE controller, type PIIX4 (disable with "--vsys 0 --unit 10 --ignore") 11: SATA controller, type AHCI (disable with "--vsys 0 --unit 11 --ignore") 12: Hard disk image: source image=box-disk1.vmdk, target path=box-disk1.vmdk, controller=11;channel=0 (change target path with "--vsys 0 --unit 12 --disk path"; disable with "--vsys 0 --unit 12 --ignore") OUTPUT } before do allow(Vagrant::Util::Platform).to receive(:windows_path). with(ovf).and_return(ovf) allow(subject).to receive(:execute).with("import", "-n", ovf). and_return(output) allow(subject).to receive(:execute).with("import", ovf, any_args) allow(subject).to receive(:get_machine_id).and_return(machine_id) end it "should return the machine id" do expect(subject).to receive(:get_machine_id).and_return(machine_id) expect(subject.import(ovf)).to eq(machine_id) end it "should return machine id using custom name" do expect(subject).to receive(:get_machine_id).with(/.*precise64_.+/).and_return(machine_id) expect(subject.import(ovf)).to eq(machine_id) end it "should include disk image on import" do expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) expect(subject).to receive(:execute) do |*args| match = args[3, args.size].detect { |a| a.include?("disk1.vmdk") } expect(match).to include("disk1.vmdk") end expect(subject.import(ovf)).to eq(machine_id) end it "should include full path for disk image on import" do expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) expect(subject).to receive(:execute) do |*args| dpath = args[3, args.size].detect { |a| a.include?("disk1.vmdk") } expect(Pathname.new(dpath).absolute?).to be_truthy end expect(subject.import(ovf)).to eq(machine_id) end context "suggested name is not provided" do before { output.sub!(/Suggested VM name/, "") } it "should raise an error" do expect { subject.import(ovf) }.to raise_error(Vagrant::Errors::VirtualBoxNoName) end end context "when within windows" do before do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) end let(:output) {<<-OUTPUT 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100% Interpreting C:\\home\\user\\.vagrant.d\\boxes\\hashicorp-VAGRANTSLASH-precise64\\1.1.0\\virtualbox\\box.ovf... OK. Disks: vmdisk1 85899345920 -1 http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized box-disk1.vmdk -1 -1 Virtual system 0: 0: Suggested OS type: "Ubuntu_64" (change with "--vsys 0 --ostype "; use "list ostypes" to list all possible values) 1: Suggested VM name "precise64" (change with "--vsys 0 --vmname ") 2: Suggested VM group "/" (change with "--vsys 0 --group ") 3: Suggested VM settings file name "C:\\home\\user\\VirtualBox VMs\\precise64\\precise64.vbox" (change with "--vsys 0 --settingsfile ") 4: Suggested VM base folder "C:\\home\\vagrant\\VirtualBox VMs" (change with "--vsys 0 --basefolder ") 5: Number of CPUs: 2 (change with "--vsys 0 --cpus ") 6: Guest memory: 384 MB (change with "--vsys 0 --memory ") 7: Network adapter: orig NAT, config 3, extra slot=0;type=NAT 8: CD-ROM (disable with "--vsys 0 --unit 8 --ignore") 9: IDE controller, type PIIX4 (disable with "--vsys 0 --unit 9 --ignore") 10: IDE controller, type PIIX4 (disable with "--vsys 0 --unit 10 --ignore") 11: SATA controller, type AHCI (disable with "--vsys 0 --unit 11 --ignore") 12: Hard disk image: source image=box-disk1.vmdk, target path=box-disk1.vmdk, controller=11;channel=0 (change target path with "--vsys 0 --unit 12 --disk path"; disable with "--vsys 0 --unit 12 --ignore") OUTPUT } it "should include disk image on import" do expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) expect(subject).to receive(:execute) do |*args| match = args[3, args.size].detect { |a| a.include?("disk1.vmdk") } expect(match).to include("disk1.vmdk") end expect(subject.import(ovf)).to eq(machine_id) end it "should update the suggested VM path from default box name" do expect(subject).to receive(:execute).with("import", "-n", ovf).and_return(output) expect(subject).to receive(:execute) do |*args| match = args[3, args.size].detect { |a| a.include?("box-disk1.vmdk") } expect(match).not_to include("/precise64/box-disk1.vmdk") expect(match).to match(/.+precise64_.+?\/box-disk1.vmdk/) end expect(subject.import(ovf)).to eq(machine_id) end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_6_1_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_6_1 do include_context "virtualbox" let(:vbox_version) { "6.1.0" } subject { VagrantPlugins::ProviderVirtualBox::Driver::Version_6_1.new(uuid) } it_behaves_like "a version 5.x virtualbox driver" it_behaves_like "a version 6.x virtualbox driver" describe "#read_dhcp_servers" do before { expect(subprocess).to receive(:execute). with("VBoxManage", "list", "dhcpservers", an_instance_of(Hash)). and_return(subprocess_result(stdout: output)) } context "with empty output" do let(:output) { "" } it "returns an empty list" do expect(subject.read_dhcp_servers).to eq([]) end end context "with a single dhcp server" do let(:output) { <<-OUTPUT.gsub(/^ */, '') NetworkName: HostInterfaceNetworking-vboxnet0 Dhcpd IP: 192.168.56.100 LowerIPAddress: 192.168.56.101 UpperIPAddress: 192.168.56.254 NetworkMask: 255.255.255.0 Enabled: Yes Global Configuration: minLeaseTime: default defaultLeaseTime: default maxLeaseTime: default Forced options: None Suppressed opts.: None 1/legacy: 255.255.255.0 Groups: None Individual Configs: None OUTPUT } it "returns a list with one entry describing that server" do expect(subject.read_dhcp_servers).to eq([{ network_name: 'HostInterfaceNetworking-vboxnet0', network: 'vboxnet0', ip: '192.168.56.100', netmask: '255.255.255.0', lower: '192.168.56.101', upper: '192.168.56.254', }]) end end context "with a multiple dhcp servers" do let(:output) { <<-OUTPUT.gsub(/^ */, '') NetworkName: HostInterfaceNetworking-vboxnet0 Dhcpd IP: 192.168.56.100 LowerIPAddress: 192.168.56.101 UpperIPAddress: 192.168.56.254 NetworkMask: 255.255.255.0 Enabled: Yes Global Configuration: minLeaseTime: default defaultLeaseTime: default maxLeaseTime: default Forced options: None Suppressed opts.: None 1/legacy: 255.255.255.0 Groups: None Individual Configs: None NetworkName: HostInterfaceNetworking-vboxnet5 Dhcpd IP: 172.28.128.2 LowerIPAddress: 172.28.128.3 UpperIPAddress: 172.28.128.254 NetworkMask: 255.255.255.0 Enabled: Yes Global Configuration: minLeaseTime: default defaultLeaseTime: default maxLeaseTime: default Forced options: None Suppressed opts.: None 1/legacy: 255.255.255.0 Groups: None Individual Configs: None OUTPUT } it "returns a list with one entry for each server" do expect(subject.read_dhcp_servers).to eq([{ network_name: 'HostInterfaceNetworking-vboxnet0', network: 'vboxnet0', ip: '192.168.56.100', netmask: '255.255.255.0', lower: '192.168.56.101', upper: '192.168.56.254', },{ network_name: 'HostInterfaceNetworking-vboxnet5', network: 'vboxnet5', ip: '172.28.128.2', netmask: '255.255.255.0', lower: '172.28.128.3', upper: '172.28.128.254', }]) end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_7_0_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "stringio" require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_7_0 do include_context "virtualbox" let(:vbox_version) { "7.0.0" } subject { VagrantPlugins::ProviderVirtualBox::Driver::Version_7_0.new(uuid) } it_behaves_like "a version 5.x virtualbox driver" it_behaves_like "a version 6.x virtualbox driver" it_behaves_like "a version 7.x virtualbox driver" describe "#read_forwarded_ports" do let(:uuid) { "MACHINE-UUID" } let(:cfg_path) { "MACHINE_CONFIG_PATH" } let(:vm_info) { %(name="vagrant-test_default_1665781960041_56631" Encryption: disabled groups="/" ostype="Ubuntu (64-bit)" UUID="#{uuid}" CfgFile="#{cfg_path}" SnapFldr="/VirtualBox VMs/vagrant-test_default_1665781960041_56631/Snapshots" LogFldr="/VirtualBox VMs/vagrant-test_default_1665781960041_56631/Logs" memory=1024) } let(:config_file) { StringIO.new(VBOX_VMCONFIG_FILE) } before do allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:version).and_return(vbox_version) end describe "VirtualBox version 7.0.0" do let(:vbox_version) { "7.0.0" } before do allow(subject).to receive(:execute).with("showvminfo", uuid, any_args).and_return(vm_info) allow(File).to receive(:open).with(cfg_path, "r").and_yield(config_file) end it "should return two port forward values" do expect(subject.read_forwarded_ports.size).to eq(2) end it "should have port forwards on slot one" do subject.read_forwarded_ports.each do |fwd| expect(fwd.first).to eq(1) end end it "should include host ip for ssh forward" do fwd = subject.read_forwarded_ports.detect { |f| f[1] == "ssh" } expect(fwd).not_to be_nil expect(fwd.last).to eq("127.0.0.1") end describe "when config file cannot be determine" do let(:vm_info) { %(name="vagrant-test_default_1665781960041_56631") } it "should raise a custom error" do expect(File).not_to receive(:open).with(cfg_path, "r") expect { subject.read_forwarded_ports }.to raise_error(Vagrant::Errors::VirtualBoxConfigNotFound) end end end describe "VirtualBox version greater than 7.0.0" do let(:vbox_version) { "7.0.1" } before do allow(subject).to receive(:execute).with("showvminfo", uuid, any_args).and_return(vm_info) end it "should not read configuration file" do expect(File).not_to receive(:open).with(cfg_path, "r") subject.read_forwarded_ports end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_7_1_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "stringio" require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_7_1 do include_context "virtualbox" subject { VagrantPlugins::ProviderVirtualBox::Driver::Version_7_1.new(uuid) } it_behaves_like "a version 5.x virtualbox driver" it_behaves_like "a version 6.x virtualbox driver" it_behaves_like "a version 7.x virtualbox driver" end ================================================ FILE: test/unit/plugins/providers/virtualbox/driver/version_7_2_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "stringio" require_relative "../base" describe VagrantPlugins::ProviderVirtualBox::Driver::Version_7_2 do include_context "virtualbox" subject { VagrantPlugins::ProviderVirtualBox::Driver::Version_7_2.new(uuid) } it_behaves_like "a version 5.x virtualbox driver" it_behaves_like "a version 6.x virtualbox driver" it_behaves_like "a version 7.x virtualbox driver" end ================================================ FILE: test/unit/plugins/providers/virtualbox/model/storage_controller_array_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) describe VagrantPlugins::ProviderVirtualBox::Model::StorageControllerArray do include_context "unit" let(:controller1) { double("controller1", name: "IDE Controller", supported?: true, boot_priority: 1) } let(:controller2) { double("controller2", name: "SATA Controller", supported?: true, boot_priority: 2) } let(:primary_disk) { {location: "/tmp/primary.vdi"} } before do subject.replace([controller1, controller2]) end describe "#get_controller" do it "gets a controller by name" do expect(subject.get_controller("IDE Controller")).to eq(controller1) end it "raises an exception if a matching storage controller can't be found" do expect { subject.get_controller(name: "Foo Controller") }. to raise_error(Vagrant::Errors::VirtualBoxDisksControllerNotFound) end end describe "#get_primary_controller" do context "with a single supported controller" do before do subject.replace([controller1]) allow(controller1).to receive(:attachments).and_return([primary_disk]) end it "returns the controller" do expect(subject.get_primary_controller).to eq(controller1) end end context "with multiple controllers" do before do allow(controller1).to receive(:attachments).and_return([]) allow(controller2).to receive(:attachments).and_return([primary_disk]) end it "returns the first supported controller with a disk attached" do expect(subject.get_primary_controller).to eq(controller2) end it "raises an error if the primary disk is attached to an unsupported controller" do allow(controller2).to receive(:supported?).and_return(false) expect { subject.get_primary_controller }. to raise_error(Vagrant::Errors::VirtualBoxDisksNoSupportedControllers) end end end describe "#hdd?" do let(:attachment) { {} } it "determines whether the given attachment represents a hard disk" do expect(subject.send(:hdd?, attachment)).to be(false) end it "returns true for disk files ending in compatible extensions" do attachment[:location] = "/tmp/primary.vdi" expect(subject.send(:hdd?, attachment)).to be(true) end it "is case insensitive" do attachment[:location] = "/tmp/PRIMARY.VDI" expect(subject.send(:hdd?, attachment)).to be(true) end end describe "#get_primary_attachment" do let(:attachment) { {location: "/tmp/primary.vdi"} } before do allow(subject).to receive(:get_primary_controller).and_return(controller2) end it "returns the first attachment on the primary controller" do allow(controller2).to receive(:get_attachment).with(port: "0", device: "0").and_return(attachment) expect(subject.get_primary_attachment).to be(attachment) end it "raises an exception if no attachment exists at port 0, device 0" do allow(controller2).to receive(:get_attachment).with(port: "0", device: "0").and_return(nil) expect { subject.get_primary_attachment }.to raise_error(Vagrant::Errors::VirtualBoxDisksPrimaryNotFound) end end describe "#get_dvd_controller" do context "with one controller" do let(:controller) { double("controller", supported?: true, boot_priority: 1) } before do subject.replace([controller]) end it "returns the controller" do expect(subject.get_dvd_controller).to be(controller) end it "raises an exception if the controller is unsupported" do allow(controller).to receive(:supported?).and_return(false) expect { subject.get_dvd_controller }.to raise_error(Vagrant::Errors::VirtualBoxDisksNoSupportedControllers) end end context "with multiple controllers" do let(:controller1) { double("controller", supported?: true, boot_priority: 2) } let(:controller2) { double("controller", supported?: true, boot_priority: 1) } let(:controller3) { double("controller", supported?: false, boot_priority: nil) } before do subject.replace([controller1, controller2, controller3]) end it "returns the first supported controller" do expect(subject.get_dvd_controller).to be(controller2) end it "raises an exception if no controllers are supported" do allow(controller1).to receive(:supported?).and_return(false) allow(controller2).to receive(:supported?).and_return(false) expect { subject.get_dvd_controller }.to raise_error(Vagrant::Errors::VirtualBoxDisksNoSupportedControllers) end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/model/storage_controller_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) describe VagrantPlugins::ProviderVirtualBox::Model::StorageController do include_context "unit" let(:name) {} let(:type) { "IntelAhci" } let(:maxportcount) { 30 } let(:attachments) {} subject { described_class.new(name, type, maxportcount, attachments) } describe "#initialize" do context "with SATA controller type" do it "recognizes a SATA controller" do expect(subject.sata?).to be(true) end it "calculates the maximum number of attachments" do expect(subject.limit).to eq(30) end it "sets the boot priority" do expect(subject.boot_priority).to eq(2) end end context "with IDE controller type" do let(:type) { "PIIX4" } let(:maxportcount) { 2 } it "recognizes an IDE controller" do expect(subject.ide?).to be(true) end it "calculates the maximum number of attachments" do expect(subject.limit).to eq(4) end it "sets the boot priority" do expect(subject.boot_priority).to eq(1) end end context "with SCSI controller type" do let(:type) { "LsiLogic" } let(:maxportcount) { 16 } it "recognizes an SCSI controller" do expect(subject.scsi?).to be(true) end it "calculates the maximum number of attachments" do expect(subject.limit).to eq(16) end it "sets the boot priority" do expect(subject.boot_priority).to eq(3) end end context "with some other type" do let(:type) { "foo" } it "is unknown" do expect(subject.ide?).to be(false) expect(subject.sata?).to be(false) end end end describe "#supported?" do it "returns true if the controller type is supported" do expect(subject.supported?).to be(true) end context "with unsupported type" do let(:type) { "foo" } it "returns false" do expect(subject.supported?).to be(false) end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/provider_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/providers/virtualbox/provider") describe VagrantPlugins::ProviderVirtualBox::Provider do let(:driver){ double("driver") } let(:provider){ double("provider", driver: driver) } let(:provider_config){ double("provider_config") } let(:uid) { "1000" } let(:machine){ double("machine", uid: uid, provider: provider, provider_config: provider_config) } let(:platform) { double("platform") } subject { described_class.new(machine) } before do stub_const("Vagrant::Util::Platform", platform) allow(platform).to receive(:windows?).and_return(false) allow(platform).to receive(:cygwin?).and_return(false) allow(platform).to receive(:wsl?).and_return(false) allow(platform).to receive(:wsl_windows_access_bypass?).and_return(false) allow(machine).to receive(:id).and_return("foo") allow(Process).to receive(:uid).and_return(uid) end describe ".usable?" do subject { described_class } it "returns true if usable" do allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:new).and_return(driver) expect(subject).to be_usable end it "raises an exception if virtualbox is not available" do allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:new). and_raise(Vagrant::Errors::VirtualBoxNotDetected) expect { subject.usable?(true) }. to raise_error(Vagrant::Errors::VirtualBoxNotDetected) end it "raises an exception if virtualbox is the wrong version" do allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:new). and_raise(Vagrant::Errors::VirtualBoxInvalidVersion, supported_versions: "1,2,3") expect { subject.usable?(true) }. to raise_error(Vagrant::Errors::VirtualBoxInvalidVersion) end it "raises an exception if virtualbox kernel module is not loaded" do allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:new). and_raise(Vagrant::Errors::VirtualBoxKernelModuleNotLoaded) expect { subject.usable?(true) }. to raise_error(Vagrant::Errors::VirtualBoxKernelModuleNotLoaded) end it "raises an exception if virtualbox installation is incomplete" do allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:new). and_raise(Vagrant::Errors::VirtualBoxInstallIncomplete) expect { subject.usable?(true) }. to raise_error(Vagrant::Errors::VirtualBoxInstallIncomplete) end it "raises an exception if VBoxManage is not found" do allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:new). and_raise(Vagrant::Errors::VBoxManageNotFoundError) expect { subject.usable?(true) }. to raise_error(Vagrant::Errors::VBoxManageNotFoundError) end end describe "#driver" do it "is initialized" do allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:new).and_return(driver) expect(subject.driver).to be(driver) end end describe "#state" do it "returns not_created if no ID" do allow(machine).to receive(:id).and_return(nil) allow(machine).to receive(:data_dir).and_return(".vagrant") expect(subject.state.id).to eq(:not_created) end end describe "#ssh_info" do let(:result) { "127.0.0.1" } let(:exit_code) { 0 } let(:ssh_info) {{:host=>result,:port=>22}} let(:ssh) { double("ssh", guest_port: 22) } let(:config) { double("config", ssh: ssh) } before do allow(VagrantPlugins::ProviderVirtualBox::Driver::Meta).to receive(:new).and_return(driver) allow(machine).to receive(:action).with(:read_state).and_return(machine_state_id: :running) allow(machine).to receive(:data_dir).and_return(".vagrant") allow(driver).to receive(:uuid).and_return("1234") allow(driver).to receive(:read_state).and_return(:running) allow(driver).to receive(:ssh_port).and_return(22) allow(machine).to receive(:config).and_return(config) end it "returns nil if machine state is not running" do allow(driver).to receive(:read_state).and_return(:not_created) expect(subject.ssh_info).to eq(nil) end it "should receive a valid address" do allow(driver).to receive(:execute).with(:get_network_config).and_return(result) allow(driver).to receive(:read_guest_ip).and_return({"ip" => "127.0.0.1"}) expect(subject.ssh_info).to eq(ssh_info) end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_examples "a version 4.x virtualbox driver" do |options| before do raise ArgumentError, "Need virtualbox context to use these shared examples." if !(defined? vbox_context) end describe "read_dhcp_servers" do before { expect(subprocess).to receive(:execute). with("VBoxManage", "list", "dhcpservers", an_instance_of(Hash)). and_return(subprocess_result(stdout: output)) } context "with empty output" do let(:output) { "" } it "returns an empty list" do expect(subject.read_dhcp_servers).to eq([]) end end context "with a single dhcp server" do let(:output) { <<-OUTPUT.gsub(/^ */, '') NetworkName: HostInterfaceNetworking-vboxnet0 IP: 172.28.128.2 NetworkMask: 255.255.255.0 lowerIPAddress: 172.28.128.3 upperIPAddress: 172.28.128.254 Enabled: Yes OUTPUT } it "returns a list with one entry describing that server" do expect(subject.read_dhcp_servers).to eq([{ network_name: 'HostInterfaceNetworking-vboxnet0', network: 'vboxnet0', ip: '172.28.128.2', netmask: '255.255.255.0', lower: '172.28.128.3', upper: '172.28.128.254', }]) end end context "with a multiple dhcp servers" do let(:output) { <<-OUTPUT.gsub(/^ */, '') NetworkName: HostInterfaceNetworking-vboxnet0 IP: 172.28.128.2 NetworkMask: 255.255.255.0 lowerIPAddress: 172.28.128.3 upperIPAddress: 172.28.128.254 Enabled: Yes NetworkName: HostInterfaceNetworking-vboxnet1 IP: 10.0.0.2 NetworkMask: 255.255.255.0 lowerIPAddress: 10.0.0.3 upperIPAddress: 10.0.0.254 Enabled: Yes OUTPUT } it "returns a list with one entry for each server" do expect(subject.read_dhcp_servers).to eq([ {network_name: 'HostInterfaceNetworking-vboxnet0', network: 'vboxnet0', ip: '172.28.128.2', netmask: '255.255.255.0', lower: '172.28.128.3', upper: '172.28.128.254'}, {network_name: 'HostInterfaceNetworking-vboxnet1', network: 'vboxnet1', ip: '10.0.0.2', netmask: '255.255.255.0', lower: '10.0.0.3', upper: '10.0.0.254'}, ]) end end end describe "read_guest_property" do it "reads the guest property of the machine referenced by the UUID" do key = "/Foo/Bar" expect(subprocess).to receive(:execute). with("VBoxManage", "guestproperty", "get", uuid, key, an_instance_of(Hash)). and_return(subprocess_result(stdout: "Value: Baz\n")) expect(subject.read_guest_property(key)).to eq("Baz") end it "raises a virtualBoxGuestPropertyNotFound exception when the value is not set" do key = "/Not/There" expect(subprocess).to receive(:execute). with("VBoxManage", "guestproperty", "get", uuid, key, an_instance_of(Hash)). and_return(subprocess_result(stdout: "No value set!")) expect { subject.read_guest_property(key) }. to raise_error Vagrant::Errors::VirtualBoxGuestPropertyNotFound end end describe "read_guest_ip" do it "reads the guest property for the provided adapter number" do key = "/VirtualBox/GuestInfo/Net/1/V4/IP" expect(subprocess).to receive(:execute). with("VBoxManage", "guestproperty", "get", uuid, key, an_instance_of(Hash)). and_return(subprocess_result(stdout: "Value: 127.1.2.3")) value = subject.read_guest_ip(1) expect(value).to eq("127.1.2.3") end it "does not accept 0.0.0.0 as a valid IP address" do key = "/VirtualBox/GuestInfo/Net/1/V4/IP" expect(subprocess).to receive(:execute). with("VBoxManage", "guestproperty", "get", uuid, key, an_instance_of(Hash)). and_return(subprocess_result(stdout: "Value: 0.0.0.0")) expect { subject.read_guest_ip(1) }. to raise_error Vagrant::Errors::VirtualBoxGuestPropertyNotFound end end describe "read_host_only_interfaces" do before { expect(subprocess).to receive(:execute). with("VBoxManage", "list", "hostonlyifs", an_instance_of(Hash)). and_return(subprocess_result(stdout: output)) } context "with empty output" do let(:output) { "" } it "returns an empty list" do expect(subject.read_host_only_interfaces).to eq([]) end end context "with a single host only interface" do let(:output) { <<-OUTPUT.gsub(/^ */, '') Name: vboxnet0 GUID: 786f6276-656e-4074-8000-0a0027000000 DHCP: Disabled IPAddress: 172.28.128.1 NetworkMask: 255.255.255.0 IPV6Address: IPV6NetworkMaskPrefixLength: 0 HardwareAddress: 0a:00:27:00:00:00 MediumType: Ethernet Status: Up VBoxNetworkName: HostInterfaceNetworking-vboxnet0 OUTPUT } it "returns a list with one entry describing that interface" do expect(subject.read_host_only_interfaces).to eq([{ name: 'vboxnet0', ip: '172.28.128.1', netmask: '255.255.255.0', ipv6_prefix: '0', status: 'Up', display_name: 'HostInterfaceNetworking-vboxnet0', }]) end end context "with multiple host only interfaces" do let(:output) { <<-OUTPUT.gsub(/^ */, '') Name: vboxnet0 GUID: 786f6276-656e-4074-8000-0a0027000000 DHCP: Disabled IPAddress: 172.28.128.1 NetworkMask: 255.255.255.0 IPV6Address: IPV6NetworkMaskPrefixLength: 0 HardwareAddress: 0a:00:27:00:00:00 MediumType: Ethernet Status: Up VBoxNetworkName: HostInterfaceNetworking-vboxnet0 Name: vboxnet1 GUID: 5764a976-8479-8388-1245-8a0048080840 DHCP: Disabled IPAddress: 10.0.0.1 NetworkMask: 255.255.255.0 IPV6Address: IPV6NetworkMaskPrefixLength: 0 HardwareAddress: 0a:00:27:00:00:01 MediumType: Ethernet Status: Up VBoxNetworkName: HostInterfaceNetworking-vboxnet1 OUTPUT } 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', ipv6_prefix: "0", status: 'Up', display_name: 'HostInterfaceNetworking-vboxnet0'}, {name: 'vboxnet1', ip: '10.0.0.1', netmask: '255.255.255.0', ipv6_prefix: "0", status: 'Up', display_name: 'HostInterfaceNetworking-vboxnet1'}, ]) 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', display_name: 'HostInterfaceNetworking-vboxnet1' }]) end end end describe "remove_dhcp_server" do it "removes the dhcp server with the specified network name" do expect(subprocess).to receive(:execute). with("VBoxManage", "dhcpserver", "remove", "--netname", "HostInterfaceNetworking-vboxnet0", an_instance_of(Hash)). and_return(subprocess_result(stdout: '')) subject.remove_dhcp_server("HostInterfaceNetworking-vboxnet0") end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_5_x_examples.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_examples "a version 5.x virtualbox driver" do |options| before do raise ArgumentError, "Need virtualbox context to use these shared examples." if !(defined? vbox_context) end describe "#shared_folders" do let(:folders) { [{:name=>"folder", :hostpath=>"/Users/brian/vagrant-folder", :transient=>false, :SharedFoldersEnableSymlinksCreate=>true}]} let(:folders_automount) { [{:name=>"folder", :hostpath=>"/Users/brian/vagrant-folder", :transient=>false, :automount=>true, :SharedFoldersEnableSymlinksCreate=>true}]} let(:folders_disabled) { [{:name=>"folder", :hostpath=>"/Users/brian/vagrant-folder", :transient=>false, :SharedFoldersEnableSymlinksCreate=>false}]} it "enables SharedFoldersEnableSymlinksCreate if true" do expect(subprocess).to receive(:execute). with("VBoxManage", "setextradata", anything, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/folder", "1", {:env => {:LANG => "C"}, :notify=>[:stdout, :stderr]}). and_return(subprocess_result(exit_code: 0)) expect(subprocess).to receive(:execute). with("VBoxManage", "sharedfolder", "add", anything, "--name", "folder", "--hostpath", "/Users/brian/vagrant-folder", {:env => {:LANG => "C"}, :notify=>[:stdout, :stderr]}). and_return(subprocess_result(exit_code: 0)) subject.share_folders(folders) end it "enables automount if option is true" do expect(subprocess).to receive(:execute). with("VBoxManage", "setextradata", anything, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/folder", "1", {:env => {:LANG => "C"}, :notify=>[:stdout, :stderr]}). and_return(subprocess_result(exit_code: 0)) expect(subprocess).to receive(:execute). with("VBoxManage", "sharedfolder", "add", anything, "--name", "folder", "--hostpath", "/Users/brian/vagrant-folder", "--automount", {:env => {:LANG => "C"}, :notify=>[:stdout, :stderr]}). and_return(subprocess_result(exit_code: 0)) subject.share_folders(folders_automount) end it "disables SharedFoldersEnableSymlinksCreate if false" do expect(subprocess).to receive(:execute). with("VBoxManage", "sharedfolder", "add", anything, "--name", "folder", "--hostpath", "/Users/brian/vagrant-folder", {:env => {:LANG => "C"}, :notify=>[:stdout, :stderr]}). and_return(subprocess_result(exit_code: 0)) subject.share_folders(folders_disabled) end end describe "#set_mac_address" do let(:mac) { "00:00:00:00:00:00" } after { subject.set_mac_address(mac) } it "should modify vm and set mac address" do expect(subprocess).to receive(:execute).with("VBoxManage", "modifyvm", anything, "--macaddress1", mac, anything). and_return(subprocess_result(exit_code: 0)) end context "when mac address is falsey" do let(:mac) { nil } it "should modify vm and set mac address to automatic value" do expect(subprocess).to receive(:execute).with("VBoxManage", "modifyvm", anything, "--macaddress1", "auto", anything). and_return(subprocess_result(exit_code: 0)) end end end describe "#ssh_port" do let(:forwards) { [[1, "ssh", 2222, 22, "127.0.0.1"], [1, "ssh", 8080, 80, ""]] } before { allow(subject).to receive(:read_forwarded_ports).and_return(forwards) } it "should return the host port" do expect(subject.ssh_port(22)).to eq(2222) end context "when multiple matches are available" do let(:forwards) { [[1, "ssh", 2222, 22, "127.0.0.1"], [1, "", 2221, 22, ""]] } it "should choose localhost port forward" do expect(subject.ssh_port(22)).to eq(2222) end context "when multiple named matches are available" do let(:forwards) { [[1, "ssh", 2222, 22, "127.0.0.1"], [1, "SSH", 2221, 22, "127.0.0.1"]] } it "should choose lowercased name forward" do expect(subject.ssh_port(22)).to eq(2222) end end end context "when only ports are defined" do let(:forwards) { [[1, "", 2222, 22, ""]] } it "should return the host port" do expect(subject.ssh_port(22)).to eq(2222) end end context "when no matches are available" do let(:forwards) { [] } it "should return nil" do expect(subject.ssh_port(22)).to be_nil end end end describe "#read_guest_ip" do context "when guest ip ends in .1" do before do key = "/VirtualBox/GuestInfo/Net/1/V4/IP" expect(subprocess).to receive(:execute). with("VBoxManage", "guestproperty", "get", uuid, key, an_instance_of(Hash)). and_return(subprocess_result(stdout: "Value: 172.28.128.1")) end it "should raise an error" do expect { subject.read_guest_ip(1) }.to raise_error(Vagrant::Errors::VirtualBoxGuestPropertyNotFound) end end end describe "#valid_ip_address?" do context "when ip is 0.0.0.0" do let(:ip) { "0.0.0.0" } it "should be false" do result = subject.send(:valid_ip_address?, ip) expect(result).to be(false) end end context "when ip address is nil" do let(:ip) { nil } it "should be false" do result = subject.send(:valid_ip_address?, ip) expect(result).to be(false) end end end end ================================================ FILE: test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_6_x_examples.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_examples "a version 6.x virtualbox driver" do |options| before do raise ArgumentError, "Need virtualbox context to use these shared examples." if !(defined? vbox_context) end end ================================================ FILE: test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_7_x_examples.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_examples "a version 7.x virtualbox driver" do |opts| before do raise ArgumentError, "Need virtualbox context to use these shared examples." if !(defined? vbox_context) end describe "#use_host_only_nets?" do context "when platform is darwin" do before do allow(Vagrant::Util::Platform).to receive(:darwin?).and_return(true) end context "when virtualbox version is less than 7" do before do allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). to receive(:version).and_return("6.0.28") end it "should return false" do expect(subject.send(:use_host_only_nets?)).to be(false) end end context "when virtualbox version is greater than 7" do before do allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). to receive(:version).and_return("7.0.2") end it "should return true" do expect(subject.send(:use_host_only_nets?)).to be(true) end end context "when virtualbox version is equal to 7" do before do allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). to receive(:version).and_return("7.0.0") end it "should return true" do expect(subject.send(:use_host_only_nets?)).to be(true) end end end context "when platform is not darwin" do before do allow(Vagrant::Util::Platform).to receive(:darwin?).and_return(false) end context "when virtualbox version is less than 7" do before do allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). to receive(:version).and_return("6.0.28") end it "should return false" do expect(subject.send(:use_host_only_nets?)).to be(false) end end context "when virtualbox version is greater than 7" do before do allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). to receive(:version).and_return("7.0.2") end it "should return false" do expect(subject.send(:use_host_only_nets?)).to be(false) end end context "when virtualbox version is equal to 7" do before do allow_any_instance_of(VagrantPlugins::ProviderVirtualBox::Driver::Meta). to receive(:version).and_return("7.0.0") end it "should return false" do expect(subject.send(:use_host_only_nets?)).to be(false) end end end end describe "#read_bridged_interfaces" do let(:bridgedifs) { VBOX_BRIDGEDIFS } before do allow(subject).to receive(:execute).and_call_original expect(subject). to receive(:execute). with("list", "bridgedifs"). and_return(bridgedifs) end context "when hostonlynets are not enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(false) end it "should return all interfaces in list" do expect(subject.read_bridged_interfaces.size).to eq(5) end it "should not read host only networks" do expect(subject).not_to receive(:read_host_only_networks) subject.read_bridged_interfaces end end context "when hostonlynets are enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(true) end it "should return all interfaces in list" do expect(subject).to receive(:read_host_only_networks).and_return([]) expect(subject.read_bridged_interfaces.size).to eq(5) end context "when hostonly networks are defined" do before do expect(subject). to receive(:execute). with("list", "hostonlynets", any_args). and_return(VBOX_HOSTONLYNETS) end it "should not return all interfaces in list" do expect(subject.read_bridged_interfaces.size).to_not eq(5) end it "should not include hostonly network devices" do expect( subject.read_bridged_interfaces.any? { |int| int[:name].start_with?("bridge") } ).to be(false) end end end context "when address is empty" do let(:bridgedifs) { VBOX_BRIDGEDIFS.sub("0.0.0.0", "") } it "should not raise an error" do expect { subject.read_bridged_interfaces }.to_not raise_error end end end describe "#delete_unused_host_only_networks" do context "when hostonlynets are not enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(false) end it "should remove host only interfaces" do expect(subject).to receive(:execute).with("list", "hostonlyifs", any_args).and_return("") expect(subject).to receive(:execute).with("list", "vms", any_args).and_return("") subject.delete_unused_host_only_networks end it "should not read host only networks" do expect(subject).to receive(:execute).with("list", "hostonlyifs", any_args).and_return("") expect(subject).to receive(:execute).with("list", "vms", any_args).and_return("") expect(subject).not_to receive(:read_host_only_networks) subject.delete_unused_host_only_networks end end context "when hostonlynets are enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(true) allow(subject).to receive(:read_host_only_networks).and_return([]) allow(subject).to receive(:execute).with("list", "vms", any_args).and_return("") end it "should not read host only interfaces" do expect(subject).not_to receive(:execute).with("list", "hostonlyifs", any_args) subject.delete_unused_host_only_networks end context "when no host only networks are defined" do before do expect(subject).to receive(:read_host_only_networks).and_return([]) end it "should not list vms" do expect(subject).not_to receive(:execute).with("list", "vms", any_args) subject.delete_unused_host_only_networks end end context "when host only networks are defined" do before do expect(subject). to receive(:read_host_only_networks). and_return([{name: "vagrantnet-vbox-1"}]) end context "when no vms are using the network" do before do expect(subject).to receive(:execute).with("list", "vms", any_args).and_return("") end it "should delete the network" do expect(subject). to receive(:execute). with("hostonlynet", "remove", "--name", "vagrantnet-vbox-1", any_args) subject.delete_unused_host_only_networks end end context "when vms are using the network" do before do expect(subject). to receive(:execute). with("list", "vms", any_args). and_return(%("VM_NAME" {VM_ID})) expect(subject). to receive(:execute). with("showvminfo", "VM_ID", any_args). and_return(%(hostonly-network="vagrantnet-vbox-1")) end it "should not delete the network" do expect(subject).not_to receive(:execute).with("hostonlynet", "remove", any_args) subject.delete_unused_host_only_networks end end end end end describe "#enable_adapters" do let(:adapters) { [{hostonly: "hostonlynetwork", adapter: 1}, {bridge: "eth0", adapter: 2}] } before do allow(subject).to receive(:execute).with("modifyvm", any_args) end context "when hostonlynets are not enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(false) end it "should only call modifyvm once" do expect(subject).to receive(:execute).with("modifyvm", any_args).once subject.enable_adapters(adapters) end it "should configure host only network using hostonlyadapter" do expect(subject).to receive(:execute) { |*args| expect(args.first).to eq("modifyvm") expect(args).to include("--hostonlyadapter1") true } subject.enable_adapters(adapters) end end context "when hostonlynets are enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(true) end it "should call modifyvm twice" do expect(subject).to receive(:execute).with("modifyvm", any_args).twice subject.enable_adapters(adapters) end it "should configure host only network using hostonlynet" do expect(subject).to receive(:execute).once expect(subject).to receive(:execute) { |*args| expect(args.first).to eq("modifyvm") expect(args).to include("--host-only-net1") true } subject.enable_adapters(adapters) end end end describe "#create_host_only_network" do let(:options) { { adapter_ip: "127.0.0.1", netmask: "255.255.255.0" } } context "when hostonlynets are disabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(false) end it "should create using hostonlyif" do expect(subject). to receive(:execute). with("hostonlyif", "create", any_args). and_return("Interface 'host_only' was successfully created") expect(subject). to receive(:execute). with("hostonlyif", "ipconfig", "host_only", any_args) subject.create_host_only_network(options) end end context "when hostonlynets are enabled" do let(:prefix) { described_class.const_get(:HOSTONLY_NAME_PREFIX) } before do allow(subject).to receive(:use_host_only_nets?).and_return(true) allow(subject).to receive(:read_host_only_networks).and_return([]) end it "should create using hostonlynet" do expect(subject). to receive(:execute). with("hostonlynet", "add", "--name", prefix + "1", "--netmask", options[:netmask], "--lower-ip", "127.0.0.0", "--upper-ip", "127.0.0.0", any_args) subject.create_host_only_network(options) end context "when other host only networks exist" do before do expect(subject). to receive(:read_host_only_networks). and_return(["custom", prefix + "1", prefix + "20"].map { |n| {name: n} }) end it "should create network with incremented name" do expect(subject). to receive(:execute). with("hostonlynet", "add", "--name", prefix + "21", any_args) subject.create_host_only_network(options) end end context "when dhcp information is included" do let(:options) { { type: :dhcp, dhcp_lower: "127.0.0.1", dhcp_upper: "127.0.1.200", netmask: "255.255.240.0" } } it "should set DHCP range" do expect(subject). to receive(:execute). with("hostonlynet", "add", "--name", anything, "--netmask", options[:netmask], "--lower-ip", options[:dhcp_lower], "--upper-ip", options[:dhcp_upper], any_args) subject.create_host_only_network(options) end end end end describe "#reconfig_host_only" do let(:interface) { {name: "iname", ipv6: "VALUE"} } context "when hostonlynets are disabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(false) end it "should apply ipv6 update" do expect(subject).to receive(:execute).with("hostonlyif", "ipconfig", interface[:name], "--ipv6", interface[:ipv6], any_args) subject.reconfig_host_only(interface) end end context "when hostonlynets are enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(true) end it "should do nothing" do expect(subject).not_to receive(:execute) subject.reconfig_host_only(interface) end end end describe "#remove_dhcp_server" do let(:dhcp_name) { double(:dhcp_name) } context "when hostonlynets are disabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(false) end it "should remove the dhcp server" do expect(subject).to receive(:execute).with("dhcpserver", "remove", "--netname", dhcp_name, any_args) subject.remove_dhcp_server(dhcp_name) end end context "when hostonlynets are enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(true) end it "should do nothing" do expect(subject).not_to receive(:execute) subject.remove_dhcp_server(dhcp_name) end end end describe "#create_dhcp_server" do let(:network) { double("network") } let(:options) { { dhcp_ip: "127.0.0.1", netmask: "255.255.255.0", dhcp_lower: "127.0.0.2", dhcp_upper: "127.0.0.200" } } context "when hostonlynets is diabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(false) end it "should create a dhcp server" do expect(subject).to receive(:execute).with("dhcpserver", "add", "--ifname", network, "--ip", options[:dhcp_ip], any_args) subject.create_dhcp_server(network, options) end end context "when hostonlynets is enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(true) end it "should do nothing" do expect(subject).not_to receive(:execute) subject.create_dhcp_server(network, options) end end end describe "#read_host_only_interfaces" do context "when hostonlynets is diabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(false) allow(subject).to receive(:execute).and_return("") end it "should list hostonlyifs" do expect(subject).to receive(:execute).with("list", "hostonlyifs", any_args).and_return("") subject.read_host_only_interfaces end it "should not call read_host_only_networks" do expect(subject).not_to receive(:read_host_only_networks) subject.read_host_only_interfaces end end context "when hostonlynets is enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(true) allow(subject).to receive(:execute).with("list", "hostonlynets", any_args). and_return(VBOX_HOSTONLYNETS) end it "should call read_host_only_networks" do expect(subject).to receive(:read_host_only_networks).and_return([]) subject.read_host_only_interfaces end it "should return defined networks" do expect(subject.read_host_only_interfaces.size).to eq(2) end it "should add compat information to network entries" do result = subject.read_host_only_interfaces expect(result.first[:netmask]).to eq(result.first[:networkmask]) expect(result.first[:status]).to eq("Up") end it "should assign the address as the first in the subnet" do result = subject.read_host_only_interfaces expect(result.first[:ip]).to eq(IPAddr.new(result.first[:lowerip]).succ.to_s) end context "when dhcp range is set" do before do allow(subject).to receive(:execute).with("list", "hostonlynets", any_args). and_return(VBOX_RANGE_HOSTONLYNETS) end it "should assign the address as the first in the dhcp range" do result = subject.read_host_only_interfaces expect(result.first[:ip]).to eq(result.first[:lowerip]) end end end end describe "#read_host_only_networks" do before do allow(subject).to receive(:execute).with("list", "hostonlynets", any_args). and_return(VBOX_HOSTONLYNETS) end it "should return defined networks" do expect(subject.send(:read_host_only_networks).size).to eq(2) end it "should return expected network information" do result = subject.send(:read_host_only_networks) expect(result.first[:name]).to eq("vagrantnet-vbox1") expect(result.first[:lowerip]).to eq("192.168.61.0") expect(result.first[:networkmask]).to eq("255.255.255.0") expect(result.last[:name]).to eq("vagrantnet-vbox2") expect(result.last[:lowerip]).to eq("192.168.22.0") expect(result.last[:networkmask]).to eq("255.255.255.0") end end describe "#read_network_interfaces" do before do allow(subject) .to receive(:execute). with("showvminfo", any_args). and_return(VBOX_GUEST_HOSTONLYVNETS_INFO) end context "when hostonlynets is disabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(false) end it "should return two interfaces" do valid_interfaces = subject.read_network_interfaces.find_all { |k, v| v[:type] != :none } expect(valid_interfaces.size).to eq(2) end it "should include a nat type" do expect(subject.read_network_interfaces.detect { |_, v| v[:type] == :nat }).to be end it "should include a hostonlynetwork type with no information" do expect(subject.read_network_interfaces[2]).to eq({type: :hostonlynetwork}) end end context "when hostonlynets is enabled" do before do allow(subject).to receive(:use_host_only_nets?).and_return(true) end it "should return two interfaces" do valid_interfaces = subject.read_network_interfaces.find_all { |k, v| v[:type] != :none } expect(valid_interfaces.size).to eq(2) end it "should include a nat type" do expect(subject.read_network_interfaces.detect { |_, v| v[:type] == :nat }).to be end it "should include a hostonly type" do expect(subject.read_network_interfaces.detect { |_, v| v[:type] == :hostonly }).to be end it "should not include a hostonlynetwork type" do expect(subject.read_network_interfaces.detect { |_, v| v[:type] == :hostonlynetwork }).to_not be end it "should include the hostonly network name" do hostonly = subject.read_network_interfaces.values.detect { |v| v[:type] == :hostonly } expect(hostonly).to be expect(hostonly[:hostonly]).to eq("vagrantnet-vbox1") end end end end VBOX_VMCONFIG_FILE=%( ) VBOX_BRIDGEDIFS=%(Name: en1: Wi-Fi (AirPort) GUID: 00000000-0000-0000-0000-000000000001 DHCP: Disabled IPAddress: 10.0.0.49 NetworkMask: 255.255.255.0 IPV6Address: IPV6NetworkMaskPrefixLength: 0 HardwareAddress: xx:xx:xx:xx:xx:01 MediumType: Ethernet Wireless: Yes Status: Up VBoxNetworkName: HostInterfaceNetworking-en1 Name: en0: Ethernet GUID: 00000000-0000-0000-0000-000000000002 DHCP: Disabled IPAddress: 0.0.0.0 NetworkMask: 0.0.0.0 IPV6Address: IPV6NetworkMaskPrefixLength: 0 HardwareAddress: xx:xx:xx:xx:xx:02 MediumType: Ethernet Wireless: No Status: Up VBoxNetworkName: HostInterfaceNetworking-en0 Name: bridge100 GUID: 00000000-0000-0000-0000-000000000003 DHCP: Disabled IPAddress: 192.168.61.1 NetworkMask: 255.255.255.0 IPV6Address: IPV6NetworkMaskPrefixLength: 0 HardwareAddress: xx:xx:xx:xx:xx:03 MediumType: Ethernet Wireless: No Status: Up VBoxNetworkName: HostInterfaceNetworking-bridge100 Name: en2: Thunderbolt 1 GUID: 00000000-0000-0000-0000-000000000004 DHCP: Disabled IPAddress: 0.0.0.0 NetworkMask: 0.0.0.0 IPV6Address: IPV6NetworkMaskPrefixLength: 0 HardwareAddress: xx:xx:xx:xx:xx:04 MediumType: Ethernet Wireless: No Status: Up VBoxNetworkName: HostInterfaceNetworking-en2 Name: bridge101 GUID: 00000000-0000-0000-0000-000000000005 DHCP: Disabled IPAddress: 192.168.22.1 NetworkMask: 255.255.255.0 IPV6Address: IPV6NetworkMaskPrefixLength: 0 HardwareAddress: xx:xx:xx:xx:xx:05 MediumType: Ethernet Wireless: No Status: Up VBoxNetworkName: HostInterfaceNetworking-bridge101) VBOX_HOSTONLYNETS=%(Name: vagrantnet-vbox1 GUID: 10000000-0000-0000-0000-000000000000 State: Enabled NetworkMask: 255.255.255.0 LowerIP: 192.168.61.0 UpperIP: 192.168.61.0 VBoxNetworkName: hostonly-vagrantnet-vbox1 Name: vagrantnet-vbox2 GUID: 20000000-0000-0000-0000-000000000000 State: Enabled NetworkMask: 255.255.255.0 LowerIP: 192.168.22.0 UpperIP: 192.168.22.0 VBoxNetworkName: hostonly-vagrantnet-vbox2) VBOX_RANGE_HOSTONLYNETS=%(Name: vagrantnet-vbox1 GUID: 10000000-0000-0000-0000-000000000000 State: Enabled NetworkMask: 255.255.255.0 LowerIP: 192.168.61.10 UpperIP: 192.168.61.100 VBoxNetworkName: hostonly-vagrantnet-vbox1 Name: vagrantnet-vbox2 GUID: 20000000-0000-0000-0000-000000000000 State: Enabled NetworkMask: 255.255.255.0 LowerIP: 192.168.22.0 UpperIP: 192.168.22.0 VBoxNetworkName: hostonly-vagrantnet-vbox2) VBOX_GUEST_HOSTONLYVNETS_INFO=%( natnet1="nat" macaddress1="080027BB1475" cableconnected1="on" nic1="nat" nictype1="82540EM" nicspeed1="0" mtu="0" sockSnd="64" sockRcv="64" tcpWndSnd="64" tcpWndRcv="64" Forwarding(0)="ssh,tcp,127.0.0.1,2222,,22" hostonly-network2="vagrantnet-vbox1" macaddress2="080027FBC15B" cableconnected2="on" nic2="hostonlynetwork" nictype2="82540EM" nicspeed2="0" nic3="none" nic4="none" nic5="none" nic6="none" nic7="none" nic8="none" ) ================================================ FILE: test/unit/plugins/providers/virtualbox/synced_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "vagrant" require Vagrant.source_root.join("test/unit/base") require Vagrant.source_root.join("plugins/providers/virtualbox/config") require Vagrant.source_root.join("plugins/providers/virtualbox/synced_folder") describe VagrantPlugins::ProviderVirtualBox::SyncedFolder do include_context "unit" let(:vm_config) do double("vm_config").tap do |vm_config| allow(vm_config).to receive(:allow_fstab_modification).and_return(true) end end let(:machine_config) do double("machine_config").tap do |top_config| allow(top_config).to receive(:vm).and_return(vm_config) end end let(:machine) do double("machine").tap do |m| allow(m).to receive(:provider_config).and_return(VagrantPlugins::ProviderVirtualBox::Config.new) allow(m).to receive(:provider_name).and_return(:virtualbox) allow(m).to receive(:config).and_return(machine_config) end end let(:folders) { {"/folder"=> {:SharedFoldersEnableSymlinksCreate=>true, :guestpath=>"/folder", :hostpath=>"/Users/brian/vagrant-folder", :automount=>false, :disabled=>false, :__vagrantfile=>true}} } subject { described_class.new } before do machine.provider_config.finalize! end describe "#usable?" do it "should be with virtualbox provider" do allow(machine).to receive(:provider_name).and_return(:virtualbox) expect(subject).to be_usable(machine) end it "should not be with another provider" do allow(machine).to receive(:provider_name).and_return(:vmware_fusion) expect(subject).not_to be_usable(machine) end it "should not be usable if not functional vboxsf" do machine.provider_config.functional_vboxsf = false expect(subject).to_not be_usable(machine) end end describe "#enable" do let(:ui){ Vagrant::UI::Silent.new } let(:guest) { double("guest") } let(:no_guestpath_folder) { {"/no_guestpath_folder"=> {:SharedFoldersEnableSymlinksCreate=>false, :guestpath=>nil, :hostpath=>"/Users/brian/vagrant-folder", :automount=>false, :disabled=>true, :__vagrantfile=>true}} } before do allow(subject).to receive(:share_folders).and_return(true) allow(machine).to receive(:ui).and_return(ui) allow(machine).to receive(:ssh_info).and_return({:username => "test"}) allow(machine).to receive(:guest).and_return(guest) end end describe "#prepare" do let(:driver) { double("driver") } let(:provider) { double("driver", driver: driver) } let(:folders_disabled) { {"/folder"=> {:SharedFoldersEnableSymlinksCreate=>false, :guestpath=>"/folder", :hostpath=>"/Users/brian/vagrant-folder", :automount=>false, :disabled=>false, :__vagrantfile=>true}} } let(:folders_automount) { {"/folder"=> {:SharedFoldersEnableSymlinksCreate=>true, :guestpath=>"/folder", :hostpath=>"/Users/brian/vagrant-folder", :disabled=>false, :automount=>true, :__vagrantfile=>true}} } let(:folders_nosymvar) { {"/folder"=> {:guestpath=>"/folder", :hostpath=>"/Users/brian/vagrant-folder", :automount=>false, :disabled=>false, :__vagrantfile=>true}} } before do allow(machine).to receive(:provider).and_return(provider) allow(machine).to receive(:env) allow(subject).to receive(:display_symlink_create_warning) end it "should prepare and share the folders" do expect(driver).to receive(:share_folders).with([{:name=>"folder", :hostpath=>"/Users/brian/vagrant-folder", :transient=>false, :automount=>false, :SharedFoldersEnableSymlinksCreate=>true}]) subject.prepare(machine, folders, nil) end it "should prepare and share the folders without symlinks enabled" do expect(driver).to receive(:share_folders).with([{:name=>"folder", :hostpath=>"/Users/brian/vagrant-folder", :transient=>false, :automount=>false, :SharedFoldersEnableSymlinksCreate=>false}]) subject.prepare(machine, folders_disabled, nil) end it "should prepare and share the folders without symlinks enabled with env var set" do stub_env('VAGRANT_DISABLE_VBOXSYMLINKCREATE'=>'1') expect(driver).to receive(:share_folders).with([{:name=>"folder", :hostpath=>"/Users/brian/vagrant-folder", :transient=>false, :automount=>false, :SharedFoldersEnableSymlinksCreate=>false}]) subject.prepare(machine, folders_nosymvar, nil) end it "should prepare and share the folders and override symlink setting" do stub_env('VAGRANT_DISABLE_VBOXSYMLINKCREATE'=>'1') expect(driver).to receive(:share_folders).with([{:name=>"folder", :hostpath=>"/Users/brian/vagrant-folder", :transient=>false, :automount=>false, :SharedFoldersEnableSymlinksCreate=>true}]) subject.prepare(machine, folders, nil) end it "should prepare and share the folders with automount enabled" do expect(driver).to receive(:share_folders).with([{:name=>"folder", :hostpath=>"/Users/brian/vagrant-folder", :transient=>false, :SharedFoldersEnableSymlinksCreate=>true, :automount=>true}]) subject.prepare(machine, folders_automount, nil) end end describe "#os_friendly_id" do it "should not replace normal chars" do expect(subject.send(:os_friendly_id, 'perfectly_valid0_name')).to eq('perfectly_valid0_name') end it "should replace spaces" do expect(subject.send(:os_friendly_id, 'Program Files')).to eq('Program_Files') end it "should replace leading underscore" do expect(subject.send(:os_friendly_id, '_vagrant')).to eq('vagrant') end it "should replace slash" do expect(subject.send(:os_friendly_id, 'va/grant')).to eq('va_grant') end it "should replace leading underscore and slash" do expect(subject.send(:os_friendly_id, '/vagrant')).to eq('vagrant') end it "should replace backslash" do expect(subject.send(:os_friendly_id, 'foo\\bar')).to eq('foo_bar') end end describe "#share_folders" do let(:folders){ {'folder1' => {hostpath: '/vagrant', transient: true}, 'folder2' => {hostpath: '/vagrant2', transient: false}} } let(:symlink_create_disable){ nil } let(:driver){ double("driver") } before do allow(subject).to receive(:display_symlink_create_warning) allow(machine).to receive(:env) allow(subject).to receive(:driver).and_return(driver) allow(driver).to receive(:share_folders) allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with("VAGRANT_DISABLE_VBOXSYMLINKCREATE").and_return(symlink_create_disable) end it "should only add transient folder" do expect(driver).to receive(:share_folders).with(any_args) do |defs| expect(defs.size).to eq(1) end subject.send(:share_folders, machine, folders, true) end it "should display symlink create warning" do expect(subject).to receive(:display_symlink_create_warning) subject.send(:share_folders, machine, folders, true) end context "with create symlink globally disabled" do let(:symlink_create_disable){ "1" } it "should not enable option within definitions" do expect(driver).to receive(:share_folders).with(any_args) do |defs| expect(defs.first[:SharedFoldersEnableSymlinksCreate]).to be(false) end subject.send(:share_folders, machine, folders, true) end it "should not display symlink warning" do expect(subject).not_to receive(:display_symlink_create_warning) subject.send(:share_folders, machine, folders, true) end end end describe "#display_symlink_create_warning" do let(:env){ double("env", ui: Vagrant::UI::Silent.new, data_dir: double("data_dir")) } let(:gate_file){ double("gate") } before{ allow(gate_file).to receive(:to_path).and_return("PATH") } after{ subject.send(:display_symlink_create_warning, env) } context "gate file does not exist" do before do allow(env.data_dir).to receive(:join).and_return(gate_file) allow(gate_file).to receive(:exist?).and_return(false) allow(FileUtils).to receive(:touch) end it "should create file" do expect(FileUtils).to receive(:touch).with("PATH") end it "should output warning to user" do expect(env.ui).to receive(:warn).and_call_original end end context "gate file does exist" do before do allow(env.data_dir).to receive(:join).and_return(gate_file) allow(gate_file).to receive(:exist?).and_return(true) allow(FileUtils).to receive(:touch) end it "should not create/update file" do expect(FileUtils).not_to receive(:touch).with("PATH") end it "should not output warning to user" do expect(env.ui).not_to receive(:warn).and_call_original end end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/cap/guest/alpine/ansible_install_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../../base" require_relative "../shared/pip_ansible_install_examples" require Vagrant.source_root.join("plugins/provisioners/ansible/cap/guest/alpine/ansible_install") describe VagrantPlugins::Ansible::Cap::Guest::Alpine::AnsibleInstall do include_context "unit" subject { VagrantPlugins::Ansible::Cap::Guest::Alpine::AnsibleInstall } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute).and_return(true) end describe "#pip_setup" do it "install required alpine packages for pip" do expect(communicator).to receive(:sudo).once.ordered. with("apk add --update --no-cache python3") expect(communicator).to receive(:sudo).once.ordered. with("if [ ! -e /usr/bin/python ]; then ln -sf python3 /usr/bin/python ; fi") expect(communicator).to receive(:sudo).once.ordered. with("apk add --update --no-cache --virtual .build-deps python3-dev libffi-dev openssl-dev build-base") subject.pip_setup(machine) end end describe "#ansible_install" do it_behaves_like "Ansible setup via pip" describe "when install_mode is :default (or unknown)" do it "installs ansible with 'apk' package manager" do expect(communicator).to receive(:sudo).once.ordered. with("apk add --update --no-cache python3 ansible") expect(communicator).to receive(:sudo).once.ordered. with("if [ ! -e /usr/bin/python ]; then ln -sf python3 /usr/bin/python ; fi") expect(communicator).to receive(:sudo).once.ordered. with("if [ ! -e /usr/bin/pip ]; then ln -sf pip3 /usr/bin/pip ; fi") subject.ansible_install(machine, :default, "", "", "") end end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/cap/guest/arch/ansible_install_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../../base" require_relative "../shared/pip_ansible_install_examples" require Vagrant.source_root.join("plugins/provisioners/ansible/cap/guest/arch/ansible_install") describe VagrantPlugins::Ansible::Cap::Guest::Arch::AnsibleInstall do include_context "unit" subject { VagrantPlugins::Ansible::Cap::Guest::Arch::AnsibleInstall } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute).and_return(true) end describe "#pip_setup" do it "install required Arch packages and call Cap::Guest::Pip::get_pip" do pip_install_cmd = "foo" expect(communicator).to receive(:sudo).once.ordered. with("pacman -Syy --noconfirm") expect(communicator).to receive(:sudo).once.ordered. with("pacman -S --noconfirm base-devel curl git python") expect(VagrantPlugins::Ansible::Cap::Guest::Pip).to receive(:get_pip).once.ordered. with(machine, pip_install_cmd) subject.pip_setup(machine, pip_install_cmd) end end describe "#ansible_install" do it_behaves_like "Ansible setup via pip" describe "when install_mode is :default (or unknown)" do it "installs ansible with 'pacman' package manager" do expect(communicator).to receive(:sudo).once.ordered. with("pacman -Syy --noconfirm") expect(communicator).to receive(:sudo).once.ordered. with("pacman -S --noconfirm ansible") subject.ansible_install(machine, :default, "", "", "") end end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/cap/guest/debian/ansible_install_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../../base" require_relative "../shared/pip_ansible_install_examples" require Vagrant.source_root.join("plugins/provisioners/ansible/cap/guest/debian/ansible_install") describe VagrantPlugins::Ansible::Cap::Guest::Debian::AnsibleInstall do include_context "unit" subject { VagrantPlugins::Ansible::Cap::Guest::Debian::AnsibleInstall } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute).and_return(true) allow(communicator).to receive(:test).and_return(false) end describe "#ansible_install" do it_behaves_like "Ansible setup via pip on Debian-based systems" describe "when install_mode is :default (or unknown)" do it "installs ansible with apt package manager" do install_backports_if_wheezy_release = < /etc/apt/sources.list.d/wheezy-backports.list fi INLINE_CRIPT expect(communicator).to receive(:sudo).once.ordered.with(install_backports_if_wheezy_release) expect(communicator).to receive(:sudo).once.ordered.with("apt-get update -y -qq") expect(communicator).to receive(:sudo).once.ordered.with("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --option \"Dpkg::Options::=--force-confold\" ansible") subject.ansible_install(machine, :default, "", "", "") end end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/cap/guest/freebsd/ansible_install_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../../base" require_relative "../shared/pip_ansible_install_examples" require Vagrant.source_root.join("plugins/provisioners/ansible/cap/guest/freebsd/ansible_install") describe VagrantPlugins::Ansible::Cap::Guest::FreeBSD::AnsibleInstall do include_context "unit" subject { VagrantPlugins::Ansible::Cap::Guest::FreeBSD::AnsibleInstall } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute).and_return(true) end describe "#ansible_install" do it_behaves_like "Ansible setup via pip is not implemented" describe "when install_mode is :default (or unknown)" do it "installs ansible with 'pkg' package manager" do expect(communicator).to receive(:sudo).with("pkg install -qy py37-ansible") subject.ansible_install(machine, :default, "", "", "") end end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/cap/guest/pip/pip_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../../base" require Vagrant.source_root.join("plugins/provisioners/ansible/cap/guest/pip/pip") describe VagrantPlugins::Ansible::Cap::Guest::Pip do include_context "unit" subject { VagrantPlugins::Ansible::Cap::Guest::Pip } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute).and_return(true) end describe "#get_pip" do describe "when no pip_install_cmd argument is provided" do it "installs pip using the default command" do expect(communicator).to receive(:execute). with("curl https://bootstrap.pypa.io/get-pip.py | sudo python") subject.get_pip(machine) end end describe "when pip_install_cmd argument is provided" do it "runs the supplied argument instead of default" do pip_install_cmd = "foo" expect(communicator).to receive(:execute).with(pip_install_cmd) subject.get_pip(machine, pip_install_cmd) end it "installs pip using the default command if the argument is empty" do pip_install_cmd = "" expect(communicator).to receive(:execute). with("curl https://bootstrap.pypa.io/get-pip.py | sudo python") subject.get_pip(machine, pip_install_cmd) end it "installs pip using the default command if the argument is nil" do expect(communicator).to receive(:execute). with("curl https://bootstrap.pypa.io/get-pip.py | sudo python") subject.get_pip(machine, nil) end end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/cap/guest/redhat/ansible_install_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../../base" require Vagrant.source_root.join("plugins/provisioners/ansible/cap/guest/redhat/ansible_install") describe VagrantPlugins::Ansible::Cap::Guest::RedHat::AnsibleInstall do include_context "unit" subject { VagrantPlugins::Ansible::Cap::Guest::RedHat::AnsibleInstall } let(:iso_env) do env = isolated_environment env.vagrantfile("") env.create_vagrant_env end let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } let(:dist) { ".el8" } let(:epel) { 1 } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute).with("rpm -E %dist").and_yield(:stdout, dist) end describe "#ansible_epel_download_url" do it "returns the correct EPEL download URL for RHEL-like versions below 10" do expect(subject.ansible_epel_download_url(machine)).to eq("https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm") end context "for RHEL-like versions 10 and above" do let(:dist) { ".el10" } it "returns the correct EPEL download URL" do out = subject.ansible_epel_download_url(machine) expect(out).to eq("https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm") end end end describe "#ansible_rpm_install" do before do expect(communicator).to receive(:test).with("/usr/bin/which -s dnf").and_return(false) expect(communicator).to receive(:execute).with("yum repolist epel | grep -q epel", error_check: false).and_return(epel) expect(communicator).to receive(:sudo).with("yum -y --enablerepo=epel install ansible") end it "installs ansible package, when epel is not installed" do expect(communicator).to receive(:sudo).with("sudo rpm -i https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm") subject.ansible_rpm_install(machine) end context "when the EPEL repository is already installed" do let(:epel) { 0 } it "installs ansible package" do expect(communicator).to_not receive(:sudo).with("sudo rpm -i https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm") subject.ansible_rpm_install(machine) end end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/cap/guest/shared/pip_ansible_install_examples.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_examples_for "Ansible setup via pip" do describe "when install_mode is :pip" do before { allow(communicator).to receive(:test) } it "installs pip and calls Cap::Guest::Pip::pip_install" do expect(communicator).to receive(:sudo).at_least(1).times.ordered expect(VagrantPlugins::Ansible::Cap::Guest::Pip).to receive(:pip_install).once.ordered. with(machine, "ansible", anything, anything, true) subject.ansible_install(machine, :pip, "", "", "") end end describe "when install_mode is :pip_args_only" do before { allow(communicator).to receive(:test) } it "installs pip and calls Cap::Guest::Pip::pip_install with 'pip_args' parameter" do pip_args = "-r /vagrant/requirements.txt" expect(communicator).to receive(:sudo).at_least(1).times.ordered expect(VagrantPlugins::Ansible::Cap::Guest::Pip).to receive(:pip_install).with(machine, "", "", pip_args, false).ordered subject.ansible_install(machine, :pip_args_only, "", pip_args, "") end end end shared_examples_for "Ansible setup via pip on Debian-based systems" do describe "installs required Debian packages and..." do before { allow(communicator).to receive(:test) } pip_install_cmd = "foo" it "calls Cap::Guest::Pip::get_pip with 'pip' install_mode" do expect(communicator).to receive(:sudo). with("apt-get update -y -qq") expect(communicator).to receive(:sudo). with("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --option \"Dpkg::Options::=--force-confold\" build-essential curl git libssl-dev libffi-dev python-dev") expect(communicator).to receive(:sudo). with("pip install --upgrade ansible") subject.ansible_install(machine, :pip, "", "", pip_install_cmd) end it "calls Cap::Guest::Pip::get_pip with 'pip_args_only' install_mode" do expect(communicator).to receive(:sudo). with("apt-get update -y -qq") expect(communicator).to receive(:sudo). with("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --option \"Dpkg::Options::=--force-confold\" build-essential curl git libssl-dev libffi-dev python-dev") expect(communicator).to receive(:sudo). with("pip install") subject.ansible_install(machine, :pip_args_only, "", "", pip_install_cmd) end context "when python-dev-is-python3 package is available" do before { allow(communicator).to receive(:test).with("apt-cache show python-dev-is-python3").and_return(true) } it "calls Cap::Guest::Pip::get_pip with 'pip' install_mode" do expect(communicator).to receive(:sudo). with("apt-get update -y -qq") expect(communicator).to receive(:sudo). with("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --option \"Dpkg::Options::=--force-confold\" build-essential curl git libssl-dev libffi-dev python-dev-is-python3") expect(communicator).to receive(:sudo). with("pip install --upgrade ansible") subject.ansible_install(machine, :pip, "", "", pip_install_cmd) end it "calls Cap::Guest::Pip::get_pip with 'pip_args_only' install_mode" do expect(communicator).to receive(:sudo). with("apt-get update -y -qq") expect(communicator).to receive(:sudo). with("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --option \"Dpkg::Options::=--force-confold\" build-essential curl git libssl-dev libffi-dev python-dev-is-python3") expect(communicator).to receive(:sudo). with("pip install") subject.ansible_install(machine, :pip_args_only, "", "", pip_install_cmd) end end end it_behaves_like "Ansible setup via pip" end shared_examples_for "Ansible setup via pip is not implemented" do describe "when install_mode is different from :default" do it "raises an AnsiblePipInstallIsNotSupported error" do expect { subject.ansible_install(machine, :ansible_the_hardway, "", "", "") }.to raise_error(VagrantPlugins::Ansible::Errors::AnsiblePipInstallIsNotSupported) end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/cap/guest/suse/ansible_install_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../../base" require_relative "../shared/pip_ansible_install_examples" require Vagrant.source_root.join("plugins/provisioners/ansible/cap/guest/suse/ansible_install") describe VagrantPlugins::Ansible::Cap::Guest::SUSE::AnsibleInstall do include_context "unit" subject { VagrantPlugins::Ansible::Cap::Guest::SUSE::AnsibleInstall } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute).and_return(true) end describe "#ansible_install" do it_behaves_like "Ansible setup via pip is not implemented" describe "when install_mode is :default (or unknown)" do it "installs ansible with 'zypper' package manager" do expect(communicator).to receive(:sudo).with("zypper --non-interactive --quiet install ansible") subject.ansible_install(machine, :default, "", "", "") end end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/cap/guest/ubuntu/ansible_install_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../../base" require_relative "../shared/pip_ansible_install_examples" require Vagrant.source_root.join("plugins/provisioners/ansible/cap/guest/ubuntu/ansible_install") describe VagrantPlugins::Ansible::Cap::Guest::Ubuntu::AnsibleInstall do include_context "unit" subject { VagrantPlugins::Ansible::Cap::Guest::Ubuntu::AnsibleInstall } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:execute).and_return(true) end describe "#ansible_install" do it_behaves_like "Ansible setup via pip on Debian-based systems" describe "when install_mode is :default (or unknown)" do describe "#ansible_apt_install" do describe "installs ansible from ansible/ansible PPA repository" do check_if_add_apt_repository_is_present="test -x \"$(which add-apt-repository)\"" it "first installs 'software-properties-common' package if add-apt-repository is not already present" do allow(communicator).to receive(:test). with(check_if_add_apt_repository_is_present).and_return(false) expect(communicator).to receive(:sudo).once.ordered. with(""" apt-get update -y -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq software-properties-common --option \"Dpkg::Options::=--force-confold\" """) expect(communicator).to receive(:sudo).once.ordered. with(""" add-apt-repository ppa:ansible/ansible -y && \ apt-get update -y -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ansible --option \"Dpkg::Options::=--force-confold\" """) subject.ansible_install(machine, :default, "", "", "") end it "adds 'ppa:ansible/ansible' and install 'ansible' package" do allow(communicator).to receive(:test). with(check_if_add_apt_repository_is_present).and_return(true) expect(communicator).to receive(:sudo). with(""" add-apt-repository ppa:ansible/ansible -y && \ apt-get update -y -qq && \ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ansible --option \"Dpkg::Options::=--force-confold\" """) subject.ansible_install(machine, :default, "", "", "") end end end end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/config/guest_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../support/shared/config" require_relative "shared" require Vagrant.source_root.join("plugins/provisioners/ansible/config/guest") describe VagrantPlugins::Ansible::Config::Guest do include_context "unit" subject { described_class.new } # FIXME: machine.ui.warn stub is not working as expected... let(:machine) { double("machine", env: Vagrant::Environment.new) } let(:communicator) { double("communicator") } let(:existing_file) { "this/path/is/a/stub" } it "supports a list of options" do supported_options = %w( become become_user compatibility_mode config_file extra_vars galaxy_command galaxy_role_file galaxy_roles_path groups host_vars install install_mode inventory_path limit pip_args pip_install_cmd playbook playbook_command provisioning_path raw_arguments skip_tags start_at_task sudo sudo_user tags tmp_path vault_password_file verbose version ) expect(get_provisioner_option_names(described_class)).to eql(supported_options) end describe "default options handling" do it_behaves_like "options shared by both Ansible provisioners" it "assigns default values to unset guest-specific options" do subject.finalize! expect(subject.install).to be(true) expect(subject.install_mode).to eql(:default) expect(subject.provisioning_path).to eql("/vagrant") expect(subject.tmp_path).to eql("/tmp/vagrant-ansible") expect(subject.pip_install_cmd).to eql("") end end describe "#validate" do before do subject.playbook = existing_file end it_behaves_like "an Ansible provisioner", "/vagrant", "local" it "falls back to :default install_mode for any invalid setting" do subject.install_mode = "from_source" subject.finalize! result = subject.validate(machine) expect(subject.install_mode).to eql(:default) end it "supports :pip install_mode" do subject.install_mode = "pip" subject.finalize! result = subject.validate(machine) expect(subject.install_mode).to eql(:pip) end it "supports :pip_args_only install_mode" do subject.install_mode = "pip_args_only" subject.finalize! result = subject.validate(machine) expect(subject.install_mode).to eql(:pip_args_only) end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/config/host_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../support/shared/config" require_relative "shared" require Vagrant.source_root.join("plugins/provisioners/ansible/config/host") describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do include_context "unit" subject { described_class.new } let(:machine) { double("machine", env: Vagrant::Environment.new) } let(:existing_file) { File.expand_path(__FILE__) } it "supports a list of options" do supported_options = %w( ask_become_pass ask_sudo_pass ask_vault_pass become become_user compatibility_mode config_file extra_vars force_remote_user galaxy_command galaxy_role_file galaxy_roles_path groups host_key_checking host_vars inventory_path limit playbook playbook_command raw_arguments raw_ssh_args skip_tags start_at_task sudo sudo_user tags vault_password_file verbose version ) expect(get_provisioner_option_names(described_class)).to eql(supported_options) end describe "default options handling" do it_behaves_like "options shared by both Ansible provisioners" it "assigns default values to unset host-specific options" do subject.finalize! expect(subject.ask_become_pass).to be(false) expect(subject.ask_sudo_pass).to be(false) # deprecated expect(subject.ask_vault_pass).to be(false) expect(subject.force_remote_user).to be(true) expect(subject.host_key_checking).to be(false) expect(subject.raw_ssh_args).to be_nil end 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 describe "ask_become_pass option" do it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :ask_become_pass, false end describe "ask_sudo_pass option" do before do # Filter the deprecation notice allow($stdout).to receive(:puts) end it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :ask_sudo_pass, false it_behaves_like "any deprecated option", :ask_sudo_pass, :ask_become_pass, true end describe "ask_vault_pass option" do it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :ask_vault_pass, false end describe "#validate" do before do subject.playbook = existing_file end it_behaves_like "an Ansible provisioner", "", "remote" it "returns an error if the raw_ssh_args is of the wrong data type" do subject.raw_ssh_args = { arg1: 1, arg2: "foo" } subject.finalize! result = subject.validate(machine) expect(result["ansible remote provisioner"]).to eql([ I18n.t("vagrant.provisioners.ansible.errors.raw_ssh_args_invalid", type: subject.raw_ssh_args.class.to_s, value: subject.raw_ssh_args.to_s) ]) end it "converts a raw_ssh_args option defined as a String into an Array" do subject.raw_arguments = "-o ControlMaster=no" subject.finalize! result = subject.validate(machine) expect(subject.raw_arguments).to eql(["-o ControlMaster=no"]) end end end ================================================ FILE: test/unit/plugins/provisioners/ansible/config/shared.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_examples_for 'options shared by both Ansible provisioners' do it "assigns default values to unset common options" do subject.finalize! expect(subject.become).to be(false) expect(subject.become_user).to be_nil expect(subject.compatibility_mode).to eql(VagrantPlugins::Ansible::COMPATIBILITY_MODE_AUTO) expect(subject.config_file).to be_nil expect(subject.extra_vars).to be_nil expect(subject.galaxy_command).to eql("ansible-galaxy install --role-file=%{role_file} --roles-path=%{roles_path} --force") expect(subject.galaxy_role_file).to be_nil expect(subject.galaxy_roles_path).to be_nil expect(subject.groups).to eq({}) expect(subject.host_vars).to eq({}) expect(subject.inventory_path).to be_nil expect(subject.limit).to be_nil expect(subject.playbook).to be_nil expect(subject.playbook_command).to eql("ansible-playbook") expect(subject.raw_arguments).to be_nil expect(subject.skip_tags).to be_nil expect(subject.start_at_task).to be_nil expect(subject.sudo).to be(false) # deprecated expect(subject.sudo_user).to be_nil # deprecated expect(subject.tags).to be_nil expect(subject.vault_password_file).to be_nil expect(subject.verbose).to be(false) expect(subject.version).to be_empty end end shared_examples_for 'any deprecated option' do |deprecated_option, new_option, option_value| it "shows the deprecation message" do expect($stdout).to receive(:puts).with("DEPRECATION: The '#{deprecated_option}' option for the Ansible provisioner is deprecated.").and_return(nil) expect($stdout).to receive(:puts).with("Please use the '#{new_option}' option instead.").and_return(nil) expect($stdout).to receive(:puts).with("The '#{deprecated_option}' option will be removed in a future release of Vagrant.\n\n").and_return(nil) subject.send("#{deprecated_option}=", option_value) subject.finalize! end end shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup | provisioner_label = "ansible #{ansible_setup} provisioner" provisioner_system = ansible_setup == "local" ? "guest" : "host" it "returns an error if the playbook option is undefined" do subject.playbook = nil subject.finalize! result = subject.validate(machine) expect(result[provisioner_label]).to eql([ I18n.t("vagrant.provisioners.ansible.errors.no_playbook") ]) end describe "compatibility_mode option" do VagrantPlugins::Ansible::COMPATIBILITY_MODES.each do |valid_mode| it "supports compatibility mode '#{valid_mode}'" do subject.compatibility_mode = valid_mode subject.finalize! result = subject.validate(machine) expect(subject.compatibility_mode).to eql(valid_mode) end end it "returns an error if the compatibility mode is not set" do subject.compatibility_mode = nil subject.finalize! result = subject.validate(machine) expect(result[provisioner_label]).to eql([ I18n.t("vagrant.provisioners.ansible.errors.no_compatibility_mode", valid_modes: "'auto', '1.8', '2.0'") ]) end %w(invalid 1.9 2.3).each do |invalid_mode| it "returns an error if the compatibility mode is invalid (e.g. '#{invalid_mode}')" do subject.compatibility_mode = invalid_mode subject.finalize! result = subject.validate(machine) expect(result[provisioner_label]).to eql([ I18n.t("vagrant.provisioners.ansible.errors.no_compatibility_mode", valid_modes: "'auto', '1.8', '2.0'") ]) end end end it "passes if the extra_vars option is a hash" do subject.extra_vars = { var1: 1, var2: "foo" } subject.finalize! result = subject.validate(machine) expect(result[provisioner_label]).to eql([]) end it "returns an error if the extra_vars option is of wrong data type" do subject.extra_vars = ["var1", 3, "var2", 5] subject.finalize! result = subject.validate(machine) expect(result[provisioner_label]).to eql([ I18n.t("vagrant.provisioners.ansible.errors.extra_vars_invalid", type: subject.extra_vars.class.to_s, value: subject.extra_vars.to_s) ]) end it "converts a raw_arguments option defined as a String into an Array" do subject.raw_arguments = "--foo=bar" subject.finalize! result = subject.validate(machine) expect(subject.raw_arguments).to eql(%w(--foo=bar)) end it "returns an error if the raw_arguments is of the wrong data type" do subject.raw_arguments = { arg1: 1, arg2: "foo" } subject.finalize! result = subject.validate(machine) expect(result[provisioner_label]).to eql([ I18n.t("vagrant.provisioners.ansible.errors.raw_arguments_invalid", type: subject.raw_arguments.class.to_s, value: subject.raw_arguments.to_s) ]) end it "it collects and returns all detected errors" do subject.compatibility_mode = nil subject.playbook = nil subject.extra_vars = ["var1", 3, "var2", 5] subject.raw_arguments = { arg1: 1, arg2: "foo" } subject.finalize! result = subject.validate(machine) expect(result[provisioner_label].size).to eql(4) expect(result[provisioner_label]).to include( I18n.t("vagrant.provisioners.ansible.errors.no_compatibility_mode", valid_modes: "'auto', '1.8', '2.0'")) expect(result[provisioner_label]).to include( I18n.t("vagrant.provisioners.ansible.errors.no_playbook")) expect(result[provisioner_label]).to include( I18n.t("vagrant.provisioners.ansible.errors.extra_vars_invalid", type: subject.extra_vars.class.to_s, value: subject.extra_vars.to_s)) expect(result[provisioner_label]).to include( I18n.t("vagrant.provisioners.ansible.errors.raw_arguments_invalid", type: subject.raw_arguments.class.to_s, value: subject.raw_arguments.to_s)) end describe "become option" do it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :become, false end describe "sudo option" do before do # Filter the deprecation notice allow($stdout).to receive(:puts) end it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :sudo, false it_behaves_like "any deprecated option", :sudo, :become, true end describe "sudo_user option" do before do # Filter the deprecation notice allow($stdout).to receive(:puts) end it_behaves_like "any deprecated option", :sudo_user, :become_user, "foo" end end ================================================ FILE: test/unit/plugins/provisioners/ansible/provisioner_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/provisioners/ansible/config/host") require Vagrant.source_root.join("plugins/provisioners/ansible/provisioner/host") # # Helper Functions # def find_last_argument_after(ref_index, ansible_playbook_args, arg_pattern) subset = ansible_playbook_args[(ref_index + 1)..(ansible_playbook_args.length-2)].reverse subset.each do |i| return true if i =~ arg_pattern end return false end describe VagrantPlugins::Ansible::Provisioner::Host do include_context "unit" subject { described_class.new(machine, config) } let(:iso_env) do # We have to create a Vagrantfile so there is a Vagrant Environment to provide: # - a location for the generated inventory # - multi-machines configuration env = isolated_environment env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" config.vm.define :machine1 config.vm.define :machine2 end VF env.create_vagrant_env end let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:config) { VagrantPlugins::Ansible::Config::Host.new } let(:ssh_info) {{ private_key_path: ['/path/to/my/key'], keys_only: true, username: 'testuser', host: '127.0.0.1', port: 2223 }} let(:default_execute_result) { Vagrant::Util::Subprocess::Result.new(0, "", "") } let(:existing_file) { File.expand_path(__FILE__) } let(:generated_inventory_dir) { File.join(machine.env.local_data_path, %w(provisioners ansible inventory)) } let(:generated_inventory_file) { File.join(generated_inventory_dir, 'vagrant_ansible_inventory') } before do allow(Vagrant::Util::Platform).to receive(:solaris?).and_return(false) allow(machine).to receive(:ssh_info).and_return(ssh_info) allow(machine.env).to receive(:active_machines) .and_return([[iso_env.machine_names[0], :dummy], [iso_env.machine_names[1], :dummy]]) stubbed_ui = Vagrant::UI::Colored.new allow(stubbed_ui).to receive(:detail).and_return("") allow(stubbed_ui).to receive(:warn).and_return("") allow(machine.env).to receive(:ui).and_return(stubbed_ui) config.playbook = 'playbook.yml' end # # Class methods for code reuse across examples # def self.it_should_check_ansible_version it "execute 'Python ansible version check before executing 'ansible-playbook'" do expect(Vagrant::Util::Subprocess).to receive(:execute) .once.with('python3', '-c', "import importlib.metadata; print('ansible ' + importlib.metadata.version('ansible'))", { notify: %i[ stdout stderr ] }) expect(Vagrant::Util::Subprocess).to receive(:execute) .once.with('ansible-playbook', any_args) end end def self.it_should_check_ansible_core_version it "executes 'Python ansible-core version check before executing 'ansible-playbook'" do expect(Vagrant::Util::Subprocess).to receive(:execute) .once.with('python3', '-c', "import importlib.metadata; print('ansible-core ' + importlib.metadata.version('ansible-core'))", { notify: %i[ stdout stderr ] }) expect(Vagrant::Util::Subprocess).to receive(:execute) .once.with('ansible-playbook', any_args) end end def self.it_should_set_arguments_and_environment_variables( expected_args_count = 5, expected_vars_count = 4, 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('ansible-playbook', any_args) { |*args| expect(args[1]).to eq("--connection=ssh") expect(args[2]).to eq("--timeout=30") inventory_count = args.count { |x| x.match(/^--inventory-file=.+$/) if x.is_a?(String) } expect(inventory_count).to be > 0 expect(args[args.length-2]).to eq("playbook.yml") }.and_return(default_execute_result) end it "sets --limit argument" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| all_limits = args.select { |x| x.match(/^(--limit=|-l)/) if x.is_a?(String) } if config.raw_arguments raw_limits = config.raw_arguments.select { |x| x.match(/^(--limit=|-l)/) if x.is_a?(String) } expect(all_limits.length - raw_limits.length).to eq(1) expect(all_limits.last).to eq(raw_limits.last) else if config.limit limit = config.limit.kind_of?(Array) ? config.limit.join(',') : config.limit expect(all_limits.last).to eq("--limit=#{limit}") else expect(all_limits.first).to eq("--limit=#{machine.name}") end end }.and_return(default_execute_result) end it "exports environment variables" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last if expected_host_key_checking expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to_not include("-o UserKnownHostsFile=/dev/null") else expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o UserKnownHostsFile=/dev/null") end expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentitiesOnly=yes") expect(cmd_opts[:env]['ANSIBLE_FORCE_COLOR']).to eql("true") expect(cmd_opts[:env]).to_not include("ANSIBLE_NOCOLOR") expect(cmd_opts[:env]['ANSIBLE_HOST_KEY_CHECKING']).to eql(expected_host_key_checking.to_s) expect(cmd_opts[:env]['PYTHONUNBUFFERED']).to eql(1) }.and_return(default_execute_result) end # "roughly" verify that only expected args/vars have been defined by the provisioner it "sets the expected number of arguments and environment variables" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args.length - 2).to eq(expected_args_count) expect(args.last[:env].length).to eq(expected_vars_count) }.and_return(default_execute_result) end it "enables '#{expected_transport_mode}' as default transport mode" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| index = args.rindex("--connection=#{expected_transport_mode}") expect(index).to be > 0 expect(find_last_argument_after(index, args, /--connection=\w+/)).to be(false) }.and_return(default_execute_result) end end def self.it_should_set_optional_arguments(arg_map) it "sets optional arguments" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| arg_map.each_pair do |vagrant_option, ansible_argument| index = args.index(ansible_argument) if config.send(vagrant_option) expect(index).to be > 0 else expect(index).to be_nil end end }.and_return(default_execute_result) end end def self.it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "configures ControlPersist (like Ansible defaults) via ANSIBLE_SSH_ARGS" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ControlMaster=auto") expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ControlPersist=60s") }.and_return(default_execute_result) end end def self.it_should_create_and_use_generated_inventory(with_user = true) it "generates an inventory with all active machines" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(config.inventory_path).to be_nil expect(File.exist?(generated_inventory_file)).to be(true) inventory_content = File.read(generated_inventory_file) _ssh = config.compatibility_mode == VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 ? "" : "_ssh" if with_user expect(inventory_content).to include("#{machine.name} ansible#{_ssh}_host=#{machine.ssh_info[:host]} ansible#{_ssh}_port=#{machine.ssh_info[:port]} ansible#{_ssh}_user='#{machine.ssh_info[:username]}' ansible_ssh_private_key_file='#{machine.ssh_info[:private_key_path][0]}'\n") else expect(inventory_content).to include("#{machine.name} ansible#{_ssh}_host=#{machine.ssh_info[:host]} ansible#{_ssh}_port=#{machine.ssh_info[:port]} ansible_ssh_private_key_file='#{machine.ssh_info[:private_key_path][0]}'\n") end expect(inventory_content).to include("# MISSING: '#{iso_env.machine_names[1]}' machine was probably removed without using Vagrant. This machine should be recreated.\n") }.and_return(default_execute_result) end it "sets as ansible inventory the directory containing the auto-generated inventory file" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_index = args.rindex("--inventory-file=#{generated_inventory_dir}") expect(inventory_index).to be > 0 expect(find_last_argument_after(inventory_index, args, /--inventory-file=\w+/)).to be(false) }.and_return(default_execute_result) end end def ensure_that_config_is_valid # Abort the test when an invalid configuration is detected config.validate(machine) if config._detected_errors.length > 0 raise "Invalid Provisioner Configuration! Detected Errors:\n#{config._detected_errors.to_s}" end end describe "#provision" do before do unless RSpec.current_example.metadata[:skip_before] config.finalize! allow(Vagrant::Util::Subprocess).to receive(:execute) .and_return(Vagrant::Util::Subprocess::Result.new(0, "", "")) allow(subject).to receive(:check_path) end end after do unless RSpec.current_example.metadata[:skip_after] ensure_that_config_is_valid subject.provision end end describe 'checking existence of Ansible configuration files' do STUBBED_INVALID_PATH = "/test/239nfmd/invalid_path".freeze it 'raises an error when the `playbook` file does not exist', skip_before: true, skip_after: true do allow(subject).to receive(:check_path).and_raise(VagrantPlugins::Ansible::Errors::AnsibleError, _key: :config_file_not_found, config_option: "playbook", path: STUBBED_INVALID_PATH, system: "host") config.playbook = STUBBED_INVALID_PATH config.finalize! ensure_that_config_is_valid expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleError, "`playbook` does not exist on the host: #{STUBBED_INVALID_PATH}") end %w(config_file extra_vars inventory_path galaxy_role_file vault_password_file).each do |option_name| it "raises an error when the '#{option_name}' does not exist", skip_before: true, skip_after: true do allow(Vagrant::Util::Subprocess).to receive(:execute) .and_return( Vagrant::Util::Subprocess::Result.new(0, "", "")) config.playbook = existing_file config.send(option_name + '=', STUBBED_INVALID_PATH) config.finalize! ensure_that_config_is_valid expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleError, "`#{option_name}` does not exist on the host: #{STUBBED_INVALID_PATH}") end end end describe 'when ansible-playbook fails' do it "raises an error", skip_before: true, skip_after: true do config.finalize! ensure_that_config_is_valid allow(subject).to receive(:check_path) allow(Vagrant::Util::Subprocess).to receive(:execute) .and_return(Vagrant::Util::Subprocess::Result.new(1, "", "")) expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCommandFailed) end end describe "with default options" do it_should_check_ansible_version it_should_check_ansible_core_version it_should_set_arguments_and_environment_variables it_should_create_and_use_generated_inventory it "does not add any group section to the generated inventory" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to_not match(/^\s*\[^\\+\]\s*$/) }.and_return(default_execute_result) end it "doesn't show the ansible-playbook command" do expect(machine.env.ui).to_not receive(:detail).with(/ansible-playbook/) end end describe "deprecated 'sudo' options are aliases for equivalent 'become' options" do before do # Filter the deprecation notices allow($stdout).to receive(:puts) config.sudo = true config.sudo_user = 'deployer' config.ask_sudo_pass = true end it_should_set_optional_arguments({"sudo" => "--sudo", "sudo_user" => "--sudo-user=deployer", "ask_sudo_pass" => "--ask-sudo-pass", "become" => "--sudo", "become_user" => "--sudo-user=deployer", "ask_become_pass" => "--ask-sudo-pass"}) end context "with compatibility_mode 'auto'" do before do config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_AUTO end valid_versions = { "0.6": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8, "1.9.4": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8, "2.5.0.0-rc1": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, "2.x.y.z": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, "4.3.2.1": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, "[core 2.11.0]": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, "7.1.0": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, "10.1.0": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 } valid_versions.each_pair do |ansible_version, mode| describe "and ansible version #{ansible_version}" do before do allow(subject).to receive(:gather_ansible_version).and_return("ansible #{ansible_version}\n...\n") end it "detects the compatibility mode #{mode}" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(config.compatibility_mode).to eq(mode) }.and_return(default_execute_result) end end end invalid_versions = [ "ansible devel", "anything 1.2", "2.9.2.1", ] invalid_versions.each do |unknown_ansible_version| describe "and `ansible version check returning '#{unknown_ansible_version}'" do before do allow(subject).to receive(:gather_ansible_version).and_return(unknown_ansible_version) end it "applies the safest compatibility mode ('#{VagrantPlugins::Ansible::SAFE_COMPATIBILITY_MODE}')" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(config.compatibility_mode).to eq(VagrantPlugins::Ansible::SAFE_COMPATIBILITY_MODE) }.and_return(default_execute_result) end it "warns about not being able to detect the best compatibility mode" do expect(machine.env.ui).to receive(:warn).with( I18n.t("vagrant.provisioners.ansible.compatibility_mode_not_detected", compatibility_mode: VagrantPlugins::Ansible::SAFE_COMPATIBILITY_MODE, gathered_version: unknown_ansible_version) + "\n") end end end end context "with compatibility_mode '#{VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8}'" do before do config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8 end it_should_check_ansible_version it_should_check_ansible_core_version it_should_create_and_use_generated_inventory it "doesn't warn about compatibility mode auto-detection" do expect(machine.env.ui).to_not receive(:warn) end end context "with compatibility_mode '#{VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0}'" do before do config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 allow(subject).to receive(:gather_ansible_version).and_return("ansible 2.3.0.0\n...\n") end it_should_create_and_use_generated_inventory it "doesn't warn about compatibility mode auto-detection" do expect(machine.env.ui).to_not receive(:warn) end describe "and an incompatible ansible version" do before do allow(subject).to receive(:gather_ansible_version).and_return("ansible 1.9.3\n...\n") end it "raises a compatibility conflict error", skip_before: false, skip_after: true do ensure_that_config_is_valid expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCompatibilityModeConflict) end end describe "deprecated 'sudo' options are aliases for equivalent 'become' options" do before do # Filter the deprecation notices allow($stdout).to receive(:puts) config.sudo = true config.sudo_user = 'deployer' config.ask_sudo_pass = true end it_should_set_optional_arguments({"sudo" => "--become", "sudo_user" => "--become-user=deployer", "ask_sudo_pass" => "--ask-become-pass", "become" => "--become", "become_user" => "--become-user=deployer", "ask_become_pass" => "--ask-become-pass"}) end end describe "with playbook_command option" do before do config.playbook_command = "custom-ansible-playbook" # set the compatibility mode to ensure that only ansible-playbook is executed config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8 end it "uses custom playbook_command to run playbooks" do expect(Vagrant::Util::Subprocess).to receive(:execute) .with("custom-ansible-playbook", any_args) .and_return(default_execute_result) end end describe "with host_vars option" do it_should_create_and_use_generated_inventory it "adds host variables (given in Hash format) to the generated inventory" do config.host_vars = { machine1: { "http_port" => 80, "comments" => "'some text with spaces and quotes'", "description" => "text with spaces but no quotes", } } expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 comments='some text with spaces and quotes' description='text with spaces but no quotes'") }.and_return(default_execute_result) end it "adds host variables (given in Array format) to the generated inventory" do config.host_vars = { machine1: ["http_port=80", "maxRequestsPerChild=808"] } expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") }.and_return(default_execute_result) end it "adds host variables (given in String format) to the generated inventory " do config.host_vars = { :machine1 => "http_port=80 maxRequestsPerChild=808" } expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") }.and_return(default_execute_result) end it "retrieves the host variables by machine name, also in String format" do config.host_vars = { "machine1" => "http_port=80 maxRequestsPerChild=808" } expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") }.and_return(default_execute_result) end end describe "with groups option" do it_should_create_and_use_generated_inventory it "adds group sections to the generated inventory" do config.groups = { "group1" => "machine1", "group1:children" => 'bar group3', "group2" => [iso_env.machine_names[1]], "group3" => ["unknown", "#{machine.name}"], "group4" => ["machine[1:2]", "machine[a:f]"], "group6" => [machine.name], "bar" => ["#{machine.name}", "group3"], "bar:children" => ["group1", "group2", "group3", "group5"], } expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) # Accept String instead of Array for group member list expect(inventory_content).to include("[group1]\nmachine1\n\n") expect(inventory_content).to include("[group1:children]\nbar\ngroup3\n\n") # Skip "lost" machines expect(inventory_content).to include("[group2]\n\n") # Skip "unknown" machines expect(inventory_content).to include("[group3]\n#{machine.name}\n\n") # Accept Symbol datatype for group names expect(inventory_content).to include("[group6]\n#{machine.name}\n\n") # Accept host range patterns expect(inventory_content).to include("[group4]\nmachine[1:2]\nmachine[a:f]\n") # Don't mix group names and host names expect(inventory_content).to include("[bar]\n#{machine.name}\n\n") # A group of groups only includes declared groups expect(inventory_content).not_to include("group5") expect(inventory_content).to match(Regexp.quote("[bar:children]\ngroup1\ngroup2\ngroup3\n") + "$") }.and_return(default_execute_result) end it "adds group vars to the generated inventory" do config.groups = { "group1" => [machine.name], "group2" => [machine.name], "group3" => [machine.name], "group1:vars" => {"hashvar1" => "hashvalue1", "hashvar2" => "hashvalue2"}, "group2:vars" => ["arrayvar1=arrayvalue1", "arrayvar2=arrayvalue2"], "group3:vars" => "stringvar1=stringvalue1 stringvar2=stringvalue2", } expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) # Hash syntax expect(inventory_content).to include("[group1:vars]\nhashvar1=hashvalue1\nhashvar2=hashvalue2\n") # Array syntax expect(inventory_content).to include("[group2:vars]\narrayvar1=arrayvalue1\narrayvar2=arrayvalue2\n") # Single string syntax expect(inventory_content).to include("[group3:vars]\nstringvar1=stringvalue1\nstringvar2=stringvalue2\n") }.and_return(default_execute_result) end it "adds 'all:vars' section to the generated inventory" do config.groups = { "all:vars" => { "var1" => "value1", "var2" => "value2" } } expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) expect(inventory_content).to include("[all:vars]\nvar1=value1\nvar2=value2\n") }.and_return(default_execute_result) end end describe "with host_key_checking option enabled" do before do config.host_key_checking = true end it_should_set_arguments_and_environment_variables 5, 4, true end describe "with boolean (flag) options disabled" do before do config.become = false config.ask_become_pass = false config.ask_vault_pass = false config.become_user = 'root' end it_should_set_arguments_and_environment_variables 6 it_should_set_optional_arguments({ "become_user" => "--sudo-user=root" }) it "it does not set boolean flag when corresponding option is set to false" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args.index("--sudo")).to be_nil expect(args.index("--ask-sudo-pass")).to be_nil expect(args.index("--ask-vault-pass")).to be_nil }.and_return(default_execute_result) end end describe "with raw_arguments option" do before do config.become = false config.force_remote_user = false config.skip_tags = %w(foo bar) config.limit = "all" config.raw_arguments = ["--connection=paramiko", "--skip-tags=ignored", "--module-path=/other/modules", "--sudo", "-l localhost", "--limit=foo", "--limit=bar", "--inventory-file=/forget/it/my/friend", "--user=lion", "--new-arg=yeah"] end it_should_set_arguments_and_environment_variables 17, 4, false, "paramiko" it "sets all raw arguments" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| config.raw_arguments.each do |raw_arg| expect(args).to include(raw_arg) end }.and_return(default_execute_result) end it "sets raw arguments after arguments related to supported options" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args.index("--user=lion")).to be > args.index("--user=testuser") expect(args.index("--inventory-file=/forget/it/my/friend")).to be > args.index("--inventory-file=#{generated_inventory_dir}") expect(args.index("--limit=bar")).to be > args.index("--limit=all") expect(args.index("--skip-tags=ignored")).to be > args.index("--skip-tags=foo,bar") }.and_return(default_execute_result) end it "sets boolean flag (e.g. --sudo) defined in raw_arguments, even if corresponding option is set to false" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include('--sudo') }.and_return(default_execute_result) end end describe "with limit option" do before do config.limit = %w(foo !bar) end 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('ansible-playbook', any_args) { |*args| expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") expect(args).to include("--user=#{machine.ssh_info[:username]}") }.and_return(default_execute_result) end end context "with winrm communicator" do let(:iso_winrm_env) do env = isolated_environment env.vagrantfile <<-VF Vagrant.configure("2") do |config| config.winrm.username = 'winner' config.winrm.password = 'winword' config.winrm.transport = :ssl config.vm.define :machine1 do |machine| machine.vm.box = "winbox" machine.vm.communicator = :winrm end end VF env.create_vagrant_env end let(:machine) { iso_winrm_env.machine(iso_winrm_env.machine_names[0], :dummy) } it_should_set_arguments_and_environment_variables it "generates an inventory with winrm connection settings" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(config.inventory_path).to be_nil expect(File.exist?(generated_inventory_file)).to be(true) inventory_content = File.read(generated_inventory_file) expect(inventory_content).to include("machine1 ansible_connection=winrm ansible_ssh_host=127.0.0.1 ansible_ssh_port=55986 ansible_ssh_user='winner' ansible_ssh_pass='winword'\n") }.and_return(default_execute_result) end describe "with force_remote_user option disabled" do before do config.force_remote_user = false end it "doesn't set the ansible remote user in inventory and use '--user' argument with the vagrant ssh username" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) expect(inventory_content).to include("machine1 ansible_connection=winrm ansible_ssh_host=127.0.0.1 ansible_ssh_port=55986 ansible_ssh_pass='winword'\n") expect(args).to include("--user=testuser") }.and_return(default_execute_result) end end end describe "with inventory_path option" do before do config.inventory_path = existing_file end 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('ansible-playbook', any_args) { |*args| expect(args).to include("--inventory-file=#{existing_file}") expect(args).not_to include("--inventory-file=#{generated_inventory_file}") expect(File.exist?(generated_inventory_file)).to be(false) }.and_return(default_execute_result) end it "uses an --extra-vars argument to force ansible_ssh_user parameter" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).not_to include("--user=#{machine.ssh_info[:username]}") expect(args).to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") }.and_return(default_execute_result) 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('ansible-playbook', any_args) { |*args| expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") expect(args).to include("--user=#{machine.ssh_info[:username]}") }.and_return(default_execute_result) end end end context "with config_file option defined" do before do config.config_file = existing_file end it "sets ANSIBLE_CONFIG environment variable" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to include("ANSIBLE_CONFIG") expect(cmd_opts[:env]['ANSIBLE_CONFIG']).to eql(existing_file) }.and_return(default_execute_result) end end describe "with ask_vault_pass option" do before do config.ask_vault_pass = true end it_should_set_arguments_and_environment_variables 6 it "should ask the vault password" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--ask-vault-pass") }.and_return(default_execute_result) end end describe "with vault_password_file option" do before do config.vault_password_file = existing_file end it_should_set_arguments_and_environment_variables 6 it "uses the given vault password file" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--vault-password-file=#{existing_file}") }.and_return(default_execute_result) end end describe "with raw_ssh_args" do before do config.raw_ssh_args = ['-o ControlMaster=no', '-o ForwardAgent=no'] end 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 expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last raw_opt_index = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ControlMaster=no") default_opt_index = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ControlMaster=auto") expect(raw_opt_index).to be < default_opt_index }.and_return(default_execute_result) end describe "and with ssh forwarding enabled" do before do ssh_info[:forward_agent] = true end it "sets '-o ForwardAgent=yes' via ANSIBLE_SSH_ARGS with higher priority than raw_ssh_args values" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last forwardAgentYes = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ForwardAgent=yes") forwardAgentNo = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ForwardAgent=no") expect(forwardAgentYes).to be < forwardAgentNo }.and_return(default_execute_result) end end end describe "with multiple SSH identities" do before do ssh_info[:private_key_path] = ['/path/to/my/key', '/an/other/identity', '/yet/an/other/key'] end 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 expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/an/other/identity") expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/yet/an/other/key") }.and_return(default_execute_result) end end describe "with an identity file containing `%`" do before do ssh_info[:private_key_path] = ['/foo%bar/key', '/bar%%buz/key'] end it "replaces `%` with `%%`" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/foo%%bar/key") expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/bar%%%%buz/key") }.and_return(default_execute_result) end end describe "with ssh forwarding enabled" do before do ssh_info[:forward_agent] = true end 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 expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ForwardAgent=yes") }.and_return(default_execute_result) end end describe "with an ssh proxy command configured" do before do ssh_info[:proxy_command] = "ssh -W %h:%p -q user@remote_libvirt_host" end it "sets '-o ProxyCommand' via ANSIBLE_SSH_ARGS" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ProxyCommand='ssh -W %h:%p -q user@remote_libvirt_host'") }.and_return(default_execute_result) end end 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 6 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("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 --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 6 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("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 --connection=ssh --timeout=30 --limit=\"machine1\" --inventory-file=#{generated_inventory_dir} -#{verbose_option} playbook.yml") end end end describe "with an invalid string" do before do config.verbose = "wrong" end it_should_set_arguments_and_environment_variables 6 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("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 --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).to_not receive(:detail).with(/ansible-playbook/) end end end describe "without colorized output" do before do allow(machine.env).to receive(:ui).and_return(Vagrant::UI::Basic.new) allow(machine.env.ui).to receive(:warn).and_return("") # hide the breaking change warning end it "disables ansible-playbook colored output" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to_not include("ANSIBLE_FORCE_COLOR") expect(cmd_opts[:env]['ANSIBLE_NOCOLOR']).to eql("true") }.and_return(default_execute_result) end end context "with version option set" do before do config.version = "2.3.4.5" end describe "and the installed ansible version is correct" do before do allow(subject).to receive(:gather_ansible_version).and_return("ansible #{config.version}\n...\n") end it "executes ansible-playbook command" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args).and_return(default_execute_result) end end describe "whitespace in version string" do before do allow(subject).to receive(:gather_ansible_version).and_return("ansible #{config.version} \n...\n") end it "sets the correct gathered_version" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args).and_return(default_execute_result) end end describe "and there is an ansible version mismatch" do before do allow(subject).to receive(:gather_ansible_version).and_return("ansible 1.9.6\n...\n") end it "raises an error about the ansible version mismatch", skip_before: false, skip_after: true do ensure_that_config_is_valid expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleVersionMismatch) end end describe "and the installed ansible version cannot be detected" do before do allow(subject).to receive(:gather_ansible_version).and_return("") end it "skips the ansible version check and executes ansible-playbook command" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args).and_return(default_execute_result) end end describe "with special value: 'latest'" do before do config.version = :latest allow(subject).to receive(:gather_ansible_version).and_return("ansible 2.2.0.1\n...\n") end it "skips the ansible version check and executes ansible-playbook command" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args).and_return(default_execute_result) end end end describe "with galaxy support" do before do config.galaxy_role_file = existing_file end it "raises an error when ansible-galaxy command fails", skip_before: true, skip_after: true do config.finalize! ensure_that_config_is_valid allow(subject).to receive(:check_path) allow(Vagrant::Util::Subprocess).to receive(:execute) .and_return(Vagrant::Util::Subprocess::Result.new(1, "", "")) expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCommandFailed) end it 'execute three commands: Python ansible version check, ansible-galaxy, and ansible-playbook' do expect(Vagrant::Util::Subprocess).to receive(:execute) .once .with('python3', '-c', "import importlib.metadata; print('ansible ' + importlib.metadata.version('ansible'))", { notify: %i[stdout stderr] }) .and_return(default_execute_result) expect(Vagrant::Util::Subprocess).to receive(:execute) .once .with('ansible-galaxy', any_args) .and_return(default_execute_result) expect(Vagrant::Util::Subprocess).to receive(:execute) .once .with('ansible-playbook', any_args) .and_return(default_execute_result) end it "doesn't show the ansible-galaxy command" do expect(machine.env.ui).to_not receive(:detail).with(/ansible-galaxy/) end describe "with verbose option enabled" do before do config.galaxy_roles_path = "/tmp/roles" config.verbose = true end it "shows the ansible-galaxy command in use" do expect(machine.env.ui).to receive(:detail) .with(%Q(ansible-galaxy install --role-file='#{existing_file}' --roles-path='/tmp/roles' --force)) end end end context "with galaxy_roles_path option defined" do before do config.galaxy_roles_path = "my-roles" end it "sets ANSIBLE_ROLES_PATH with corresponding absolute path" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to include("ANSIBLE_ROLES_PATH") expect(cmd_opts[:env]['ANSIBLE_ROLES_PATH']).to eql(File.join(machine.env.root_path, "my-roles")) }.and_return(default_execute_result) end end context "with extra_vars option defined" do describe "with a hash value" do before do config.extra_vars = { var1: %Q(string with 'apo$trophe$', \\, " and =), var2: { x: 42 } } end it_should_set_optional_arguments({ "extra_vars" => "--extra-vars={\"var1\":\"string with 'apo$trophe$', \\\\, \\\" and =\",\"var2\":{\"x\":42}}" }) end describe "with a string value referring to file path (with the '@' prefix)" do before do config.extra_vars = "@#{existing_file}" end it_should_set_optional_arguments({ "extra_vars" => "--extra-vars=@#{File.expand_path(__FILE__)}" }) end end # The Vagrant Ansible provisioner does not validate the coherency of # argument combinations, and lets ansible-playbook complain. describe "with a maximum of options" do before do # vagrant general options ssh_info[:forward_agent] = true ssh_info[:private_key_path] = ['/my/key1', '/my/key2'] # command line arguments config.galaxy_roles_path = "/up/to the stars" config.extra_vars = { var1: %Q(string with 'apo$trophe$', \\, " and =), var2: { x: 42 } } config.become = true config.become_user = 'deployer' config.verbose = "vvv" config.ask_become_pass = true config.ask_vault_pass = true config.vault_password_file = existing_file config.tags = %w(db www) config.skip_tags = %w(foo bar) config.limit = 'machine*:&vagrant:!that_one' config.start_at_task = "joe's awesome task" config.raw_arguments = ["--why-not", "--su-user=foot", "--ask-su-pass", "--limit=all", "--private-key=./myself.key", "--extra-vars='{\"var3\":\"foo\"}'"] # environment variables config.config_file = existing_file config.host_key_checking = true config.raw_ssh_args = ['-o ControlMaster=no'] end it_should_set_arguments_and_environment_variables 21, 6, true it_should_explicitly_enable_ansible_ssh_control_persist_defaults it_should_set_optional_arguments({ "extra_vars" => "--extra-vars={\"var1\":\"string with 'apo$trophe$', \\\\, \\\" and =\",\"var2\":{\"x\":42}}", "become" => "--sudo", "become_user" => "--sudo-user=deployer", "verbose" => "-vvv", "ask_become_pass" => "--ask-sudo-pass", "ask_vault_pass" => "--ask-vault-pass", "vault_password_file" => "--vault-password-file=#{File.expand_path(__FILE__)}", "tags" => "--tags=db,www", "skip_tags" => "--skip-tags=foo,bar", "limit" => "--limit=machine*:&vagrant:!that_one", "start_at_task" => "--start-at-task=joe's awesome task", }) it "also includes given raw arguments" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--why-not") expect(args).to include("--su-user=foot") expect(args).to include("--ask-su-pass") expect(args).to include("--limit=all") expect(args).to include("--private-key=./myself.key") }.and_return(default_execute_result) end it "shows the ansible-playbook command, with additional quotes when required" do expect(machine.env.ui).to receive(:detail) .with(%Q(PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_ROLES_PATH='/up/to the stars' ANSIBLE_CONFIG='#{existing_file}' ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_SSH_ARGS='-o IdentitiesOnly=yes -o IdentityFile=/my/key1 -o IdentityFile=/my/key2 -o ForwardAgent=yes -o ControlMaster=no -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --connection=ssh --timeout=30 --ask-sudo-pass --ask-vault-pass --limit="machine*:&vagrant:!that_one" --inventory-file=#{generated_inventory_dir} --extra-vars=\\{\\"var1\\":\\"string\\ with\\ \\'apo\\$trophe\\$\\',\\ \\\\\\\\,\\ \\\\\\"\\ and\\ \\=\\",\\"var2\\":\\{\\"x\\":42\\}\\} --sudo --sudo-user=deployer -vvv --vault-password-file=#{existing_file} --tags=db,www --skip-tags=foo,bar --start-at-task="joe's awesome task" --why-not --su-user=foot --ask-su-pass --limit=all --private-key=./myself.key --extra-vars='{\"var3\":\"foo\"}' playbook.yml)) end end # # Special cases related to the VM provider context # context "with Docker provider on a non-Linux host" do let(:fake_host_ssh_info) {{ private_key_path: ['/path/to/docker/host/key'], username: 'boot9docker', host: '127.0.0.1', port: 2299 }} let(:fake_host_vm) { double("host_vm").tap do |h| allow(h).to receive(:ssh_info).and_return(fake_host_ssh_info) end } before do allow(machine).to receive(:provider_name).and_return(:docker) allow(machine.provider).to receive(:host_vm?).and_return(true) allow(machine.provider).to receive(:host_vm).and_return(fake_host_vm) end it "uses an SSH ProxyCommand to reach the VM" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ProxyCommand='ssh boot9docker@127.0.0.1 -p 2299 -i /path/to/docker/host/key -o Compression=yes -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no exec nc %h %p 2>/dev/null'") }.and_return(default_execute_result) end end # # Special cases related to the Vagrant Host operating system in use # context "on a Windows host" do before do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) allow(machine.ui).to receive(:warn) # Set the compatibility mode to only get the Windows warning config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8 end it "warns that Windows is not officially supported for the Ansible control machine" do expect(machine.env.ui).to receive(:warn) .with(I18n.t("vagrant.provisioners.ansible.windows_not_supported_for_control_machine") + "\n") end end context "on a Solaris-like host" do before do allow(Vagrant::Util::Platform).to receive(:solaris?).and_return(true) end it "does not set IdentitiesOnly=yes in ANSIBLE_SSH_ARGS" do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to_not include("-o IdentitiesOnly=yes") }.and_return(default_execute_result) end describe "and with host_key_checking option enabled" do it "does not set ANSIBLE_SSH_ARGS environment variable" do config.host_key_checking = true expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to_not include('ANSIBLE_SSH_ARGS') }.and_return(Vagrant::Util::Subprocess::Result.new(0, "", "")) end end end describe 'with config.ssh.keys_only = false' do it 'does not set IdentitiesOnly=yes in ANSIBLE_SSH_ARGS' do ssh_info[:keys_only] = false expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to_not include("-o IdentitiesOnly=yes") }.and_return(default_execute_result) end end describe '#get_inventory_argument' do context 'when ansible version is not detected' do before do config.version = nil allow(subject).to receive(:gather_ansible_version).and_return("ansible #{config.version}\n...\n") allow(subject).to receive(:gather_ansible_version).with("ansible-core").and_return("ansible #{config.version}\n...\n") end it 'returns the default inventory command' do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--inventory-file=#{generated_inventory_dir}") }.and_return(default_execute_result) end end context 'when ansible version is detected' do describe 'version >= 2.19' do before do config.version = "2.19.0" allow(subject).to receive(:gather_ansible_version).with("ansible").and_return("ansible #{config.version}\n...\n") allow(subject).to receive(:gather_ansible_version).with("ansible-core").and_return("ansible-core #{config.version}\n...\n") end it 'returns --inventory as the inventory command' do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--inventory=#{generated_inventory_dir}") }.and_return(default_execute_result) end end describe 'version < 2.19' do before do config.version = "2.18.5" allow(subject).to receive(:gather_ansible_version).with("ansible").and_return("ansible #{config.version}\n...\n") allow(subject).to receive(:gather_ansible_version).with("ansible-core").and_return("ansible-core #{config.version}\n...\n") end it 'returns --inventory-file as the inventory command' do expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--inventory-file=#{generated_inventory_dir}") }.and_return(default_execute_result) end end end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/cap/freebsd/chef_installed_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/cap/freebsd/chef_installed") describe VagrantPlugins::Chef::Cap::FreeBSD::ChefInstalled 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config") } subject { described_class } before do allow(machine).to receive(:communicate).and_return(communicator) end describe "#chef_installed" do describe "when chef-workstation" do let(:version) { "15.0.0" } let(:command) { "test -x /opt/chef-workstation/bin/chef&& /opt/chef-workstation/bin/chef --version | grep '15.0.0'" } it "returns true if installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(true) subject.chef_installed(machine, "chef-workstation", version) end it "returns false if not installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(false) expect(subject.chef_installed(machine, "chef-workstation", version)).to be_falsey end end describe "when not chef-workstation" do let(:version) { "15.0.0" } let(:command) { "test -x /opt/chef/bin/chef-client&& /opt/chef/bin/chef-client --version | grep '15.0.0'" } it "returns true if installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(true) subject.chef_installed(machine, "chef_solo", version) end it "returns false if not installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(false) expect(subject.chef_installed(machine, "chef_solo", version)).to be_falsey end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/cap/linux/chef_installed_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/cap/linux/chef_installed") describe VagrantPlugins::Chef::Cap::Linux::ChefInstalled 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config") } subject { described_class } before do allow(machine).to receive(:communicate).and_return(communicator) end describe "#chef_installed" do describe "when chef-workstation" do let(:version) { "15.0.0" } let(:command) { "test -x /opt/chef-workstation/bin/chef&& /opt/chef-workstation/bin/chef --version | grep '15.0.0'" } it "returns true if installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(true) subject.chef_installed(machine, "chef-workstation", version) end it "returns false if not installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(false) expect(subject.chef_installed(machine, "chef-workstation", version)).to be_falsey end end describe "when not chef-workstation" do let(:version) { "15.0.0" } let(:command) { "test -x /opt/chef/bin/chef-client&& /opt/chef/bin/chef-client --version | grep '15.0.0'" } it "returns true if installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(true) subject.chef_installed(machine, "chef_solo", version) end it "returns false if not installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(false) expect(subject.chef_installed(machine, "chef_solo", version)).to be_falsey end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/cap/omnios/chef_installed_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/cap/omnios/chef_installed") describe VagrantPlugins::Chef::Cap::OmniOS::ChefInstalled 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config") } subject { described_class } before do allow(machine).to receive(:communicate).and_return(communicator) end describe "#chef_installed" do describe "when chef-workstation" do let(:version) { "15.0.0" } let(:command) { "test -x /opt/chef-workstation/bin/chef&& /opt/chef-workstation/bin/chef --version | grep '15.0.0'" } it "returns true if installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(true) subject.chef_installed(machine, "chef-workstation", version) end it "returns false if not installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(false) expect(subject.chef_installed(machine, "chef-workstation", version)).to be_falsey end end describe "when not chef-workstation" do let(:version) { "15.0.0" } let(:command) { "test -x /opt/chef/bin/chef-client&& /opt/chef/bin/chef-client --version | grep '15.0.0'" } it "returns true if installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(true) subject.chef_installed(machine, "chef_solo", version) end it "returns false if not installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(false) expect(subject.chef_installed(machine, "chef_solo", version)).to be_falsey end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/cap/windows/chef_installed_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/cap/windows/chef_installed") describe VagrantPlugins::Chef::Cap::Windows::ChefInstalled 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config") } subject { described_class } before do allow(machine).to receive(:communicate).and_return(communicator) end describe "#chef_installed" do describe "when chef-workstation" do let(:version) { "15.0.0" } let(:command) { "if ((&chef --version) -Match \"15.0.0\"){ exit 0 } else { exit 1 }" } it "returns true if installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(true) subject.chef_installed(machine, "chef-workstation", version) end it "returns false if not installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(false) expect(subject.chef_installed(machine, "chef-workstation", version)).to be_falsey end end describe "when chef-workstation" do let(:version) { "15.0.0" } let(:command) { "if ((&chef-client --version) -Match \"15.0.0\"){ exit 0 } else { exit 1 }" } it "returns true if installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(true) subject.chef_installed(machine, "chef_solo", version) end it "returns false if not installed" do expect(machine.communicate).to receive(:test). with(command, sudo: true).and_return(false) expect(subject.chef_installed(machine, "chef_solo", version)).to be_falsey end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/command_builder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/command_builder") describe VagrantPlugins::Chef::CommandBuilder do let(:machine) { double("machine") } let(:chef_config) { double("chef_config") } before(:each) do allow(chef_config).to receive(:install).and_return(true) allow(chef_config).to receive(:version).and_return("12.0.0") allow(chef_config).to receive(:provisioning_path).and_return("/tmp/vagrant-chef-1") allow(chef_config).to receive(:arguments).and_return(nil) allow(chef_config).to receive(:binary_env).and_return(nil) allow(chef_config).to receive(:binary_path).and_return(nil) allow(chef_config).to receive(:binary_env).and_return(nil) allow(chef_config).to receive(:log_level).and_return(:info) end describe ".initialize" do it "raises an error when chef type is not client or solo" do expect { VagrantPlugins::Chef::CommandBuilder.new(:client_bad, chef_config) }. to raise_error(RuntimeError) end it "does not raise an error for :client" do expect { VagrantPlugins::Chef::CommandBuilder.new(:client, chef_config) }.to_not raise_error end it "does not raise an error for :solo" do expect { VagrantPlugins::Chef::CommandBuilder.new(:solo, chef_config) }.to_not raise_error end end describe "#command" do describe "windows" do subject do VagrantPlugins::Chef::CommandBuilder.new(:client, chef_config, windows: true) end it "executes the chef-client in PATH by default" do expect(subject.command).to match(/^chef-client/) end it "executes the chef-client using full path if binary_path is specified" do allow(chef_config).to receive(:binary_path).and_return( "c:\\opscode\\chef\\bin\\chef-client") expect(subject.command).to match(/^c:\\opscode\\chef\\bin\\chef-client\\chef-client/) end it "builds a guest friendly client.rb path" do expect(subject.command).to include( '--config c:\\tmp\\vagrant-chef-1\\client.rb') end it "builds a guest friendly solo.json path" do expect(subject.command).to include( '--json-attributes c:\\tmp\\vagrant-chef-1\\dna.json') end it "includes Chef arguments if specified" do allow(chef_config).to receive(:arguments).and_return("bacon pants") expect(subject.command).to include( " bacon pants") end it "includes --no-color if UI is not colored" do expect(subject.command).to include( " --no-color") end it "includes --force-formatter if Chef > 10" do expect(subject.command).to include( " --force-formatter") end it "does not include --force-formatter if Chef < 11" do allow(chef_config).to receive(:version).and_return("10.0") expect(subject.command).to_not include( " --force-formatter") end it "does not include --force-formatter if we did not install Chef" do allow(chef_config).to receive(:install).and_return(false) expect(subject.command).to_not include( " --force-formatter") end end describe "linux" do subject do VagrantPlugins::Chef::CommandBuilder.new(:client, chef_config, windows: false) end it "executes the chef-client in PATH by default" do expect(subject.command).to match(/^chef-client/) end it "executes the chef-client using full path if binary_path is specified" do allow(chef_config).to receive(:binary_path).and_return( "/opt/chef/chef-client") expect(subject.command).to match(/^\/opt\/chef\/chef-client/) end it "builds a guest friendly client.rb path" do expect(subject.command).to include( "--config /tmp/vagrant-chef-1/client.rb") end it "builds a guest friendly solo.json path" do expect(subject.command).to include( "--json-attributes /tmp/vagrant-chef-1/dna.json") end it "includes Chef arguments if specified" do allow(chef_config).to receive(:arguments).and_return("bacon") expect(subject.command).to include( " bacon") end it "includes --no-color if UI is not colored" do expect(subject.command).to include( " --no-color") end it "includes environment variables if specified" do allow(chef_config).to receive(:binary_env).and_return("ENVVAR=VAL") expect(subject.command).to match(/^ENVVAR=VAL /) end it "does not include --force-formatter if Chef < 11" do allow(chef_config).to receive(:version).and_return("10.0") expect(subject.command).to_not include( " --force-formatter") end it "does not include --force-formatter if we did not install Chef" do allow(chef_config).to receive(:install).and_return(false) expect(subject.command).to_not include( " --force-formatter") end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/config/base_runner_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/config/base_runner") describe VagrantPlugins::Chef::Config::BaseRunner do include_context "unit" subject { described_class.new } let(:machine) { double("machine") } describe "#arguments" do it "defaults to nil" do subject.finalize! expect(subject.arguments).to be(nil) end end describe "#attempts" do it "defaults to 1" do subject.finalize! expect(subject.attempts).to eq(1) end end describe "#custom_config_path" do it "defaults to nil" do subject.finalize! expect(subject.custom_config_path).to be(nil) end end describe "#environment" do it "defaults to nil" do subject.finalize! expect(subject.environment).to be(nil) end end describe "#encrypted_data_bag_secret_key_path" do it "defaults to nil" do subject.finalize! expect(subject.encrypted_data_bag_secret_key_path).to be(nil) end end describe "#formatter" do it "defaults to nil" do subject.finalize! expect(subject.formatter).to be(nil) end end describe "#http_proxy" do it "defaults to nil" do subject.finalize! expect(subject.http_proxy).to be(nil) end end describe "#http_proxy_user" do it "defaults to nil" do subject.finalize! expect(subject.http_proxy_user).to be(nil) end end describe "#http_proxy_pass" do it "defaults to nil" do subject.finalize! expect(subject.http_proxy_pass).to be(nil) end end describe "#https_proxy" do it "defaults to nil" do subject.finalize! expect(subject.https_proxy).to be(nil) end end describe "#https_proxy_user" do it "defaults to nil" do subject.finalize! expect(subject.https_proxy_user).to be(nil) end end describe "#https_proxy_pass" do it "defaults to nil" do subject.finalize! expect(subject.https_proxy_pass).to be(nil) end end describe "#log_level" do it "defaults to :info" do subject.finalize! expect(subject.log_level).to be(:info) end it "is converted to a symbol" do subject.log_level = "foo" subject.finalize! expect(subject.log_level).to eq(:foo) end end describe "#no_proxy" do it "defaults to nil" do subject.finalize! expect(subject.no_proxy).to be(nil) end end describe "#node_name" do it "defaults to nil" do subject.finalize! expect(subject.node_name).to be(nil) end end describe "#provisioning_path" do it "defaults to nil" do subject.finalize! expect(subject.provisioning_path).to be(nil) end end describe "#file_backup_path" do it "defaults to nil" do subject.finalize! expect(subject.file_backup_path).to be(nil) end end describe "#file_cache_path" do it "defaults to nil" do subject.finalize! expect(subject.file_cache_path).to be(nil) end end describe "#verbose_logging" do it "defaults to false" do subject.finalize! expect(subject.verbose_logging).to be(false) end end describe "#enable_reporting" do it "defaults to true" do subject.finalize! expect(subject.enable_reporting).to be(true) end end describe "#run_list" do it "defaults to an empty array" do subject.finalize! expect(subject.run_list).to be_a(Array) expect(subject.run_list).to be_empty end end describe "#json" do it "defaults to an empty hash" do subject.finalize! expect(subject.json).to be_a(Hash) expect(subject.json).to be_empty end end describe "#add_recipe" do context "when the prefix is given" do it "adds the value to the run_list" do subject.add_recipe("recipe[foo::bar]") expect(subject.run_list).to eq %w(recipe[foo::bar]) end end context "when the prefix is not given" do it "adds the prefixed value to the run_list" do subject.add_recipe("foo::bar") expect(subject.run_list).to eq %w(recipe[foo::bar]) end end end describe "#add_role" do context "when the prefix is given" do it "adds the value to the run_list" do subject.add_role("role[foo]") expect(subject.run_list).to eq %w(role[foo]) end end context "when the prefix is not given" do it "adds the prefixed value to the run_list" do subject.add_role("foo") expect(subject.run_list).to eq %w(role[foo]) end end end describe "#validate_base" do context "when #custom_config_path does not exist" do let(:path) do next "/path/to/file" if !Vagrant::Util::Platform.windows? "C:/path/to/file" end before do allow(File).to receive(:file?) .with(path) .and_return(false) allow(machine).to receive(:env) .and_return(double("env", root_path: "", )) end it "returns an error" do subject.custom_config_path = path subject.finalize! expect(subject.validate_base(machine)) .to eq ['Path specified for "custom_config_path" does not exist.'] end end end describe "#merge" do it "merges the json hash" do a = described_class.new.tap do |i| i.json = { "foo" => "bar" } end b = described_class.new.tap do |i| i.json = { "zip" => "zap" } end result = a.merge(b) expect(result.json).to eq( "foo" => "bar", "zip" => "zap", ) end it "appends the run_list array" do a = described_class.new.tap do |i| i.run_list = ["recipe[foo::bar]"] end b = described_class.new.tap do |i| i.run_list = ["recipe[zip::zap]"] end result = a.merge(b) expect(result.run_list).to eq %w( recipe[foo::bar] recipe[zip::zap] ) end end end ================================================ FILE: test/unit/plugins/provisioners/chef/config/base_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/config/base") describe VagrantPlugins::Chef::Config::Base do include_context "unit" subject { described_class.new } let(:machine) { double("machine") } describe "#binary_path" do it "defaults to nil" do subject.finalize! expect(subject.binary_path).to be(nil) end end describe "#binary_env" do it "defaults to nil" do subject.finalize! expect(subject.binary_env).to be(nil) end end describe "#product" do it "defaults to \"chef\"" do subject.finalize! expect(subject.product).to eq("chef") end end describe "#install" do it "defaults to true" do subject.finalize! expect(subject.install).to be(true) end it "is converted to a symbol" do subject.install = "force" subject.finalize! expect(subject.install).to eq(:force) end end describe "#log_level" do it "defaults to :info" do subject.finalize! expect(subject.log_level).to be(:info) end it "is converted to a symbol" do subject.log_level = "foo" subject.finalize! expect(subject.log_level).to eq(:foo) end end describe "#channel" do it "defaults to \"stable\"" do subject.finalize! expect(subject.channel).to eq("stable") end end describe "#version" do it "defaults to :latest" do subject.finalize! expect(subject.version).to eq(:latest) end it "converts the string 'latest' to a symbol" do subject.version = "latest" subject.finalize! expect(subject.version).to eq(:latest) end end describe "#installer_download_path" do it "defaults to nil" do subject.finalize! expect(subject.installer_download_path).to be(nil) end end describe "#omnibus_url" do it "defaults to https://omnitruck.chef.io" do subject.finalize! expect(subject.omnibus_url).to eq("https://omnitruck.chef.io") end it "makes use of the configured url" do subject.omnibus_url = "https://omnitruck.example.com" subject.finalize! expect(subject.omnibus_url).to eq("https://omnitruck.example.com") end end end ================================================ FILE: test/unit/plugins/provisioners/chef/config/chef_apply_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/config/chef_apply") describe VagrantPlugins::Chef::Config::ChefApply do include_context "unit" subject { described_class.new } let(:machine) { double("machine") } def chef_error(key, options = {}) I18n.t("vagrant.provisioners.chef.#{key}", **options) end describe "#recipe" do it "defaults to nil" do subject.finalize! expect(subject.recipe).to be(nil) end end describe "#upload_path" do it "defaults to /tmp/vagrant-chef-apply.rb" do subject.finalize! expect(subject.upload_path).to eq("/tmp/vagrant-chef-apply") end end describe "#validate" do before do allow(machine).to receive(:env) .and_return(double("env", root_path: "", )) subject.recipe = <<-EOH package "foo" EOH end let(:result) { subject.validate(machine) } let(:errors) { result["chef apply provisioner"] } context "when the recipe is nil" do it "returns an error" do subject.recipe = nil subject.finalize! expect(errors).to include chef_error("recipe_empty") end end context "when the recipe is empty" do it "returns an error" do subject.recipe = " " subject.finalize! expect(errors).to include chef_error("recipe_empty") end end context "when the log_level is an empty array" do it "returns an error" do subject.log_level = " " subject.finalize! expect(errors).to include chef_error("log_level_empty") end end context "when the upload_path is nil" do it "returns an error" do subject.upload_path = nil subject.finalize! expect(errors).to include chef_error("upload_path_empty") end end context "when the upload_path is an empty array" do it "returns an error" do subject.upload_path = " " subject.finalize! expect(errors).to include chef_error("upload_path_empty") end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/config/chef_client_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/config/chef_client") describe VagrantPlugins::Chef::Config::ChefClient do include_context "unit" subject { described_class.new } let(:machine) { double("machine") } describe "#chef_server_url" do it "defaults to nil" do subject.finalize! expect(subject.chef_server_url).to be(nil) end end describe "#client_key_path" do it "defaults to nil" do subject.finalize! expect(subject.client_key_path).to be(nil) end end describe "#delete_client" do it "defaults to false" do subject.finalize! expect(subject.delete_client).to be(false) end end describe "#delete_node" do it "defaults to false" do subject.finalize! expect(subject.delete_node).to be(false) end end describe "#validation_key_path" do it "defaults to nil" do subject.finalize! expect(subject.validation_key_path).to be(nil) end end describe "#validation_client_name" do it "defaults to chef-validator" do subject.finalize! expect(subject.validation_client_name).to eq("chef-validator") end end describe "#validate" do before do allow(machine).to receive(:env) .and_return(double("env", root_path: "", )) subject.chef_server_url = "https://example.com" subject.validation_key_path = "/path/to/key.pem" end let(:result) { subject.validate(machine) } let(:errors) { result["chef client provisioner"] } context "when the chef_server_url is nil" do it "returns an error" do subject.chef_server_url = nil subject.finalize! expect(errors).to eq([I18n.t("vagrant.config.chef.server_url_empty")]) end end context "when the chef_server_url is blank" do it "returns an error" do subject.chef_server_url = " " subject.finalize! expect(errors).to eq([I18n.t("vagrant.config.chef.server_url_empty")]) end end context "when the validation_key_path is nil" do it "returns an error" do subject.validation_key_path = nil subject.finalize! expect(errors).to eq([I18n.t("vagrant.config.chef.validation_key_path")]) end end context "when the validation_key_path is blank" do it "returns an error" do subject.validation_key_path = " " subject.finalize! expect(errors).to eq([I18n.t("vagrant.config.chef.validation_key_path")]) end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/config/chef_solo_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/config/chef_solo") describe VagrantPlugins::Chef::Config::ChefSolo do include_context "unit" subject { described_class.new } let(:machine) { double("machine") } describe "#cookbooks_path" do it "defaults to something" do subject.finalize! expect(subject.cookbooks_path).to eq([ [:host, "cookbooks"], [:vm, "cookbooks"], ]) end end describe "#data_bags_path" do it "defaults to an empty array" do subject.finalize! expect(subject.data_bags_path).to be_a(Array) expect(subject.data_bags_path).to be_empty end end describe "#environments_path" do it "defaults to an empty array" do subject.finalize! expect(subject.environments_path).to be_a(Array) expect(subject.environments_path).to be_empty end it "merges deeply nested paths" do subject.environments_path = ["/foo", "/bar", ["/zip"]] subject.finalize! expect(subject.environments_path) .to eq([:host, :host, :host].zip %w(/foo /bar /zip)) end end describe "#recipe_url" do it "defaults to nil" do subject.finalize! expect(subject.recipe_url).to be(nil) end end describe "#roles_path" do it "defaults to an empty array" do subject.finalize! expect(subject.roles_path).to be_a(Array) expect(subject.roles_path).to be_empty 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! expect(subject.synced_folder_type).to be(nil) end end describe "#validate" do before do allow(machine).to receive(:env) .and_return(double("env", root_path: "", )) subject.cookbooks_path = ["/cookbooks", "/more/cookbooks"] end let(:result) { subject.validate(machine) } let(:errors) { result["chef solo provisioner"] } context "when the cookbooks_path is nil" do it "returns an error" do subject.cookbooks_path = nil subject.finalize! expect(errors).to eq [I18n.t("vagrant.config.chef.cookbooks_path_empty")] end end context "when the cookbooks_path is an empty array" do it "returns an error" do subject.cookbooks_path = [] subject.finalize! expect(errors).to eq [I18n.t("vagrant.config.chef.cookbooks_path_empty")] end end context "when the cookbooks_path is an array with nil" do it "returns an error" do subject.cookbooks_path = [nil, nil] subject.finalize! expect(errors).to eq [I18n.t("vagrant.config.chef.cookbooks_path_empty")] end end context "when environments is given" do before do subject.environment = "production" end context "when the environments_path is nil" do it "returns an error" do subject.environments_path = nil subject.finalize! expect(errors).to eq [I18n.t("vagrant.config.chef.environment_path_required")] end end context "when the environments_path is an empty array" do it "returns an error" do subject.environments_path = [] subject.finalize! expect(errors).to eq [I18n.t("vagrant.config.chef.environment_path_required")] end end context "when the environments_path is an array with nil" do it "returns an error" do subject.environments_path = [nil, nil] subject.finalize! expect(errors).to eq [I18n.t("vagrant.config.chef.environment_path_required")] end end context "when the environments_path does not exist" do it "returns an error" do env_path = "/path/to/environments/that/will/never/exist" subject.environments_path = env_path subject.finalize! expect(errors).to eq [ I18n.t("vagrant.config.chef.environment_path_missing", path: env_path ) ] end end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/config/chef_zero_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/config/chef_zero") describe VagrantPlugins::Chef::Config::ChefZero do include_context "unit" subject { described_class.new } let(:machine) { double("machine") } describe "#cookbooks_path" do it "defaults to something" do subject.finalize! expect(subject.cookbooks_path).to eq([ [:host, "cookbooks"], [:vm, "cookbooks"], ]) end end describe "#data_bags_path" do it "defaults to an empty array" do subject.finalize! expect(subject.data_bags_path).to be_a(Array) expect(subject.data_bags_path).to be_empty end end describe "#environments_path" do it "defaults to an empty array" do subject.finalize! expect(subject.environments_path).to be_a(Array) expect(subject.environments_path).to be_empty end it "merges deeply nested paths" do subject.environments_path = ["/foo", "/bar", ["/zip"]] subject.finalize! expect(subject.environments_path) .to eq([:host, :host, :host].zip %w(/foo /bar /zip)) end end describe "#roles_path" do it "defaults to an empty array" do subject.finalize! expect(subject.roles_path).to be_a(Array) expect(subject.roles_path).to be_empty 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! expect(subject.synced_folder_type).to be(nil) end end describe "#validate" do before do allow(machine).to receive(:env) .and_return(double("env", root_path: "", )) subject.cookbooks_path = ["/cookbooks", "/more/cookbooks"] end let(:result) { subject.validate(machine) } let(:errors) { result["chef zero provisioner"] } context "when the cookbooks_path is nil" do it "returns an error" do subject.cookbooks_path = nil subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.cookbooks_path_empty")) end end context "when the cookbooks_path is an empty array" do it "returns an error" do subject.cookbooks_path = [] subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.cookbooks_path_empty")) end end context "when the cookbooks_path is an array with nil" do it "returns an error" do subject.cookbooks_path = [nil, nil] subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.cookbooks_path_empty")) end end context "when the nodes_path is nil" do it "returns an error" do subject.nodes_path = nil subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.nodes_path_empty")) end end context "when an element of nodes_path does not exist on disk" do it "returns an error" do nodes_path = ["/path/to/nodes/that/will/never/exist"] subject.nodes_path = nodes_path subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.nodes_path_missing", path: nodes_path )) end end context "when the nodes_path is an empty array" do it "returns an error" do subject.nodes_path = [] subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.nodes_path_empty")) end end context "when the nodes_path is an array with nil" do it "returns an error" do subject.nodes_path = [nil, nil] subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.nodes_path_empty")) end end context "when environments is given" do before do subject.environment = "production" end context "when the environments_path is nil" do it "returns an error" do subject.environments_path = nil subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.environment_path_required")) end end context "when the environments_path is an empty array" do it "returns an error" do subject.environments_path = [] subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.environment_path_required")) end end context "when the environments_path is an array with nil" do it "returns an error" do subject.environments_path = [nil, nil] subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.environment_path_required")) end end context "when the environments_path does not exist" do it "returns an error" do env_path = "/path/to/environments/that/will/never/exist" subject.environments_path = env_path subject.finalize! expect(errors).to include(I18n.t("vagrant.config.chef.environment_path_missing", path: env_path, )) end end end end end ================================================ FILE: test/unit/plugins/provisioners/chef/omnibus_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/omnibus") describe VagrantPlugins::Chef::Omnibus do describe "#sh_command" do it "includes the project name" do command = described_class.sh_command("chef", nil, "stable", "https://omnitruck.chef.io") expect(command).to include %|-P "chef"| end it "includes the channel" do command = described_class.sh_command("chef", nil, "stable", "https://omnitruck.chef.io") expect(command).to include %|-c "stable"| end it "includes the version" do command = described_class.sh_command("chef", "1.2.3", "stable", "https://omnitruck.chef.io") expect(command).to include %|-v "1.2.3"| end it "includes the Omnibus installation URL" do command = described_class.sh_command("chef", "1.2.3", "stable", "https://omnitruck.chef.io") expect(command).to include %|https://omnitruck.chef.io/install.sh| end it "includes the download path" do command = described_class.sh_command("chef", "1.2.3", "stable", "https://omnitruck.chef.io", download_path: "/some/path", ) expect(command).to include %|-d "/some/path"| end end describe "#ps_command" do it "includes the project name" do command = described_class.ps_command("chef", nil, "stable", "https://omnitruck.chef.io") expect(command).to include %|-project 'chef'| end it "includes the channel" do command = described_class.ps_command("chef", nil, "stable", "https://omnitruck.chef.io") expect(command).to include %|-channel 'stable'| end it "includes the version" do command = described_class.ps_command("chef", "1.2.3", "stable", "https://omnitruck.chef.io") expect(command).to include %|-version '1.2.3'| end it "includes the Omnibus installation URL" do command = described_class.ps_command("chef", "1.2.3", "stable", "https://omnitruck.chef.io") expect(command).to include %|https://omnitruck.chef.io/install.ps1| end end end ================================================ FILE: test/unit/plugins/provisioners/chef/provisioner/base_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/provisioner/base") describe VagrantPlugins::Chef::Provisioner::Base 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:config) { double("config") } subject { described_class.new(machine, config) } before do allow(config).to receive(:node_name) allow(config).to receive(:node_name=) end describe "#node_name" do let(:env) { double("env") } let(:root_path) { "/my/root" } before do allow(machine).to receive(:env).and_return(env) allow(env).to receive(:root_path).and_return(root_path) end it "defaults to node_name if given" do config = OpenStruct.new(node_name: "name") instance = described_class.new(machine, config) expect(instance.config.node_name).to eq("name") end it "defaults to hostname if given" do machine.config.vm.hostname = "by.hostname" instance = described_class.new(machine, OpenStruct.new(node_name: nil)) expect(instance.config.node_name).to eq("by.hostname") end it "generates a random name if no hostname or node_name is given" do machine.config.vm.hostname = nil instance = described_class.new(machine, OpenStruct.new(node_name: nil)) expect(instance.config.node_name).to match(/vagrant\-.+/) end it "does not set node_name if configuration does not define it" do expect(config).to receive(:respond_to?).with(:node_name).and_return(false) expect(config).not_to receive(:node_name) described_class.new(machine, config) end end describe "#encrypted_data_bag_secret_key_path" do let(:env) { double("env") } let(:root_path) { "/my/root" } before do allow(machine).to receive(:env).and_return(env) allow(env).to receive(:root_path).and_return(root_path) end it "returns absolute path as is" do expect(config).to receive(:encrypted_data_bag_secret_key_path). and_return("/foo/bar") expect(subject.encrypted_data_bag_secret_key_path).to eq "/foo/bar" end it "returns relative path joined to root_path" do expect(config).to receive(:encrypted_data_bag_secret_key_path). and_return("secret") expect(subject.encrypted_data_bag_secret_key_path).to eq "/my/root/secret" end end describe "#guest_encrypted_data_bag_secret_key_path" do it "returns nil if host path is not configured" do allow(config).to receive(:encrypted_data_bag_secret_key_path).and_return(nil) allow(config).to receive(:provisioning_path).and_return("/tmp/foo") expect(subject.guest_encrypted_data_bag_secret_key_path).to be_nil end it "returns path under config.provisioning_path" do allow(config).to receive(:encrypted_data_bag_secret_key_path).and_return("secret") allow(config).to receive(:provisioning_path).and_return("/tmp/foo") expect(File.dirname(subject.guest_encrypted_data_bag_secret_key_path)). to eq "/tmp/foo" end end end ================================================ FILE: test/unit/plugins/provisioners/chef/provisioner/chef_solo_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/provisioners/chef/provisioner/chef_solo") describe VagrantPlugins::Chef::Provisioner::ChefSolo 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:config) { double("config") } subject { described_class.new(machine, config) } before do allow(config).to receive(:node_name) allow(config).to receive(:node_name=) end describe "#expanded_folders" do before { allow(subject).to receive(:windows?).and_return(true) } it "handles the default Windows provisioning path" do allow(config).to receive(:provisioning_path).and_return(nil) remote_path = subject.expanded_folders([[:vm, "cookbooks-1"]])[0][2] expect(remote_path).to eq("/vagrant-chef/cookbooks-1") end it "removes drive letter prefix from path" do allow(config).to receive(:provisioning_path).and_return(nil) expect(File).to receive(:expand_path).and_return("C:/vagrant-chef/cookbooks-1") result = subject.expanded_folders([[:vm, "cookbooks-1"]]) remote_path = result[0][2] expect(remote_path).to eq("/vagrant-chef/cookbooks-1") end end describe "#expanded_folders" do it "expands Windows absolute provisioning path with relative path" do provisioning_path = "C:/vagrant-chef-1" unexpanded_path = "cookbooks-1" allow(config).to receive(:provisioning_path).and_return(provisioning_path) remote_path = subject.expanded_folders([[:vm, unexpanded_path]])[0][2] expect(remote_path).to eq("/vagrant-chef-1/cookbooks-1") end it "expands Windows absolute provisioning path with absolute path" do provisioning_path = "C:/vagrant-chef-1" unexpanded_path = "/cookbooks-1" allow(config).to receive(:provisioning_path).and_return(provisioning_path) remote_path = subject.expanded_folders([[:vm, unexpanded_path]])[0][2] expect(remote_path).to eq("/cookbooks-1") end it "expands Windows absolute provisioning path with Windows absolute path" do provisioning_path = "C:/vagrant-chef-1" unexpanded_path = "D:/cookbooks-1" allow(config).to receive(:provisioning_path).and_return(provisioning_path) remote_path = subject.expanded_folders([[:vm, unexpanded_path]])[0][2] expect(remote_path).to eq("/cookbooks-1") end it "expands absolute provisioning path with Windows absolute path" do provisioning_path = "/vagrant-chef-1" unexpanded_path = "D:/cookbooks-1" allow(config).to receive(:provisioning_path).and_return(provisioning_path) remote_path = subject.expanded_folders([[:vm, unexpanded_path]])[0][2] expect(remote_path).to eq("/cookbooks-1") end end end ================================================ FILE: test/unit/plugins/provisioners/container/client_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/container/client") describe VagrantPlugins::ContainerProvisioner::Client do let(:machine) { double("machine", communicate: communicator, ui: ui) } let(:ui) { Vagrant::UI::Silent.new } let(:communicator) { double("communicator") } let(:container_command) { "CONTAINER_COMMAND" } subject { described_class.new(machine, container_command) } describe "#container_name" do it "converts a container name to a run appropriate form" do config = { :name => "test/test:1.1.1", :original_name => "test/test:1.1.1" } expect(subject.container_name(config)).to eq("test-test-1.1.1") end end describe "#build_images" do before { allow(communicator).to receive(:sudo) } it "should use sudo to run command" do expect(communicator).to receive(:sudo).with(/#{Regexp.escape(container_command)}/) subject.build_images([["path", {}]]) end it "should output information to use" do expect(ui).to receive(:info).and_call_original subject.build_images([["path", {}]]) end it "should handle communicator output" do expect(communicator).to receive(:sudo).with(/#{Regexp.escape(container_command)}/). and_yield(:stdout, "some output") subject.build_images([["path", {}]]) end end describe "#pull_images" do before do allow(communicator).to receive(:sudo) end it "should use sudo to run command" do expect(communicator).to receive(:sudo).with(/#{Regexp.escape(container_command)}/) subject.pull_images(:image) end it "should output information to use" do expect(ui).to receive(:info).and_call_original subject.pull_images(:image) end end end ================================================ FILE: test/unit/plugins/provisioners/container/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/container/config") require Vagrant.source_root.join("plugins/kernel_v2/config/vm") describe VagrantPlugins::ContainerProvisioner::Config do subject { described_class.new } describe "#build_image" do it "stores them" do subject.build_image("foo") subject.build_image("bar", foo: :bar) subject.finalize! expect(subject.build_images.length).to eql(2) expect(subject.build_images[0]).to eql(["foo", {}]) expect(subject.build_images[1]).to eql(["bar", { foo: :bar }]) end end describe "#images" do it "stores them in a set" do subject.images = ["1", "1", "2"] subject.finalize! expect(subject.images.to_a.sort).to eql(["1", "2"]) end it "overrides previously set images" do subject.images = ["3"] subject.images = ["1", "1", "2"] subject.finalize! expect(subject.images.to_a.sort).to eql(["1", "2"]) end end describe "#merge" do it "has all images to pull" do subject.pull_images("1") other = described_class.new other.pull_images("2", "3") result = subject.merge(other) expect(result.images.to_a.sort).to eq( ["1", "2", "3"]) end it "has all the containers to run" do subject.run("foo", image: "bar", daemonize: false) subject.run("bar") other = described_class.new other.run("foo", image: "foo") result = subject.merge(other) result.finalize! cs = result.containers expect(cs.length).to eq(2) expect(cs["foo"]).to eq({ auto_assign_name: true, image: "foo", daemonize: false, restart: "always", }) expect(cs["bar"]).to eq({ auto_assign_name: true, image: "bar", daemonize: true, restart: "always", }) end it "has all the containers to build" do subject.build_image("foo") other = described_class.new other.build_image("bar") result = subject.merge(other) result.finalize! images = result.build_images expect(images.length).to eq(2) expect(images[0]).to eq(["foo", {}]) expect(images[1]).to eq(["bar", {}]) end end describe "#pull_images" do it "adds images to the list of images to build" do subject.pull_images("1") subject.pull_images("2", "3") subject.finalize! expect(subject.images.to_a.sort).to eql(["1", "2", "3"]) end end describe "#run" do it "runs the given image" do subject.run("foo") subject.finalize! expect(subject.containers).to eql({ "foo" => { auto_assign_name: true, daemonize: true, image: "foo", restart: "always", } }) end it "can not auto assign name" do subject.run("foo", auto_assign_name: false) subject.finalize! expect(subject.containers).to eql({ "foo" => { auto_assign_name: false, daemonize: true, image: "foo", restart: "always", } }) end it "can not daemonize" do subject.run("foo", daemonize: false) subject.finalize! expect(subject.containers).to eql({ "foo" => { auto_assign_name: true, daemonize: false, image: "foo", restart: "always", } }) end end end ================================================ FILE: test/unit/plugins/provisioners/docker/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/docker/config") require Vagrant.source_root.join("plugins/provisioners/docker/provisioner") require Vagrant.source_root.join("plugins/kernel_v2/config/vm") describe VagrantPlugins::DockerProvisioner::Config do subject { described_class.new } describe "#post_install_provision" do it "raises an error if 'docker' provisioner was provided" do expect {subject.post_install_provision("myprov", :type=>"docker", :inline=>"echo 'hello'")} .to raise_error(VagrantPlugins::DockerProvisioner::DockerError) end it "setups a basic provisioner" do prov = double() mock_provisioner = "mock" mock_provisioners = [mock_provisioner] allow(VagrantPlugins::Kernel_V2::VMConfig).to receive(:new). and_return(prov) allow(prov).to receive(:provision).and_return(mock_provisioners) allow(prov).to receive(:provisioners).and_return(mock_provisioners) subject.post_install_provision("myprov", :inline=>"echo 'hello'") expect(subject.post_install_provisioner).to eq(mock_provisioner) end end end ================================================ FILE: test/unit/plugins/provisioners/docker/installer_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/docker/provisioner") describe VagrantPlugins::DockerProvisioner::Installer do include_context "unit" subject { described_class.new(machine) } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:ready?).and_return(true) allow(communicator).to receive(:test).with(/Linux/).and_return(true) end describe "#ensure_installed" do it "returns if docker capability not present" do allow(machine).to receive_message_chain(:guest, :capability?).with(:docker_installed).and_return(false) expect(subject.ensure_installed()).to eq(false) end it "does not install docker if already present" do expect(communicator).to receive(:test).with(/docker/, {:sudo=>true}).and_return(true) allow(communicator).to receive(:test).and_return(true) expect(subject.ensure_installed()).to eq(true) end it "installs docker if not present" do allow(machine).to receive_message_chain(:guest, :capability?).with(:docker_installed).and_return(true) allow(machine).to receive_message_chain(:guest, :capability).with(:docker_install).and_return(false) allow(machine).to receive_message_chain(:guest, :capability).with(:docker_installed).and_return(false) # Expect to raise error since we are mocking out the test for docker install to return false expect {subject.ensure_installed()}.to raise_error(VagrantPlugins::DockerProvisioner::DockerError) end end end ================================================ FILE: test/unit/plugins/provisioners/docker/plugin_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/docker/provisioner") describe VagrantPlugins::DockerProvisioner::Plugin do subject { described_class } it "has valid guest capabilities" do subject.components.guest_capabilities.each do |guest, caps| caps.each do |cap| subject.components.guest_capabilities[guest][cap] end end end end ================================================ FILE: test/unit/plugins/provisioners/docker/provisioner_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/docker/provisioner") describe VagrantPlugins::DockerProvisioner::Provisioner do include_context "unit" subject { described_class.new(machine, config, installer, client) } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:config) { double("config") } let(:communicator) { double("comm") } let(:guest) { double("guest") } let(:client) { double("client") } let(:installer) { double("installer") } let(:hook) { double("hook") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(communicator).to receive(:execute).and_return(true) allow(communicator).to receive(:upload).and_return(true) allow(guest).to receive(:capability?).and_return(false) allow(guest).to receive(:capability).and_return(false) allow(client).to receive(:start_service).and_return(true) allow(client).to receive(:daemon_running?).and_return(true) allow(config).to receive(:images).and_return(Set.new) allow(config).to receive(:build_images).and_return(Set.new) allow(config).to receive(:containers).and_return(Hash.new) end describe "#provision" do let(:provisioner) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("spec-test", :shell) prov.config = {} prov end it "invokes a post_install_provisioner if defined and docker is installed" do allow(installer).to receive(:ensure_installed).and_return(true) allow(config).to receive(:post_install_provisioner).and_return(provisioner) allow(machine).to receive(:env).and_return(iso_env) allow(machine.env).to receive(:hook).and_return(true) expect(machine.env).to receive(:hook).with(:run_provisioner, anything) subject.provision() end it "does not invoke post_install_provisioner if not defined" do allow(installer).to receive(:ensure_installed).and_return(true) allow(config).to receive(:post_install_provisioner).and_return(nil) allow(machine).to receive(:env).and_return(iso_env) allow(machine.env).to receive(:hook).and_return(true) expect(machine.env).not_to receive(:hook).with(:run_provisioner, anything) subject.provision() end it "raises an error if docker daemon isn't running" do allow(installer).to receive(:ensure_installed).and_return(false) allow(client).to receive(:start_service).and_return(false) allow(client).to receive(:daemon_running?).and_return(false) expect { subject.provision() }. to raise_error(VagrantPlugins::DockerProvisioner::DockerError) end end end ================================================ FILE: test/unit/plugins/provisioners/file/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/provisioners/file/config") describe VagrantPlugins::FileUpload::Config do include_context "unit" subject { described_class.new } let(:env) do iso_env = isolated_environment iso_env.vagrantfile("") iso_env.create_vagrant_env end let(:machine) { double("machine", env: env) } describe "#validate" do it "returns an error if destination is not specified" do existing_file = File.expand_path(__FILE__) subject.source = existing_file subject.finalize! result = subject.validate(machine) expect(result["File provisioner"]).to eql([ I18n.t("vagrant.provisioners.file.no_dest_file") ]) end it "returns an error if source is not specified" do subject.destination = "/tmp/foo" subject.finalize! result = subject.validate(machine) expect(result["File provisioner"]).to eql([ I18n.t("vagrant.provisioners.file.no_source_file") ]) end it "returns an error if source file does not exist" do non_existing_file = "/this/does/not/exist" subject.source = non_existing_file subject.destination = "/tmp/foo" subject.finalize! result = subject.validate(machine) expect(result["File provisioner"]).to eql([ I18n.t("vagrant.provisioners.file.path_invalid", path: File.expand_path(non_existing_file)) ]) end it "passes with absolute source path" do existing_absolute_path = File.expand_path(__FILE__) subject.source = existing_absolute_path subject.destination = "/tmp/foo" subject.finalize! result = subject.validate(machine) expect(result["File provisioner"]).to eql([]) end it "passes with relative source path" do path = env.root_path.join("foo") path.open("w+") { |f| f.write("hello") } existing_relative_path = "foo" subject.source = existing_relative_path subject.destination = "/tmp/foo" subject.finalize! result = subject.validate(machine) expect(result["File provisioner"]).to eql([]) end end end ================================================ FILE: test/unit/plugins/provisioners/file/provisioner_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/provisioners/file/provisioner") describe VagrantPlugins::FileUpload::Provisioner do include_context "unit" subject { described_class.new(machine, config) } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:config) { double("config") } let(:communicator) { double("comm") } let(:guest) { double("guest") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(communicator).to receive(:execute).and_return(true) allow(communicator).to receive(:upload).and_return(true) allow(guest).to receive(:capability?).and_return(false) end describe "#provision" do it "creates the destination directory" do allow(config).to receive(:source).and_return("/source") allow(config).to receive(:destination).and_return("/foo/bar") subject.provision end it "creates the destination directory with a space" do allow(config).to receive(:source).and_return("/source") allow(config).to receive(:destination).and_return("/foo bar/bar") subject.provision end it "creates the destination directory above file" do allow(config).to receive(:source).and_return("/source/file.sh") allow(config).to receive(:destination).and_return("/foo/bar/file.sh") subject.provision end it "uploads the file" do allow(config).to receive(:source).and_return("/source") allow(config).to receive(:destination).and_return("/foo/bar") expect(communicator).to receive(:upload).with("/source", "/foo/bar") subject.provision end it "expands the source file path" do allow(config).to receive(:source).and_return("source") allow(config).to receive(:destination).and_return("/foo/bar") expect(communicator).to receive(:upload).with( File.expand_path("#{machine.env.cwd}/source"), "/foo/bar") subject.provision end it "expands the destination file path if capable" do allow(config).to receive(:source).and_return("/source") allow(config).to receive(:destination).and_return("$HOME/foo") expect(guest).to receive(:capability?). with(:shell_expand_guest_path).and_return(true) expect(guest).to receive(:capability). with(:shell_expand_guest_path, "$HOME/foo").and_return("/home/foo") expect(communicator).to receive(:upload).with("/source", "/home/foo") subject.provision end it "appends a '/.' if the destination doesnt end with a file separator" do allow(config).to receive(:source).and_return("/source") allow(config).to receive(:destination).and_return("/foo/bar") allow(File).to receive(:directory?).with("/source").and_return(true) expect(guest).to receive(:capability?). with(:shell_expand_guest_path).and_return(true) expect(guest).to receive(:capability). with(:shell_expand_guest_path, "/foo/bar").and_return("/foo/bar") expect(communicator).to receive(:upload).with("/source/.", "/foo/bar") subject.provision end it "appends a '/.' to expanded source if defined in original source" do allow(config).to receive(:source).and_return("/source/.") allow(File).to receive(:directory?).with("/source").and_return(true) allow(config).to receive(:destination).and_return("/foo/bar") expect(communicator).to receive(:upload).with("/source/.", "/foo/bar") subject.provision end end end ================================================ FILE: test/unit/plugins/provisioners/podman/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/podman/config") require Vagrant.source_root.join("plugins/provisioners/podman/provisioner") require Vagrant.source_root.join("plugins/kernel_v2/config/vm") describe VagrantPlugins::PodmanProvisioner::Config do subject { described_class.new } describe "#post_install_provision" do it "raises an error if 'podman' provisioner was provided" do expect {subject.post_install_provision("myprov", :type=>"podman", :inline=>"echo 'hello'")} .to raise_error(VagrantPlugins::PodmanProvisioner::PodmanError) end it "setups a basic provisioner" do prov = double() mock_provisioner = "mock" mock_provisioners = [mock_provisioner] allow(VagrantPlugins::Kernel_V2::VMConfig).to receive(:new). and_return(prov) allow(prov).to receive(:provision).and_return(mock_provisioners) allow(prov).to receive(:provisioners).and_return(mock_provisioners) subject.post_install_provision("myprov", :inline=>"echo 'hello'") expect(subject.post_install_provisioner).to eq(mock_provisioner) end end end ================================================ FILE: test/unit/plugins/provisioners/podman/installer_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/podman/provisioner") describe VagrantPlugins::PodmanProvisioner::Installer do include_context "unit" subject { described_class.new(machine) } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:communicator) { double("comm") } let(:install_mode) { :kubic } before do allow(machine).to receive(:communicate).and_return(communicator) allow(communicator).to receive(:ready?).and_return(true) allow(communicator).to receive(:test).with(/Linux/).and_return(true) end describe "#ensure_installed" do it "returns if podman capability not present" do allow(machine).to receive_message_chain(:guest, :capability?).with(:podman_installed).and_return(false) expect(subject.ensure_installed(install_mode)).to eq(false) end it "does not install podman if already present" do expect(communicator).to receive(:test).with(/podman/).and_return(true) allow(communicator).to receive(:test).and_return(true) expect(subject.ensure_installed(install_mode)).to eq(true) end end end ================================================ FILE: test/unit/plugins/provisioners/podman/provisioner_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/podman/provisioner") describe VagrantPlugins::PodmanProvisioner::Provisioner do include_context "unit" subject { described_class.new(machine, config, installer, client) } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:config) { double("config") } let(:communicator) { double("comm") } let(:guest) { double("guest") } let(:client) { double("client") } let(:installer) { double("installer") } let(:hook) { double("hook") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(communicator).to receive(:execute).and_return(true) allow(communicator).to receive(:upload).and_return(true) allow(guest).to receive(:capability?).and_return(false) allow(guest).to receive(:capability).and_return(false) allow(client).to receive(:start_service).and_return(true) allow(client).to receive(:daemon_running?).and_return(true) allow(config).to receive(:images).and_return(Set.new) allow(config).to receive(:build_images).and_return(Set.new) allow(config).to receive(:containers).and_return(Hash.new) end describe "#provision" do let(:provisioner) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("spec-test", :shell) prov.config = {} prov end it "invokes a post_install_provisioner if defined and podman is installed" do allow(installer).to receive(:ensure_installed).and_return(true) allow(config).to receive(:post_install_provisioner).and_return(provisioner) allow(config).to receive(:kubic).and_return(false) allow(machine).to receive(:env).and_return(iso_env) allow(machine.env).to receive(:hook).and_return(true) expect(machine.env).to receive(:hook).with(:run_provisioner, anything) subject.provision() end it "does not invoke post_install_provisioner if not defined" do allow(installer).to receive(:ensure_installed).and_return(true) allow(config).to receive(:post_install_provisioner).and_return(nil) allow(config).to receive(:kubic).and_return(false) allow(machine).to receive(:env).and_return(iso_env) allow(machine.env).to receive(:hook).and_return(true) expect(machine.env).not_to receive(:hook).with(:run_provisioner, anything) subject.provision() end end end ================================================ FILE: test/unit/plugins/provisioners/puppet/provisioner/puppet_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/puppet/provisioner/puppet") describe VagrantPlugins::Puppet::Provisioner::Puppet 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:config) { double("config") } let(:facts) { [] } let(:communicator) { double("comm") } let(:guest) { double("guest") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:module_paths) { ["etc/puppet/modules"] } # make this something real subject { described_class.new(machine, config) } before do allow(machine).to receive(:communicate).and_return(comm) end describe "#run_puppet_apply" do let(:options) { "--environment production" } let(:binary_path) { "/opt/puppetlabs/bin" } let(:manifest_file) { "default.pp" } it "runs puppet on a manifest" do allow(config).to receive(:options).and_return(options) allow(config).to receive(:environment_path).and_return(false) allow(config).to receive(:facter).and_return(facts) allow(config).to receive(:binary_path).and_return(binary_path) allow(config).to receive(:environment_variables).and_return({hello: "there", test: "test"}) allow(config).to receive(:working_directory).and_return(false) allow(config).to receive(:manifest_file).and_return(manifest_file) allow(config).to receive(:structured_facts).and_return(double("structured_facts")) allow_message_expectations_on_nil allow(@module_paths).to receive(:map) { module_paths } allow(@module_paths).to receive(:empty?).and_return(true) expect(machine).to receive(:communicate).and_return(comm) expect(machine.communicate).to receive(:sudo).with("hello=\"there\" test=\"test\" /opt/puppetlabs/bin/puppet apply --environment production --color=false --detailed-exitcodes ", anything) subject.run_puppet_apply() end it "properly sets env variables on windows" do allow(config).to receive(:options).and_return(options) allow(config).to receive(:environment_path).and_return(false) allow(config).to receive(:facter).and_return(facts) allow(config).to receive(:binary_path).and_return(binary_path) allow(config).to receive(:environment_variables).and_return({hello: "there", test: "test"}) allow(config).to receive(:working_directory).and_return(false) allow(config).to receive(:manifest_file).and_return(manifest_file) allow(config).to receive(:structured_facts).and_return(double("structured_facts")) allow(subject).to receive(:windows?).and_return(true) allow_message_expectations_on_nil allow(@module_paths).to receive(:map) { module_paths } allow(@module_paths).to receive(:empty?).and_return(true) expect(machine).to receive(:communicate).and_return(comm) expect(machine.communicate).to receive(:sudo).with("$env:hello=\"there\"; $env:test=\"test\"; /opt/puppetlabs/bin/puppet apply --environment production --color=false --detailed-exitcodes ", anything) subject.run_puppet_apply() end end describe "#provision" do let(:options) { double("options") } let(:binary_path) { "/opt/puppetlabs/bin" } let(:manifest_file) { "default.pp" } let(:module_paths) { ["etc/puppet/modules"] } # make this something real let(:environment_paths) { ["/etc/puppet/environment"] } it "builds structured facts if set" do allow(machine).to receive(:guest).and_return(double("guest")) allow(machine.guest).to receive(:capability?).and_return(false) allow(config).to receive(:environment_path).and_return(environment_paths) allow(config).to receive(:environment).and_return("production") allow(config).to receive(:manifests_path).and_return(manifest_file) allow(config).to receive(:temp_dir).and_return("/tmp") allow(config).to receive(:hiera_config_path).and_return(false) allow(subject).to receive(:parse_environment_metadata).and_return(true) allow(subject).to receive(:verify_binary).and_return(true) allow(subject).to receive(:run_puppet_apply).and_return(true) allow_message_expectations_on_nil allow(@module_paths).to receive(:each) { module_paths } allow(config).to receive(:facter).and_return({"coolfacts"=>"here they are"}) allow(config).to receive(:structured_facts).and_return(true) expect(machine.communicate).to receive(:upload).with(anything, "/tmp/vagrant_facts.yaml") expect(machine.communicate).to receive(:sudo).with("mkdir -p /tmp; chmod 0777 /tmp", {}) expect(machine.communicate).to receive(:sudo).with("cp /tmp/vagrant_facts.yaml /etc/puppetlabs/facter/facts.d/vagrant_facts.yaml") subject.provision() end it "does not build structured facts if not set" do allow(machine).to receive(:guest).and_return(double("guest")) allow(machine.guest).to receive(:capability?).and_return(false) allow(config).to receive(:environment_path).and_return(environment_paths) allow(config).to receive(:environment).and_return("production") allow(config).to receive(:manifests_path).and_return(manifest_file) allow(config).to receive(:temp_dir).and_return("/tmp") allow(config).to receive(:hiera_config_path).and_return(false) allow(subject).to receive(:parse_environment_metadata).and_return(true) allow(subject).to receive(:verify_binary).and_return(true) allow(subject).to receive(:run_puppet_apply).and_return(true) allow_message_expectations_on_nil allow(@module_paths).to receive(:each) { module_paths } allow(config).to receive(:facter).and_return({"coolfacts"=>"here they are"}) allow(config).to receive(:structured_facts).and_return(nil) expect(machine.communicate).not_to receive(:upload).with(anything, "/tmp/vagrant_facts.yaml") expect(machine.communicate).not_to receive(:sudo).with("cp /tmp/vagrant_facts.yaml /etc/puppetlabs/facter/facts.d/vagrant_facts.yaml") subject.provision() end end end ================================================ FILE: test/unit/plugins/provisioners/salt/bootstrap_downloader_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/provisioners/salt/bootstrap_downloader") describe VagrantPlugins::Salt::BootstrapDownloader do include_context "unit" subject { described_class.new(:computer) } describe "verify_sha256" do let(:sha256) { "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } let(:bad_sha256) { "ffffffffffffffff" } let(:sha256_file) { StringIO.new("#{sha256} test_script_value") } let(:test_script) { StringIO.new("test_script_value") } it "does not error if both shas match" do allow(subject).to receive(:download).and_return(sha256_file) allow(Digest::SHA256).to receive(:hexdigest).and_return(sha256) expect{subject.verify_sha256(test_script)}.to_not raise_error end it "raises an exception if shas don't match" do allow(subject).to receive(:download).and_return(sha256_file) allow(Digest::SHA256).to receive(:hexdigest).and_return(bad_sha256) expect{subject.verify_sha256(test_script)}.to raise_error(VagrantPlugins::Salt::Errors::InvalidShasumError) { |err| expect(err.message).to include("The bootstrap-salt script downloaded from '#{described_class::URL}' couldn't be verified.") expect(err.message).to include("Expected SHA256 '#{sha256}', but computed '#{bad_sha256}'") } end it "raises the correct error message to a windows guest" do subject = described_class.new(:windows) allow(subject).to receive(:download).and_return(sha256_file) allow(Digest::SHA256).to receive(:hexdigest).and_return(bad_sha256) expect{subject.verify_sha256(test_script)}.to raise_error(VagrantPlugins::Salt::Errors::InvalidShasumError) { |err| expect(err.message).to include("The bootstrap-salt script downloaded from '#{described_class::WINDOWS_URL}' couldn't be verified.") } end end end ================================================ FILE: test/unit/plugins/provisioners/salt/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/salt/config") describe VagrantPlugins::Salt::Config do include_context "unit" subject { described_class.new } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } describe "validate" do let(:error_key) { "salt provisioner" } it "passes by default" do subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end context "minion_config" do it "fails if minion_config is set and missing" do subject.minion_config = "/nope/nope/i/dont/exist" subject.finalize! result = subject.validate(machine) expect(result[error_key]).to_not be_empty end it "is valid if is set and not missing" do subject.minion_config = File.expand_path(__FILE__) subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end end context "master_config" do it "fails if master_config is set and missing" do subject.master_config = "/ceci/nest/pas/une/path" subject.finalize! result = subject.validate(machine) expect(result[error_key]).to_not be_empty end it "is valid if is set and not missing" do subject.master_config = File.expand_path(__FILE__) subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end end context "grains_config" do it "fails if grains_config is set and missing" do subject.grains_config = "/nope/still/not/here" subject.finalize! result = subject.validate(machine) expect(result[error_key]).to_not be_empty end it "is valid if is set and not missing" do subject.grains_config = File.expand_path(__FILE__) subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end end context "salt_call_args" do it "fails if salt_call_args is not an array" do subject.salt_call_args = "--flags" subject.finalize! result = subject.validate(machine) expect(result[error_key]).to_not be_empty end it "is valid if is set and not missing" do subject.salt_call_args = ["--flags"] subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end end context "salt_args" do it "fails if not an array" do subject.salt_args = "--flags" subject.finalize! result = subject.validate(machine) expect(result[error_key]).to_not be_empty end it "is valid if is set and not missing" do subject.salt_args = ["--flags"] subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end end context "python_version" do it "is valid if is set and not missing" do subject.python_version = "2" subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end it "can be a string" do subject.python_version = "2" subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end it "can be an integer" do subject.python_version = 2 subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end it "is not a number that is not an integer" do subject.python_version = 2.7 subject.finalize! result = subject.validate(machine) expect(result[error_key]).to_not be_empty end it "is not a string that does not parse to an integer" do subject.python_version = '2.7' subject.finalize! result = subject.validate(machine) expect(result[error_key]).to_not be_empty end end context "version" do it "is valid if is set without install_type on Windows" do allow(machine.config.vm).to receive(:communicator).and_return(:winrm) subject.version = "2018.3.3" subject.finalize! result = subject.validate(machine) expect(result[error_key]).to be_empty end it "is invalid if is set without install_type on Linux" do subject.version = "2018.3.3" subject.finalize! result = subject.validate(machine) expect(result[error_key]).to_not be_empty end end end end ================================================ FILE: test/unit/plugins/provisioners/salt/provisioner_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/provisioners/salt/provisioner") describe VagrantPlugins::Salt::Provisioner do include_context "unit" subject { described_class.new(machine, config) } 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:config) { double("config") } let(:communicator) { double("comm") } let(:guest) { double("guest") } before do allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(communicator).to receive(:execute).and_return(true) allow(communicator).to receive(:upload).and_return(true) allow(guest).to receive(:capability?).and_return(false) end describe "#provision" do context "minion" do let(:python_version) { "2" } before do allow(config).to receive(:seed_master).and_return([]) allow(config).to receive(:install_master).and_return(true) allow(config).to receive(:install_syndic).and_return(true) allow(config).to receive(:no_minion).and_return(true) allow(config).to receive(:python_version).and_return(python_version) allow(config).to receive(:install_type).and_return('stable') allow(config).to receive(:install_args).and_return('develop') allow(config).to receive(:verbose).and_return(true) allow(config).to receive(:master_json_config).and_return(true) allow(config).to receive(:minion_json_config).and_return(true) allow(config).to receive(:bootstrap_options).and_return("") end it "does not add linux-only bootstrap flags when on windows" do additional_windows_options = "-only -these options -should -remain" allow(machine.config.vm).to receive(:communicator).and_return(:winrm) expect(config).to receive(:bootstrap_options).twice.and_return(additional_windows_options) result = subject.bootstrap_options(true, true, "C:\\salttmp") expect(result.strip).to eq(additional_windows_options) end context "python version" do before { allow(communicator).to receive(:sudo) } context "when not set" do let(:python_version) { nil } it "should not include python flag" do result = subject.bootstrap_options(true, true, "/tmp") expect(result).not_to include("-python") end end context "when set" do it "should include python flag" do result = subject.bootstrap_options(true, true, "/tmp") expect(result).to include("python#{python_version}") end end end end end describe "#get_pillar" do context "windows" do it "escapes pillar data for powershell and returns as json" do allow(machine.config.vm).to receive(:communicator).and_return(:winrm) allow(config).to receive(:pillar_data).and_return({"cat"=>"qubit"}) expect(subject.get_pillar).to eq(" --% pillar={\"\"\"cat\"\"\":\"\"\"qubit\"\"\"}") end end context "linux" do it "returns pillar data as json" do allow(machine.config.vm).to receive(:communicator).and_return(:false) allow(config).to receive(:pillar_data).and_return({"cat"=>"shimi"}) expect(subject.get_pillar).to eq(" pillar='{\"cat\":\"shimi\"}'") end end context "empty data" do it "returns nothing if pillar data is empty" do allow(config).to receive(:pillar_data).and_return({}) expect(subject.get_pillar).to eq(nil) end end end describe "#call_highstate" do context "master" do it "passes along extra cli flags" do allow(config).to receive(:run_highstate).and_return(true) allow(config).to receive(:verbose).and_return(true) allow(config).to receive(:masterless).and_return(false) allow(config).to receive(:minion_id).and_return(nil) allow(config).to receive(:log_level).and_return(nil) allow(config).to receive(:colorize).and_return(false) allow(config).to receive(:pillar_data).and_return([]) allow(config).to receive(:install_master).and_return(true) allow(config).to receive(:salt_args).and_return(["--async"]) allow(machine.communicate).to receive(:sudo) allow(machine.config.vm).to receive(:communicator).and_return(:notwinrm) expect(machine.communicate).to receive(:sudo).with("salt '*' state.highstate --verbose --log-level=debug --no-color --async", {:error_key=>:ssh_bad_exit_status_muted}) subject.call_highstate() end it "has no additional cli flags if not included" do allow(config).to receive(:run_highstate).and_return(true) allow(config).to receive(:verbose).and_return(true) allow(config).to receive(:masterless).and_return(false) allow(config).to receive(:minion_id).and_return(nil) allow(config).to receive(:log_level).and_return(nil) allow(config).to receive(:colorize).and_return(false) allow(config).to receive(:pillar_data).and_return([]) allow(config).to receive(:install_master).and_return(true) allow(config).to receive(:salt_args).and_return(nil) allow(machine.communicate).to receive(:sudo) allow(machine.config.vm).to receive(:communicator).and_return(:notwinrm) expect(machine.communicate).to receive(:sudo).with("salt '*' state.highstate --verbose --log-level=debug --no-color ", {:error_key=>:ssh_bad_exit_status_muted}) subject.call_highstate() end end context "with masterless" do it "passes along extra cli flags" do allow(config).to receive(:run_highstate).and_return(true) allow(config).to receive(:verbose).and_return(true) allow(config).to receive(:masterless).and_return(true) allow(config).to receive(:minion_id).and_return(nil) allow(config).to receive(:log_level).and_return(nil) allow(config).to receive(:colorize).and_return(false) allow(config).to receive(:pillar_data).and_return([]) allow(config).to receive(:salt_args).and_return(["--async"]) allow(config).to receive(:salt_call_args).and_return(["--output-dif"]) allow(machine.communicate).to receive(:sudo) allow(machine.config.vm).to receive(:communicator).and_return(:notwinrm) allow(config).to receive(:install_master).and_return(false) expect(machine.communicate).to receive(:sudo).with("salt-call state.highstate --retcode-passthrough --local --log-level=debug --no-color --output-dif", {:error_key=>:ssh_bad_exit_status_muted}) subject.call_highstate() end it "has no additional cli flags if not included" do allow(config).to receive(:run_highstate).and_return(true) allow(config).to receive(:verbose).and_return(true) allow(config).to receive(:masterless).and_return(true) allow(config).to receive(:minion_id).and_return(nil) allow(config).to receive(:log_level).and_return(nil) allow(config).to receive(:colorize).and_return(false) allow(config).to receive(:pillar_data).and_return([]) allow(config).to receive(:salt_call_args).and_return(nil) allow(config).to receive(:salt_args).and_return(nil) allow(machine.communicate).to receive(:sudo) allow(machine.config.vm).to receive(:communicator).and_return(:notwinrm) allow(config).to receive(:install_master).and_return(false) expect(machine.communicate).to receive(:sudo).with("salt-call state.highstate --retcode-passthrough --local --log-level=debug --no-color ", {:error_key=>:ssh_bad_exit_status_muted}) subject.call_highstate() end end end end ================================================ FILE: test/unit/plugins/provisioners/shell/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe "VagrantPlugins::Shell::Config" do let(:described_class) do VagrantPlugins::Shell::Plugin.components.configs[:provisioner][:shell] end let(:machine) { double('machine', env: Vagrant::Environment.new) } let(:file_that_exists) { File.expand_path(__FILE__) } subject { described_class.new } describe "validate" do it "passes with no args" do subject.path = file_that_exists subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to eq([]) end it "passes with string args" do subject.path = file_that_exists subject.args = "a string" subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to eq([]) end it "passes with integer args" do subject.path = file_that_exists subject.args = 1 subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to eq([]) end it "passes with array args" do subject.path = file_that_exists subject.args = ["an", "array"] subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to eq([]) end it "returns an error if args is neither a string nor an array" do neither_array_nor_string = Object.new subject.path = file_that_exists subject.args = neither_array_nor_string subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to eq([ I18n.t("vagrant.provisioners.shell.args_bad_type") ]) end it "handles scalar array args" do subject.path = file_that_exists subject.args = ["string", 1, 2] subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to eq([]) end it "returns an error if args is an array with non-scalar types" do subject.path = file_that_exists subject.args = [[1]] subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to eq([ 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.powershell_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 it "returns an error if the environment is not a hash" do subject.env = "foo" subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to include( I18n.t("vagrant.provisioners.shell.env_must_be_a_hash") ) end it "returns an error if file and script are unset" do subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to include( I18n.t("vagrant.provisioners.shell.no_path_or_inline") ) end it "returns an error if inline and path are both set" do subject.inline = "script" subject.path = "script" result = subject.validate(machine) expect(result["shell provisioner"]).to include( I18n.t("vagrant.provisioners.shell.path_and_inline_set") ) end it "returns no error when inline and path are unset but reset is true" do subject.reset = true subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to be_empty end it "returns no error when inline and path are unset but reboot is true" do subject.reboot = true subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to be_empty end it "returns no error if upload_path is unset" do subject.inline = "script" subject.finalize! result = subject.validate(machine) expect(result["shell provisioner"]).to be_empty end end describe 'finalize!' do it 'changes integer args into strings' do subject.path = file_that_exists subject.args = 1 subject.finalize! expect(subject.args).to eq('1') end it 'changes integer args in arrays into strings' do subject.path = file_that_exists subject.args = ["string", 1, 2] subject.finalize! expect(subject.args).to eq(["string", '1', '2']) end it "no longer sets a default for upload_path" do subject.finalize! expect(subject.upload_path).to eq(nil) end context "with sensitive option enabled" do it 'marks environment variable values sensitive' do subject.env = {"KEY1" => "VAL1", "KEY2" => "VAL2"} subject.sensitive = true expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with("VAL1") expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with("VAL2") subject.finalize! end end context "with sensitive option disabled" do it 'does not mark environment variable values sensitive' do subject.env = {"KEY1" => "VAL1", "KEY2" => "VAL2"} subject.sensitive = false expect(Vagrant::Util::CredentialScrubber).not_to receive(:sensitive) subject.finalize! end end end end ================================================ FILE: test/unit/plugins/provisioners/shell/provisioner_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/provisioners/shell/provisioner") describe "Vagrant::Shell::Provisioner" do include_context "unit" let(:default_win_path) { "C:/tmp/vagrant-shell" } let(:env){ isolated_environment } let(:machine) { double(:machine, env: env, id: "ID").tap { |machine| allow(machine).to receive_message_chain(:config, :vm, :communicator).and_return(:not_winrm) allow(machine).to receive_message_chain(:config, :vm, :guest).and_return(:linux) allow(machine).to receive_message_chain(:communicate, :tap) {} } } before do allow(env).to receive(:tmp_path).and_return(Pathname.new("/dev/null")) end context "when reset is enabled" do let(:path) { nil } let(:inline) { "" } let(:communicator) { double("communicator") } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => path, :inline => inline, :binary => false, :reset => true, :reboot => false, ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } before { allow(machine).to receive(:communicate).and_return(communicator) allow(vsp).to receive(:provision_ssh) } it "should provision and then reset the connection" do expect(vsp).to receive(:provision_ssh) expect(communicator).to receive(:reset!) vsp.provision end context "when path and inline are not set" do let(:path) { nil } let(:inline) { nil } it "should reset the connection and not provision" do expect(vsp).not_to receive(:provision_ssh) expect(communicator).to receive(:reset!) vsp.provision end end end context "when reboot is enabled" do let(:path) { nil } let(:inline) { "" } let(:communicator) { double("communicator") } let(:guest) { double("guest") } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => path, :inline => inline, :binary => false, :reset => false, :reboot => true ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } before { allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(vsp).to receive(:provision_ssh) } it "should provision and then reboot the guest" do expect(vsp).to receive(:provision_ssh) expect(guest).to receive(:capability).with(:reboot) vsp.provision end context "when path and inline are not set" do let(:path) { nil } let(:inline) { nil } it "should reboot the guest and not provision" do expect(vsp).not_to receive(:provision_ssh) expect(guest).to receive(:capability).with(:reboot) vsp.provision end end end context "with a script that contains invalid us-ascii byte sequences" do let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => nil, :inline => script_that_is_incorrectly_us_ascii_encoded, :binary => false, :reset => false, :reboot => false ) } let(:script_that_is_incorrectly_us_ascii_encoded) { [207].pack("c*").force_encoding("US-ASCII") } it "does not raise an exception when normalizing newlines" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect { vsp.provision }.not_to raise_error end end context "with a script that was set to freeze the string" do TEST_CONSTANT_VARIABLE = <<-TEST_CONSTANT_VARIABLE.freeze echo test TEST_CONSTANT_VARIABLE let(:script) { TEST_CONSTANT_VARIABLE } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => nil, :inline => script, :binary => false, :reset => false, :reboot => false ) } it "does not raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) RSpec::Expectations.configuration.on_potential_false_positives = :nothing # This test should be fine, since we are specifically looking for the # string 'freeze' when RuntimeError is raised expect { vsp.provision }.not_to raise_error(RuntimeError) end end context "with remote script" do let(:filechecksum) { double("filechecksum", checksum: checksum_value) } let(:checksum_value) { double("checksum_value") } before do allow(FileChecksum).to receive(:new).and_return(filechecksum) allow_any_instance_of(Vagrant::Util::Downloader).to receive(:execute_curl).and_return(true) end context "that does not have matching sha1 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => nil, :sha1 => 'EXPECTED_VALUE', :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that does not have matching sha256 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => nil, :sha1 => nil, :sha256 => 'EXPECTED_VALUE', :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that does not have matching sha384 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => nil, :sha1 => nil, :sha256 => nil, :sha384 => 'EXPECTED_VALUE', :sha512 => nil, :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that does not have matching sha512 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => nil, :sha1 => nil, :sha256 => nil, :sha384 => nil, :sha512 => 'EXPECTED_VALUE', :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that does not have matching md5 checksum" do let(:checksum_value) { "INVALID_VALUE" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => true, :path => "http://example.com/script.sh", :binary => false, :md5 => 'EXPECTED_VALUE', :sha1 => nil, :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false ) } it "should raise an exception" do vsp = VagrantPlugins::Shell::Provisioner.new(machine, config) expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end end describe "#upload_path" do context "when upload path is not set" do let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => nil, :remote? => false, :path => "doesn't matter", :inline => "doesn't matter", :binary => false, :reset => true, :reboot => false, ) } it "should default to /tmp/vagrant-shell" do expect(vsp.upload_path).to eq("/tmp/vagrant-shell") end context "windows" do before do allow(machine).to receive_message_chain(:config, :vm, :guest).and_return(:windows) end it "should default to C:/tmp/vagrant-shell" do expect(vsp.upload_path).to eq("C:/tmp/vagrant-shell") end end end context "when upload_path is set" do let(:upload_path) { "arbitrary" } let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => upload_path, :remote? => false, :path => "doesn't matter", :inline => "doesn't matter", :binary => false, :reset => true, :reboot => false, ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } it "should use the value from from config" do expect(vsp.upload_path).to eq("arbitrary") end context "windows" do let(:upload_path) { "C:\\Windows\\Temp" } before do allow(machine).to receive_message_chain(:config, :vm, :guest).and_return(:windows) end it "should normalize the slashes" do expect(vsp.upload_path).to eq("C:/Windows/Temp") end end end context "with cached value" do let(:config) { double(:config) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } before do vsp.instance_variable_set(:@_upload_path, "anything") end it "should use cached value" do expect(vsp.upload_path).to eq("anything") end end end describe "#provision_winrm" do let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => "script/info.ps1", :binary => false, :md5 => nil, :sha1 => 'EXPECTED_VALUE', :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false, :powershell_args => "", :name => nil, :privileged => false, :powershell_elevated_interactive => false, :keep_color => true, ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } let(:communicator) { double("communicator") } let(:guest) { double("guest") } let(:ui) { Vagrant::UI::Silent.new } before { allow(guest).to receive(:capability?).with(:wait_for_reboot).and_return(false) allow(communicator).to receive(:sudo) allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(machine).to receive(:ui).and_return(ui) allow(vsp).to receive(:with_script_file).and_yield(config.path) allow(communicator).to receive(:upload).with(config.path, /arbitrary.ps1$/) } it "should output all received output" do stdout = ["two lines\n", "from stdout\n"] stderr = ["one line\n", "and partial from stderr"] expect(communicator).to receive(:sudo). and_yield(:stdout, stdout.first). and_yield(:stderr, stderr.first). and_yield(:stderr, stderr.last). and_yield(:stdout, stdout.last) allow(ui).to receive(:detail) expect(ui).to receive(:detail).with("two lines", any_args) expect(ui).to receive(:detail).with("from stdout", any_args) expect(ui).to receive(:detail).with("one line", any_args) expect(ui).to receive(:detail).with("and partial from stderr", any_args) vsp.send(:provision_winrm, "") end it "ensures that files are uploaded with an extension" do allow(vsp).to receive(:with_script_file).and_yield(config.path) expect(communicator).to receive(:upload).with(config.path, /arbitrary.ps1$/) vsp.send(:provision_winrm, "") end context "bat file being uploaded" do before do allow(config).to receive(:path).and_return("script/info.bat") allow(vsp).to receive(:with_script_file).and_yield(config.path) end it "ensures that files are uploaded same extension as provided path.bat" do expect(communicator).to receive(:upload).with(config.path, /arbitrary/) expect(communicator).to receive(:sudo).with(/arbitrary.bat/, anything) vsp.send(:provision_winrm, "") end end context "inline option set" do let(:config) { double( :config, :args => "doesn't matter", :env => {}, :remote? => false, :inline => "some commands", :upload_path => nil, :path => nil, :binary => false, :md5 => nil, :sha1 => 'EXPECTED_VALUE', :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false, :powershell_args => "", :name => nil, :privileged => false, :powershell_elevated_interactive => false ) } it "creates an executable with an extension" do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return(nil) allow(vsp).to receive(:with_script_file).and_yield(default_win_path) allow(communicator).to receive(:upload).with(default_win_path, /vagrant-shell/) expect(communicator).to receive(:sudo).with(/vagrant-shell.ps1/, anything) vsp.send(:provision_winrm, "") end end end describe "#provision_winssh" do let(:config) { double( :config, :args => "doesn't matter", :env => {}, :upload_path => "arbitrary", :remote? => false, :path => nil, :inline => "something", :binary => false, :md5 => nil, :sha1 => 'EXPECTED_VALUE', :sha256 => nil, :sha384 => nil, :sha512 => nil, :reset => false, :reboot => false, :powershell_args => "", :name => nil, :privileged => false, :powershell_elevated_interactive => false, :keep_color => true, ) } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } let(:communicator) { double("communicator") } let(:guest) { double("guest") } let(:ui) { Vagrant::UI::Silent.new } before { allow(guest).to receive(:capability?).with(:wait_for_reboot).and_return(false) allow(communicator).to receive(:sudo) allow(communicator).to receive(:upload) allow(communicator).to receive_message_chain(:machine_config_ssh, :shell) allow(machine).to receive(:communicate).and_return(communicator) allow(machine).to receive(:guest).and_return(guest) allow(machine).to receive(:ui).and_return(ui) allow(machine).to receive(:ssh_info).and_return(true) } context "ps1 file being uploaded" do before do allow(config).to receive(:path).and_return("script/info.ps1") allow(vsp).to receive(:with_script_file).and_yield(config.path) end it "ensures that files are uploaded same extension as provided path.ps1" do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return("cmd") expect(communicator).to receive(:upload).with(config.path, /arbitrary.ps1/) expect(communicator).to receive(:execute).with(/powershell.*arbitrary.ps1/, anything) vsp.send(:provision_winssh, "") end it "should output all received output" do stdout = ["two lines\n", "from stdout\n"] stderr = ["one line\n", "and partial from stderr"] expect(communicator).to receive(:execute). and_yield(:stdout, stdout.first). and_yield(:stderr, stderr.first). and_yield(:stderr, stderr.last). and_yield(:stdout, stdout.last) allow(ui).to receive(:detail) expect(ui).to receive(:detail).with("two lines", any_args) expect(ui).to receive(:detail).with("from stdout", any_args) expect(ui).to receive(:detail).with("one line", any_args) expect(ui).to receive(:detail).with("and partial from stderr", any_args) vsp.send(:provision_winssh, "") end end context "bat file being uploaded" do before do allow(config).to receive(:path).and_return("script/info.bat") allow(vsp).to receive(:with_script_file).and_yield(config.path) end it "ensures that files are uploaded same extension as provided path.bat" do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return("cmd") expect(communicator).to receive(:upload).with(config.path, /arbitrary.bat/) expect(communicator).to receive(:execute).with(/cmd.*arbitrary.bat/, anything) vsp.send(:provision_winssh, "") end end context "with inline script" do before do allow(vsp).to receive(:with_script_file).and_yield("/tmp/file/contents") end context "when upload path has a .ps1 extension" do before do allow(config).to receive(:upload_path).and_return("c:/tmp/vagrant-shell.ps1") end it "executes the remote script with powershell" do expect(communicator).to receive(:upload).with(anything, config.upload_path) expect(communicator).to receive(:execute).with(/powershell.*\.ps1/, anything) vsp.send(:provision_winssh, "") end end context "when upload path has a .bat extension" do before do allow(config).to receive(:upload_path).and_return("c:/tmp/vagrant-shell.bat") end it "executes the remote script with cmd" do expect(communicator).to receive(:upload).with(anything, config.upload_path) expect(communicator).to receive(:execute).with(/cmd.*\.bat/, anything) vsp.send(:provision_winssh, "") end end context "when upload path has no extension" do before do allow(config).to receive(:upload_path).and_return("c:/tmp/vagrant-shell") end context "when winssh shell is cmd" do before do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return("cmd") end it "adds an extension and executes the remote script with cmd" do expect(communicator).to receive(:upload).with(anything, /\.bat$/) expect(communicator).to receive(:execute).with(/cmd.*\.bat/, anything) vsp.send(:provision_winssh, "") end end context "when winssh shell is powershell" do before do allow(machine).to receive_message_chain(:config, :winssh, :shell).and_return("powershell") end it "adds an extension executes the remote script with powershell" do expect(communicator).to receive(:upload).with(anything, /\.ps1$/) expect(communicator).to receive(:execute).with(/powershell.*\.ps1/, anything) vsp.send(:provision_winssh, "") end end end end end describe "#handle_comm" do let(:ui) { Vagrant::UI::Silent.new } let(:keep_color) { false } let(:config) { double( :config, :keep_color => keep_color, ) } let(:env){ isolated_environment } let(:machine) { double(:machine, env: env, id: "ID") } let(:vsp) { VagrantPlugins::Shell::Provisioner.new(machine, config) } before do allow(machine).to receive(:ui).and_return(ui) end context "when type is stdout" do let(:type) { :stdout } let(:data) { "output data" } it "should output data through the ui" do expect(ui).to receive(:detail).and_call_original vsp.send(:handle_comm, type, data) end it "should color the output" do expect(ui).to receive(:detail).with(data, hash_including(color: :green)). and_call_original vsp.send(:handle_comm, type, data) end context "when configured to keep color" do let(:keep_color) { true } it "should not color the output" do expect(ui).to receive(:detail) do |msg, **opts| expect(msg).to eq(data) expect(opts).to be_empty end vsp.send(:handle_comm, type, data) end end end context "when type is stderr" do let(:type) { :stderr } let(:data) { "output data" } it "should output data through the ui" do expect(ui).to receive(:detail).and_call_original vsp.send(:handle_comm, type, data) end it "should color the output" do expect(ui).to receive(:detail).with(data, hash_including(color: :red)). and_call_original vsp.send(:handle_comm, type, data) end context "when configured to keep color" do let(:keep_color) { true } it "should not color the output" do expect(ui).to receive(:detail) do |msg, **opts| expect(msg).to eq(data) expect(opts).to be_empty end vsp.send(:handle_comm, type, data) end end end context "when type is not stdout or stderr" do let(:type) { :stdnull } let(:data) { "output data" } it "should not output data through the ui" do expect(ui).not_to receive(:detail) vsp.send(:handle_comm, type, data) end end end end ================================================ FILE: test/unit/plugins/provisioners/support/shared/config.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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| it "returns the assigned boolean value (#{bool})" do subject.send("#{attr_name}=", bool) subject.finalize! expect(subject.send(attr_name)).to eql(bool) end end it "returns the default value (#{attr_default_value}) if undefined" do subject.finalize! expect(subject.send(attr_name)).to eql(attr_default_value) end [nil, 'true', 'false', 1, 0, 'this is not a boolean'].each do |nobool| it "returns the default value when assigned value is invalid (#{nobool.class}: #{nobool})" do subject.send("#{attr_name}=", nobool) subject.finalize! expect(subject.send(attr_name)).to eql(attr_default_value) end end end ================================================ FILE: test/unit/plugins/pushes/atlas/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/pushes/atlas/config") describe VagrantPlugins::AtlasPush::Config do include_context "unit" before(:all) do I18n.load_path << Vagrant.source_root.join("plugins/pushes/atlas/locales/en.yml") I18n.reload! end let(:machine) { double("machine") } around(:each) do |example| with_temp_env("ATLAS_TOKEN" => nil) do example.run end end before do subject.token = "foo" end describe "#address" do it "defaults to nil" do subject.finalize! expect(subject.address).to be(nil) end end describe "#app" do it "defaults to nil" do subject.finalize! expect(subject.app).to be(nil) end end describe "#dir" do it "defaults to ." do subject.finalize! expect(subject.dir).to eq(".") end end describe "#vcs" do it "defaults to true" do subject.finalize! expect(subject.vcs).to be(true) end end describe "#uploader_path" do it "defaults to nil" do subject.finalize! expect(subject.uploader_path).to be(nil) end end describe "#validate" do before do allow(machine).to receive(:env) .and_return(double("env", root_path: "", data_dir: Pathname.new(""), )) subject.app = "sethvargo/bacon" subject.dir = "." subject.vcs = true subject.uploader_path = "uploader" end let(:result) { subject.validate(machine) } let(:errors) { result["Atlas push"] } context "when the token is missing" do context "when a vagrant-login token exists" do before do allow(subject).to receive(:token_from_vagrant_login) .and_return("token_from_vagrant_login") end it "uses the token from vagrant-login" do subject.token = "" subject.finalize! expect(errors).to be_empty expect(subject.token).to eq("token_from_vagrant_login") end end context "when a token is given in the Vagrantfile" do before do allow(subject).to receive(:token_from_vagrant_login) .and_return("token_from_vagrant_login") end it "uses the token in the Vagrantfile" do subject.token = "token_from_vagrantfile" subject.finalize! expect(errors).to be_empty expect(subject.token).to eq("token_from_vagrantfile") end end context "when a token is in the environment" do it "uses the token in the Vagrantfile" do with_temp_env("ATLAS_TOKEN" => "foo") do subject.finalize! end expect(errors).to be_empty expect(subject.token).to eq("foo") end end context "when no token is given" do before do allow(subject).to receive(:token_from_vagrant_login) .and_return(nil) end it "returns an error" do subject.token = "" subject.finalize! expect(errors).to include(I18n.t("atlas_push.errors.missing_token")) end end end context "when the app is missing" do it "returns an error" do subject.app = "" subject.finalize! expect(errors).to include(I18n.t("atlas_push.errors.missing_attribute", attribute: "app", )) end end context "when the dir is missing" do it "returns an error" do subject.dir = "" subject.finalize! expect(errors).to include(I18n.t("atlas_push.errors.missing_attribute", attribute: "dir", )) end end context "when the vcs is missing" do it "does not return an error" do subject.vcs = "" subject.finalize! expect(errors).to be_empty end end context "when the uploader_path is missing" do it "returns an error" do subject.uploader_path = "" subject.finalize! expect(errors).to be_empty end end end describe "#merge" do context "when includes are given" do let(:one) { described_class.new } let(:two) { described_class.new } it "merges the result" do one.includes = %w(a b c) two.includes = %w(c d e) result = one.merge(two) expect(result.includes).to eq(%w(a b c d e)) end end context "when excludes are given" do let(:one) { described_class.new } let(:two) { described_class.new } it "merges the result" do one.excludes = %w(a b c) two.excludes = %w(c d e) result = one.merge(two) expect(result.excludes).to eq(%w(a b c d e)) end end end describe "#include" do it "adds the item to the list" do subject.include("me") expect(subject.includes).to include("me") end end describe "#exclude" do it "adds the item to the list" do subject.exclude("not me") expect(subject.excludes).to include("not me") end end end ================================================ FILE: test/unit/plugins/pushes/atlas/push_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/pushes/atlas/config") require Vagrant.source_root.join("plugins/pushes/atlas/push") describe VagrantPlugins::AtlasPush::Push do include_context "unit" let(:bin) { VagrantPlugins::AtlasPush::Push::UPLOADER_BIN } let(:env) do iso_env = isolated_environment iso_env.vagrantfile("") iso_env.create_vagrant_env end let(:config) do VagrantPlugins::AtlasPush::Config.new.tap do |c| c.finalize! end end subject { described_class.new(env, config) } before do # Stub this right away to avoid real execs allow(Vagrant::Util::SafeExec).to receive(:exec) end # DEPRECATED # describe "#push" do # it "pushes with the uploader" do # allow(subject).to receive(:uploader_path).and_return("foo") # expect(subject).to receive(:execute).with("foo") # subject.push # end # it "raises an exception if the uploader couldn't be found" do # expect(subject).to receive(:uploader_path).and_return(nil) # expect { subject.push }.to raise_error( # VagrantPlugins::AtlasPush::Errors::UploaderNotFound) # end # end # describe "#execute" do # let(:app) { "foo/bar" } # before do # config.app = app # end # it "sends the basic flags" do # expect(Vagrant::Util::SafeExec).to receive(:exec). # with("foo", "-vcs", app, env.root_path.to_s) # subject.execute("foo") # end # it "doesn't send VCS if disabled" do # expect(Vagrant::Util::SafeExec).to receive(:exec). # with("foo", app, env.root_path.to_s) # config.vcs = false # subject.execute("foo") # end # it "sends includes" do # expect(Vagrant::Util::SafeExec).to receive(:exec). # with("foo", "-vcs", "-include", "foo", "-include", # "bar", app, env.root_path.to_s) # config.includes = ["foo", "bar"] # subject.execute("foo") # end # it "sends excludes" do # expect(Vagrant::Util::SafeExec).to receive(:exec). # with("foo", "-vcs", "-exclude", "foo", "-exclude", # "bar", app, env.root_path.to_s) # config.excludes = ["foo", "bar"] # subject.execute("foo") # end # it "sends custom server address" do # expect(Vagrant::Util::SafeExec).to receive(:exec). # with("foo", "-vcs", "-address", "foo", app, env.root_path.to_s) # config.address = "foo" # subject.execute("foo") # end # it "sends custom token" do # expect(Vagrant::Util::SafeExec).to receive(:exec). # with("foo", "-vcs", "-token", "atlas_token", app, env.root_path.to_s) # config.token = "atlas_token" # subject.execute("foo") # end # context "when metadata is available" do # let(:env) do # iso_env = isolated_environment # iso_env.vagrantfile <<-EOH # Vagrant.configure("2") do |config| # config.vm.box = "hashicorp/precise64" # config.vm.box_url = "https://atlas.hashicorp.com/hashicorp/precise64" # end # EOH # iso_env.create_vagrant_env # end # it "sends the metadata" do # expect(Vagrant::Util::SafeExec).to receive(:exec). # with("foo", "-vcs", "-metadata", "box=hashicorp/precise64", # "-metadata", "box_url=https://atlas.hashicorp.com/hashicorp/precise64", # "-token", "atlas_token", app, env.root_path.to_s) # config.token = "atlas_token" # subject.execute("foo") # end # end # end # describe "#uploader_path" do # let(:scratch) do # Pathname.new(Dir.mktmpdir("vagrant-test-atlas-push-upload-path")) # end # after do # FileUtils.rm_rf(scratch) # end # it "should return the configured path if set" do # config.uploader_path = "foo" # expect(subject.uploader_path).to eq("foo") # end # it "should look up the uploader via PATH if not set" do # allow(Vagrant).to receive(:in_installer?).and_return(false) # expect(Vagrant::Util::Which).to receive(:which). # with(described_class.const_get(:UPLOADER_BIN)). # and_return("bar") # expect(subject.uploader_path).to eq("bar") # end # it "should look up the uploader in the embedded dir if installer" do # allow(Vagrant).to receive(:in_installer?).and_return(true) # allow(Vagrant).to receive(:installer_embedded_dir).and_return(scratch.to_s) # bin_path = scratch.join("bin", bin) # bin_path.dirname.mkpath # bin_path.open("w+") { |f| f.write("hi") } # expect(subject.uploader_path).to eq(bin_path.to_s) # end # it "should look up the uploader in the PATH if not in the installer" do # allow(Vagrant).to receive(:in_installer?).and_return(true) # allow(Vagrant).to receive(:installer_embedded_dir).and_return(scratch.to_s) # expect(Vagrant::Util::Which).to receive(:which). # with(described_class.const_get(:UPLOADER_BIN)). # and_return("bar") # expect(subject.uploader_path).to eq("bar") # end # it "should return nil if its not found anywhere" do # allow(Vagrant).to receive(:in_installer?).and_return(false) # allow(Vagrant::Util::Which).to receive(:which).and_return(nil) # expect(subject.uploader_path).to be_nil # end # end end ================================================ FILE: test/unit/plugins/pushes/ftp/adapter_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "fake_ftp" require Vagrant.source_root.join("plugins/pushes/ftp/adapter") describe VagrantPlugins::FTPPush::Adapter do include_context "unit" subject do described_class.new("127.0.0.1:2345", "sethvargo", "bacon", foo: "bar", ) end describe "#initialize" do it "sets the instance variables" do expect(subject.host).to eq("127.0.0.1") expect(subject.port).to eq(2345) expect(subject.username).to eq("sethvargo") expect(subject.password).to eq("bacon") expect(subject.options).to eq(foo: "bar") expect(subject.server).to be(nil) end end describe "#parse_host" do it "has a default value" do allow(subject).to receive(:default_port) .and_return(5555) result = subject.parse_host("127.0.0.1") expect(result[0]).to eq("127.0.0.1") expect(result[1]).to eq(5555) end end end describe VagrantPlugins::FTPPush::FTPAdapter do include_context "unit" before(:all) do @server = nil with_random_port do |port1, port2| @server = FakeFtp::Server.new(port1, port2) end @server.start end after(:all) { @server.stop } let(:server) { @server } before { server.reset } subject do described_class.new("127.0.0.1:#{server.port}", "sethvargo", "bacon") end describe "#default_port" do it "is 21" do expect(subject.default_port).to eq(21) end end describe "#upload" do before do @dir = Dir.mktmpdir("vagrant-ftp-push-adapter-upload") FileUtils.touch("#{@dir}/file") end after do FileUtils.rm_rf(@dir) end it "uploads the file" do subject.connect do |ftp| ftp.upload("#{@dir}/file", "/file") end expect(server.files).to include("/file") end it "uploads in passive mode" do subject.options[:passive] = true subject.connect do |ftp| ftp.upload("#{@dir}/file", "/file") end expect(server.file("/file")).to be_passive end end end describe VagrantPlugins::FTPPush::SFTPAdapter do include_context "unit" subject do described_class.new("127.0.0.1:2345", "sethvargo", "bacon", foo: "bar", ) end describe "#default_port" do it "is 22" do expect(subject.default_port).to eq(22) end end describe "#upload" do it "uploads the file" do pending "a way to mock an SFTP server" test_with_mock_sftp_server end end end ================================================ FILE: test/unit/plugins/pushes/ftp/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/pushes/ftp/config") describe VagrantPlugins::FTPPush::Config do include_context "unit" before(:all) do I18n.load_path << Vagrant.source_root.join("plugins/pushes/ftp/locales/en.yml") I18n.reload! end subject { described_class.new } let(:machine) { double("machine") } describe "#host" do it "defaults to nil" do subject.finalize! expect(subject.host).to be(nil) end end describe "#username" do it "defaults to nil" do subject.finalize! expect(subject.username).to be(nil) end end describe "#password" do it "defaults to nil" do subject.finalize! expect(subject.password).to be(nil) end end describe "#passive" do it "defaults to true" do subject.finalize! expect(subject.passive).to be(true) end end describe "#secure" do it "defaults to false" do subject.finalize! expect(subject.secure).to be(false) end end describe "#destination" do it "defaults to /" do subject.finalize! expect(subject.destination).to eq("/") end end describe "#dir" do it "defaults to ." do subject.finalize! expect(subject.dir).to eq(".") end end describe "#merge" do context "when includes are given" do let(:one) { described_class.new } let(:two) { described_class.new } it "merges the result" do one.includes = %w(a b c) two.includes = %w(c d e) result = one.merge(two) expect(result.includes).to eq(%w(a b c d e)) end end context "when excludes are given" do let(:one) { described_class.new } let(:two) { described_class.new } it "merges the result" do one.excludes = %w(a b c) two.excludes = %w(c d e) result = one.merge(two) expect(result.excludes).to eq(%w(a b c d e)) end end end describe "#validate" do before do allow(machine).to receive(:env) .and_return(double("env", root_path: "", )) subject.host = "ftp.example.com" subject.username = "sethvargo" subject.password = "bacon" subject.destination = "/" subject.dir = "." end let(:result) { subject.validate(machine) } let(:errors) { result["FTP push"] } context "when the host is missing" do it "returns an error" do subject.host = "" subject.finalize! expect(errors).to include(I18n.t("ftp_push.errors.missing_attribute", attribute: "host", )) end end context "when the username is missing" do it "returns an error" do subject.username = "" subject.finalize! expect(errors).to include(I18n.t("ftp_push.errors.missing_attribute", attribute: "username", )) end end context "when the password is missing" do it "does not return an error" do subject.password = "" subject.finalize! expect(errors).to be_empty end end context "when the destination is missing" do it "returns an error" do subject.destination = "" subject.finalize! expect(errors).to include(I18n.t("ftp_push.errors.missing_attribute", attribute: "destination", )) end end context "when the dir is missing" do it "returns an error" do subject.dir = "" subject.finalize! expect(errors).to include(I18n.t("ftp_push.errors.missing_attribute", attribute: "dir", )) end end end describe "#include" do it "adds the item to the list" do subject.include("me") expect(subject.includes).to include("me") end end describe "#exclude" do it "adds the item to the list" do subject.exclude("not me") expect(subject.excludes).to include("not me") end end end ================================================ FILE: test/unit/plugins/pushes/ftp/push_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "fake_ftp" require Vagrant.source_root.join("plugins/pushes/ftp/push") describe VagrantPlugins::FTPPush::Push do include_context "unit" let(:env) { isolated_environment } let(:config) do double("config", host: "127.0.0.1:#{@port}", username: "sethvargo", password: "bacon", passive: false, secure: false, destination: "/var/www/site", ) end let(:ui) { Vagrant::UI::Silent.new } subject { described_class.new(env, config) } before do allow(env).to receive(:root_path) .and_return(File.expand_path("..", __FILE__)) allow(env).to receive(:ui) .and_return(ui) end describe "#push" do before(:all) do @server = nil with_random_port do |port1, port2| @port = port1 @server = FakeFtp::Server.new(port1, port2) end @server.start @dir = Dir.mktmpdir("vagrant-ftp-push") FileUtils.touch("#{@dir}/.hidden.rb") FileUtils.touch("#{@dir}/application.rb") FileUtils.touch("#{@dir}/config.rb") FileUtils.touch("#{@dir}/Gemfile") FileUtils.touch("#{@dir}/data.txt") FileUtils.mkdir("#{@dir}/empty_folder") end after(:all) do FileUtils.rm_rf(@dir) @server.stop end let(:server) { @server } before do allow(config).to receive(:dir) .and_return(@dir) allow(config).to receive(:includes) .and_return([]) allow(config).to receive(:excludes) .and_return(%w(*.rb)) end after do server.reset end it "pushes the files to the server" do subject.push expect(server.files).to eq(%w[/var/www/site/Gemfile /var/www/site/data.txt]) end it "raises informative exception when too many files to process" do expect(subject).to receive(:all_files).and_raise(SystemStackError) expect{ subject.push }.to raise_error(VagrantPlugins::FTPPush::Errors::TooManyFiles) end context "when VAGRANT_CWD is set to something relative" do # this will be the PWD for the test context let(:pwd) { Pathname.new(Dir.mktmpdir("vagrant-ftp-push-pwd")) } before do # this path should have a ../ in it since the pwd is another temp dir # and measuring path from one to the other. they're most likely to be # siblings relative_path = Pathname.new(@dir).relative_path_from(pwd) allow(env).to receive(:root_path) { relative_path.to_s } # reset config.dir to its default since it was set absolute above allow(config).to receive(:dir) { "." } end it "properly paths out the files to upload" do Vagrant::Util::SafeChdir.safe_chdir(pwd) do subject.push end expect(server.files).to eq(%w(/var/www/site/Gemfile /var/www/site/data.txt)) ensure FileUtils.rm_rf(pwd) end end end describe "#connect" do before do allow_any_instance_of(VagrantPlugins::FTPPush::FTPAdapter) .to receive(:connect) .and_yield(:ftp) allow_any_instance_of(VagrantPlugins::FTPPush::SFTPAdapter) .to receive(:connect) .and_yield(:sftp) end context "when secure is requested" do before do allow(config).to receive(:secure) .and_return(true) end it "yields a new SFTPAdapter" do expect { |b| subject.connect(&b) }.to yield_with_args(:sftp) end end context "when secure is not requested" do before do allow(config).to receive(:secure) .and_return(false) end it "yields a new FTPAdapter" do expect { |b| subject.connect(&b) }.to yield_with_args(:ftp) end end end describe "#all_files" do before(:all) do @dir = Dir.mktmpdir("vagrant-ftp-push-push-all-files") FileUtils.touch("#{@dir}/.hidden.rb") FileUtils.touch("#{@dir}/application.rb") FileUtils.touch("#{@dir}/config.rb") FileUtils.touch("#{@dir}/Gemfile") FileUtils.mkdir("#{@dir}/empty_folder") FileUtils.mkdir("#{@dir}/folder") FileUtils.mkdir("#{@dir}/folder/.git") FileUtils.touch("#{@dir}/folder/.git/config") FileUtils.touch("#{@dir}/folder/server.rb") end after(:all) do FileUtils.rm_rf(@dir) end let(:files) do subject.all_files.map do |file| file.sub("#{@dir}/", "") end end before do allow(config).to receive(:dir) .and_return(@dir) allow(config).to receive(:includes) .and_return(%w(not_a_file.rb still_not_a_file.rb)) allow(config).to receive(:excludes) .and_return(%w(*.rb)) end it "returns the list of real files + includes, without excludes" do expect(files).to eq(%w( Gemfile folder/.git/config )) end end describe "#includes_files" do before(:all) do @dir = Dir.mktmpdir("vagrant-ftp-push-includes-files") FileUtils.touch("#{@dir}/.hidden.rb") FileUtils.touch("#{@dir}/application.rb") FileUtils.touch("#{@dir}/config.rb") FileUtils.touch("#{@dir}/Gemfile") FileUtils.mkdir("#{@dir}/folder") FileUtils.mkdir("#{@dir}/folder/.git") FileUtils.touch("#{@dir}/folder/.git/config") FileUtils.touch("#{@dir}/folder/server.rb") end after(:all) do FileUtils.rm_rf(@dir) end let(:files) do subject.includes_files.map do |file| file.sub("#{@dir}/", "") end end before do allow(config).to receive(:dir) .and_return(@dir) end def set_includes(value) allow(config).to receive(:includes) .and_return(value) end it "includes the file" do set_includes(["Gemfile"]) expect(files).to eq(%w( Gemfile )) end it "includes the files that are subdirectories" do set_includes(["folder"]) expect(files).to eq(%w( folder folder/.git folder/.git/config folder/server.rb )) end it "includes files that match a pattern" do set_includes(["*.rb"]) expect(files).to eq(%w( .hidden.rb application.rb config.rb )) end end describe "#filter_excludes" do let(:dir) { "/root/dir" } let(:list) do %W( #{dir}/.hidden.rb #{dir}/application.rb #{dir}/config.rb #{dir}/Gemfile #{dir}/folder #{dir}/folder/.git #{dir}/folder/.git/config #{dir}/folder/server.rb /path/outside/you.rb /path/outside/me.rb /path/outside/folder/bacon.rb ) end before do allow(config).to receive(:dir) .and_return(dir) end it "excludes files" do subject.filter_excludes!(list, %w(*.rb)) expect(list).to eq(%W( #{dir}/Gemfile #{dir}/folder #{dir}/folder/.git #{dir}/folder/.git/config )) end it "excludes files in a directory" do subject.filter_excludes!(list, %w(folder)) expect(list).to eq(%W( #{dir}/.hidden.rb #{dir}/application.rb #{dir}/config.rb #{dir}/Gemfile /path/outside/you.rb /path/outside/me.rb /path/outside/folder/bacon.rb )) end it "excludes specific files in a directory" do subject.filter_excludes!(list, %w(/path/outside/folder/*.rb)) expect(list).to eq(%W( #{dir}/.hidden.rb #{dir}/application.rb #{dir}/config.rb #{dir}/Gemfile #{dir}/folder #{dir}/folder/.git #{dir}/folder/.git/config #{dir}/folder/server.rb /path/outside/you.rb /path/outside/me.rb )) end it "excludes files outside the #dir" do subject.filter_excludes!(list, %w(/path/outside)) expect(list).to eq(%W( #{dir}/.hidden.rb #{dir}/application.rb #{dir}/config.rb #{dir}/Gemfile #{dir}/folder #{dir}/folder/.git #{dir}/folder/.git/config #{dir}/folder/server.rb )) end end end ================================================ FILE: test/unit/plugins/pushes/heroku/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/pushes/heroku/config") describe VagrantPlugins::HerokuPush::Config do include_context "unit" before(:all) do I18n.load_path << Vagrant.source_root.join("plugins/pushes/heroku/locales/en.yml") I18n.reload! end subject { described_class.new } let(:machine) { double("machine") } describe "#app" do it "defaults to nil" do subject.finalize! expect(subject.app).to be(nil) end end describe "#dir" do it "defaults to ." do subject.finalize! expect(subject.dir).to eq(".") end end describe "#git_bin" do it "defaults to git" do subject.finalize! expect(subject.git_bin).to eq("git") end end describe "#remote" do it "defaults to git" do subject.finalize! expect(subject.remote).to eq("heroku") end end describe "#validate" do before do allow(machine).to receive(:env) .and_return(double("env", root_path: "", )) subject.app = "bacon" subject.dir = "." subject.git_bin = "git" subject.remote = "heroku" end let(:result) { subject.validate(machine) } let(:errors) { result["Heroku push"] } context "when the app is missing" do it "does not return an error" do subject.app = "" subject.finalize! expect(errors).to be_empty end end context "when the git_bin is missing" do it "returns an error" do subject.git_bin = "" subject.finalize! expect(errors).to include(I18n.t("heroku_push.errors.missing_attribute", attribute: "git_bin", )) end end context "when the remote is missing" do it "returns an error" do subject.remote = "" subject.finalize! expect(errors).to include(I18n.t("heroku_push.errors.missing_attribute", attribute: "remote", )) end end context "when the dir is missing" do it "returns an error" do subject.dir = "" subject.finalize! expect(errors).to include(I18n.t("heroku_push.errors.missing_attribute", attribute: "dir", )) end end end end ================================================ FILE: test/unit/plugins/pushes/heroku/push_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/platform" require Vagrant.source_root.join("plugins/pushes/heroku/push") describe VagrantPlugins::HerokuPush::Push do include_context "unit" before(:all) do I18n.load_path << Vagrant.source_root.join("plugins/pushes/heroku/locales/en.yml") I18n.reload! end let(:env) { isolated_environment } let(:config) do double("config", app: "bacon", dir: "lib", git_bin: "git", remote: "heroku", ) end subject { described_class.new(env, config) } describe "#push" do let(:branch) { "master" } let(:dir) { "#{root_path}/#{config.dir}" } let(:root_path) do next "/handy/dandy" if !Vagrant::Util::Platform.windows? "C:/handy/dandy" end before do allow(subject).to receive(:git_branch) .and_return(branch) allow(subject).to receive(:verify_git_bin!) allow(subject).to receive(:verify_git_repo!) allow(subject).to receive(:has_git_remote?) allow(subject).to receive(:add_heroku_git_remote) allow(subject).to receive(:git_push_heroku) allow(subject).to receive(:execute!) allow(env).to receive(:root_path) .and_return(root_path) end it "verifies the git bin is present" do expect(subject).to receive(:verify_git_bin!) .with(config.git_bin) subject.push end it "verifies the directory is a git repo" do expect(subject).to receive(:verify_git_repo!) .with(dir) subject.push end context "when the heroku remote exists" do before do allow(subject).to receive(:has_git_remote?) .and_return(true) end it "does not add the heroku remote" do expect(subject).to_not receive(:add_heroku_git_remote) subject.push end end context "when the heroku remote does not exist" do before do allow(subject).to receive(:has_git_remote?) .and_return(false) end it "adds the heroku remote" do expect(subject).to receive(:add_heroku_git_remote) .with(config.remote, config.app, dir) subject.push end end it "pushes to heroku" do expect(subject).to receive(:git_push_heroku) .with(config.remote, branch, dir) subject.push end end describe "#verify_git_bin!" do context "when git does not exist" do before do allow(Vagrant::Util::Which).to receive(:which) .with("git") .and_return(nil) end it "raises an exception" do expect { subject.verify_git_bin!("git") } .to raise_error(VagrantPlugins::HerokuPush::Errors::GitNotFound) { |error| expect(error.message).to eq(I18n.t("heroku_push.errors.git_not_found", bin: "git", )) } end end context "when git exists" do before do allow(Vagrant::Util::Which).to receive(:which) .with("git") .and_return("git") end it "does not raise an exception" do expect { subject.verify_git_bin!("git") }.to_not raise_error end end end describe "#verify_git_repo!" do context "when the path is a git repo" do before do allow(File).to receive(:directory?) .with("/repo/path/.git") .and_return(false) end it "raises an exception" do expect { subject.verify_git_repo!("/repo/path") } .to raise_error(VagrantPlugins::HerokuPush::Errors::NotAGitRepo) { |error| expect(error.message).to eq(I18n.t("heroku_push.errors.not_a_git_repo", path: "/repo/path", )) } end end context "when the path is not a git repo" do before do allow(File).to receive(:directory?) .with("/repo/path/.git") .and_return(true) end it "does not raise an exception" do expect { subject.verify_git_repo!("/repo/path") }.to_not raise_error end end end describe "#git_push_heroku" do let(:dir) { "." } before { allow(subject).to receive(:execute!) } it "executes the proper command" do expect(subject).to receive(:execute!) .with("git", "--git-dir", "#{dir}/.git", "--work-tree", dir, "push", "bacon", "hamlet:master", ) subject.git_push_heroku("bacon", "hamlet", dir) end end describe "#has_git_remote?" do let(:dir) { "." } let(:process) do double("process", stdout: "origin\r\nbacon\nhello" ) end before do allow(subject).to receive(:execute!) .and_return(process) end it "executes the proper command" do expect(subject).to receive(:execute!) .with("git", "--git-dir", "#{dir}/.git", "--work-tree", dir, "remote", ) subject.has_git_remote?("bacon", dir) end it "returns true when the remote exists" do expect(subject.has_git_remote?("origin", dir)).to be(true) expect(subject.has_git_remote?("bacon", dir)).to be(true) expect(subject.has_git_remote?("hello", dir)).to be(true) end it "returns false when the remote does not exist" do expect(subject.has_git_remote?("nope", dir)).to be(false) end end describe "#add_heroku_git_remote" do let(:dir) { "." } before do allow(subject).to receive(:execute!) allow(subject).to receive(:heroku_git_url) .with("app") .and_return("HEROKU_URL") end it "executes the proper command" do expect(subject).to receive(:execute!) .with("git", "--git-dir", "#{dir}/.git", "--work-tree", dir, "remote", "add", "bacon", "HEROKU_URL", ) subject.add_heroku_git_remote("bacon", "app", dir) end end describe "#interpret_app" do it "returns the basename of the directory" do expect(subject.interpret_app("/foo/bar/blitz")).to eq("blitz") end end describe "#heroku_git_url" do it "returns the proper string" do expect(subject.heroku_git_url("bacon")) .to eq("git@heroku.com:bacon.git") end end describe "#git_dir" do it "returns the .git directory for the path" do expect(subject.git_dir("/path")).to eq("/path/.git") end end describe "#git_branch" do let(:stdout) { "" } let(:process) { double("process", stdout: stdout) } before do allow(subject).to receive(:execute!) .and_return(process) end let(:branch) { subject.git_branch("/path") } context "when the branch is not prefixed" do let(:stdout) { "bacon" } it "returns the correct name" do expect(branch).to eq("bacon") end end end describe "#execute!" do let(:exit_code) { 0 } let(:stdout) { "This is the output" } let(:stderr) { "This is the errput" } let(:process) do double("process", exit_code: exit_code, stdout: stdout, stderr: stderr, ) end before do allow(Vagrant::Util::Subprocess).to receive(:execute) .and_return(process) end it "creates a subprocess" do expect(Vagrant::Util::Subprocess).to receive(:execute) expect { subject.execute! }.to_not raise_error end it "returns the resulting process" do expect(subject.execute!).to be(process) end context "when the exit code is non-zero" do let(:exit_code) { 1 } it "raises an exception" do klass = VagrantPlugins::HerokuPush::Errors::CommandFailed cmd = ["foo", "bar"] expect { subject.execute!(*cmd) }.to raise_error(klass) { |error| expect(error.message).to eq(I18n.t("heroku_push.errors.command_failed", cmd: cmd.join(" "), stdout: stdout, stderr: stderr, )) } end end end end ================================================ FILE: test/unit/plugins/pushes/local-exec/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/pushes/local-exec/config") describe VagrantPlugins::LocalExecPush::Config do include_context "unit" before(:all) do I18n.load_path << Vagrant.source_root.join("plugins/pushes/local-exec/locales/en.yml") I18n.reload! end let(:machine) { double("machine") } describe "#script" do it "defaults to nil" do subject.finalize! expect(subject.script).to be(nil) end end describe "#inline" do it "defaults to nil" do subject.finalize! expect(subject.inline).to be(nil) end end describe "#args" do it "defaults to nil" do subject.finalize! expect(subject.args).to be(nil) end end describe "#validate" do before do allow(machine).to receive(:env) .and_return(double("env", root_path: "", )) subject.finalize! end let(:result) { subject.validate(machine) } let(:errors) { result["Local Exec push"] } context "when script is present" do before { subject.script = "foo.sh" } context "when inline is present" do before { subject.inline = "echo" } it "returns an error" do expect(errors).to include( I18n.t("local_exec_push.errors.cannot_specify_script_and_inline") ) end end context "when inline is not present" do before { subject.inline = "" } it "does not return an error" do expect(errors).to be_empty end it "passes with string args" do subject.args = "a string" expect(errors).to be_empty end it "passes with integer args" do subject.args = 1 expect(errors).to be_empty end it "passes with array args" do subject.args = ["an", "array"] expect(errors).to be_empty end it "returns an error if args is neither a string nor an array" do neither_array_nor_string = Object.new subject.args = neither_array_nor_string expect(errors).to include( I18n.t("local_exec_push.errors.args_bad_type") ) end it "handles scalar array args" do subject.args = ["string", 1, 2] expect(errors).to be_empty end it "returns an error if args is an array with non-scalar types" do subject.args = [[1]] expect(errors).to include( I18n.t("local_exec_push.errors.args_bad_type") ) end end end context "when script is not present" do before { subject.script = "" } context "when inline is present" do before { subject.inline = "echo" } it "does not return an error" do expect(errors).to be_empty end it "passes with string args" do subject.args = "a string" expect(errors).to be_empty end it "passes with integer args" do subject.args = 1 expect(errors).to be_empty end it "passes with array args" do subject.args = ["an", "array"] expect(errors).to be_empty end it "returns an error if args is neither a string nor an array" do neither_array_nor_string = Object.new subject.args = neither_array_nor_string expect(errors).to include( I18n.t("local_exec_push.errors.args_bad_type") ) end it "handles scalar array args" do subject.args = ["string", 1, 2] expect(errors).to be_empty end it "returns an error if args is an array with non-scalar types" do subject.args = [[1]] expect(errors).to include( I18n.t("local_exec_push.errors.args_bad_type") ) end end context "when inline is not present" do before { subject.inline = "" } it "returns an error" do expect(errors).to include(I18n.t("local_exec_push.errors.missing_attribute", attribute: "script", )) end end end end end ================================================ FILE: test/unit/plugins/pushes/local-exec/push_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/pushes/local-exec/push") describe VagrantPlugins::LocalExecPush::Push do include_context "unit" before(:all) do I18n.load_path << Vagrant.source_root.join("plugins/pushes/local-exec/locales/en.yml") I18n.reload! end let(:env) { isolated_environment } let(:config) do double("config", script: nil, inline: nil, args: "some args", ) end subject { described_class.new(env, config) } before do allow(env).to receive(:root_path) .and_return(File.expand_path("..", __FILE__)) end describe "#push" do before do allow(subject).to receive(:execute_inline!) allow(subject).to receive(:execute_script!) allow(subject).to receive(:execute!) end context "when inline is given" do before { allow(config).to receive(:inline).and_return("echo") } it "executes the inline script" do expect(subject).to receive(:execute_inline!) .with(config.inline, config.args) subject.push end end context "when script is given" do before { allow(config).to receive(:script).and_return("foo.sh") } it "executes the script" do expect(subject).to receive(:execute_script!) .with(config.script, config.args) subject.push end end end describe "#execute_inline!" do before { allow(subject).to receive(:execute_script!) } it "writes the script to a tempfile" do expect(Tempfile).to receive(:new).and_call_original subject.execute_inline!("echo", config.args) end it "executes the script" do expect(subject).to receive(:execute_script!) subject.execute_inline!("echo", config.args) end end describe "#execute_script!" do before do allow(subject).to receive(:execute!) allow(FileUtils).to receive(:chmod) end it "expands the path relative to the machine root" do expect(subject).to receive(:execute!) .with(File.expand_path("foo.sh", env.root_path)) subject.execute_script!("./foo.sh", nil) end it "makes the file executable" do expect(FileUtils).to receive(:chmod) .with("+x", File.expand_path("foo.sh", env.root_path)) subject.execute_script!("./foo.sh", config.args) end it "calls execute!" do expect(subject).to receive(:execute!) .with(File.expand_path("foo.sh", env.root_path)) subject.execute_script!("./foo.sh", nil) end context "when args is given" do it "passes string args to execute!" do expect(subject).to receive(:execute!) .with(File.expand_path("foo.sh", env.root_path) + " " + config.args) subject.execute_script!("./foo.sh", config.args) end it "passes array args as string to execute!" do expect(subject).to receive(:execute!) .with(File.expand_path("foo.sh", env.root_path) + " \"one\" \"two\" \"three\"") subject.execute_script!("./foo.sh", ["one", "two", "three"]) end end end describe "#execute!" do it "uses exec on unix" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(false) expect(Vagrant::Util::SafeExec).to receive(:exec) expect { subject.execute! }.to_not raise_error end it "uses subprocess on windows" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) result = double("result", exit_code: 0) expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(result) expect { subject.execute! }.to raise_error { |e| expect(e).to be_a(SystemExit) } end end end ================================================ FILE: test/unit/plugins/pushes/noop/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/pushes/noop/config") describe VagrantPlugins::NoopDeploy::Config do include_context "unit" subject { described_class.new } let(:machine) { double("machine") } describe "#validate" do end end ================================================ FILE: test/unit/plugins/synced_folders/nfs/action_cleanup_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/synced_folders/nfs/action_cleanup") describe VagrantPlugins::SyncedFolderNFS::ActionCleanup 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(:host) { double("host") } let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:app) { lambda {} } let(:env) { { machine: machine, } } subject { described_class.new(app, env) } before do allow(machine.env).to receive(:host).and_return(host) end it "does nothing if there are no valid IDs" do expect(app).to receive(:call).with(env) subject.call(env) end it "does nothing if the host doesn't support pruning NFS" do allow(host).to receive(:capability?).with(:nfs_prune).and_return(false) expect(host).to receive(:capability).never expect(app).to receive(:call).with(env) subject.call(env) end it "prunes the NFS entries if valid IDs are given" do env[:nfs_valid_ids] = [1,2,3] allow(host).to receive(:capability?).with(:nfs_prune).and_return(true) expect(host).to receive(:capability).with(:nfs_prune, machine.ui, [1,2,3]).ordered expect(app).to receive(:call).with(env).ordered subject.call(env) end end ================================================ FILE: test/unit/plugins/synced_folders/nfs/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/synced_folders/nfs/config") describe VagrantPlugins::SyncedFolderNFS::Config do subject { described_class.new } context "defaults" do before do subject.finalize! end its(:functional) { should be(true) } its(:map_gid) { should eq(:auto) } its(:map_uid) { should eq(:auto) } end end ================================================ FILE: test/unit/plugins/synced_folders/rsync/command/rsync_auto_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/synced_folders/rsync/command/rsync_auto") describe VagrantPlugins::SyncedFolderRSync::Command::RsyncAuto do include_context "unit" let(:argv) { [] } 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(:synced_folders_empty) { {} } let(:synced_folders_dupe) { {"1234": {type: "rsync", exclude: false, hostpath: "/Users/brian/code/vagrant-sandbox"}, "5678": {type: "rsync", exclude: false, hostpath: "/Not/The/Same/Path"}, "0912": {type: "rsync", exclude: false, hostpath: "/Users/brian/code/relative-dir"}}} let(:helper_class) { VagrantPlugins::SyncedFolderRSync::RsyncHelper } let(:paths) { {} } let(:ssh_info) {{}} def machine_stub(name) double(name).tap do |m| allow(m).to receive(:id).and_return("foo") allow(m).to receive(:reload).and_return(nil) allow(m).to receive(:ssh_info).and_return(ssh_info) allow(m).to receive(:ui).and_return(iso_env.ui) allow(m).to receive(:provider).and_return(double("provider")) allow(m).to receive(:state).and_return(double("state", id: :not_created)) allow(m).to receive(:env).and_return(iso_env) allow(m).to receive(:config).and_return(double("config")) end end subject do described_class.new(argv, iso_env).tap end describe "#execute" do let (:machine) { machine_stub("m") } let (:cached_folders) { { rsync: synced_folders_dupe } } # NOTE: `relative-dir` is not actually a "relative dir" in this data structure # due to the fact that when vagrant stores synced folders, it path expands # them with root_dir, and when you grab those synced_folders options from # the machines config file, they end up being a full path rather than a # relative path, and so these tests reflect that. # For reference: # https://github.com/hashicorp/vagrant/blob/9c1b014536e61b332cfaa00774a87a240cce8ed9/lib/vagrant/action/builtin/synced_folders.rb#L45-L46 let(:config_synced_folders) { {"/vagrant": {type: "rsync", hostpath: "/Users/brian/code/vagrant-sandbox"}, "/vagrant/other-dir": {type: "rsync", hostpath: "/Users/brian/code/vagrant-sandbox/other-dir"}, "/vagrant/relative-dir": {type: "rsync", hostpath: "/Users/brian/code/relative-dir"}}} before do allow(subject).to receive(:with_target_vms) { |&block| block.call machine } allow(machine.state).to receive(:id).and_return(:created) allow(machine.env).to receive(:cwd). and_return("/Users/brian/code/vagrant-sandbox") allow(machine.provider).to receive(:capability?).and_return(false) allow(machine.config).to receive(:vm).and_return(double("vm")) allow(machine.config.vm).to receive(:synced_folders).and_return(config_synced_folders) allow(subject).to receive(:synced_folders). with(machine, cached: true).and_return(cached_folders) allow(helper_class).to receive(:rsync_single).and_return(true) allow(Vagrant::Util::Busy).to receive(:busy).and_return(true) allow(Listen).to receive(:to).and_return(true) end it "does not sync folders outside of the cwd" do allow(machine.ui).to receive(:info).and_call_original expect(machine.ui).to receive(:info). with("Not syncing /Not/The/Same/Path as it is not part of the current working directory."). and_call_original expect(machine.ui).to receive(:info). with("Watching: /Users/brian/code/vagrant-sandbox"). and_call_original expect(machine.ui).to receive(:info). with("Watching: /Users/brian/code/relative-dir"). and_call_original expect(helper_class).to receive(:rsync_single) expect(Listen).to receive(:to). with("/Users/brian/code/vagrant-sandbox", "/Users/brian/code/relative-dir", {:ignore=>[/.vagrant\//], :force_polling=>false}) subject.execute end context "with --rsync-chown option" do let(:argv) { ["--rsync-chown"] } it "should enable rsync_ownership on folder options" do expect(helper_class).to receive(:rsync_single). with(anything, anything, hash_including(rsync_ownership: true)) subject.execute end end end subject do described_class.new(argv, iso_env).tap do |s| allow(s).to receive(:synced_folders).and_return(synced_folders_empty) end end describe "#callback" do it "syncs modified folders to the proper path" do paths["/foo"] = [ { machine: machine_stub("m1"), opts: double("opts_m1") }, { machine: machine_stub("m2"), opts: double("opts_m2") }, ] paths["/bar"] = [ { machine: machine_stub("m3"), opts: double("opts_m3") }, ] paths["/foo"].each do |data| expect(helper_class).to receive(:rsync_single). with(data[:machine], data[:machine].ssh_info, data[:opts]). once end m = ["/foo/bar"] a = [] r = [] subject.callback(paths, m, a, r) end it "syncs added folders to the proper path" do paths["/foo"] = [ { machine: machine_stub("m1"), opts: double("opts_m1") }, { machine: machine_stub("m2"), opts: double("opts_m2") }, ] paths["/bar"] = [ { machine: machine_stub("m3"), opts: double("opts_m3") }, ] paths["/foo"].each do |data| expect(helper_class).to receive(:rsync_single). with(data[:machine], data[:machine].ssh_info, data[:opts]). once end m = [] a = ["/foo/bar"] r = [] subject.callback(paths, m, a, r) end it "syncs removed folders to the proper path" do paths["/foo"] = [ { machine: machine_stub("m1"), opts: double("opts_m1") }, { machine: machine_stub("m2"), opts: double("opts_m2") }, ] paths["/bar"] = [ { machine: machine_stub("m3"), opts: double("opts_m3") }, ] paths["/foo"].each do |data| expect(helper_class).to receive(:rsync_single). with(data[:machine], data[:machine].ssh_info, data[:opts]). once end m = [] a = [] r = ["/foo/bar"] subject.callback(paths, m, a, r) end it "doesn't fail if guest error occurs" do paths["/foo"] = [ { machine: machine_stub("m1"), opts: double("opts_m1") }, { machine: machine_stub("m2"), opts: double("opts_m2") }, ] paths["/bar"] = [ { machine: machine_stub("m3"), opts: double("opts_m3") }, ] paths["/foo"].each do |data| expect(helper_class).to receive(:rsync_single). with(data[:machine], data[:machine].ssh_info, data[:opts]). and_raise(Vagrant::Errors::MachineGuestNotReady) end m = [] a = [] r = ["/foo/bar"] expect { subject.callback(paths, m, a, r) }. to_not raise_error end it "doesn't sync machines with no ID" do paths["/foo"] = [ { machine: machine_stub("m1"), opts: double("opts_m1") }, ] paths["/foo"].each do |data| allow(data[:machine]).to receive(:id).and_return(nil) expect(helper_class).to_not receive(:rsync_single) end m = [] a = [] r = ["/foo/bar"] expect { subject.callback(paths, m, a, r) }. to_not raise_error end context "on failure" do let(:machine) { machine_stub("m1") } let(:opts) { double("opts_m1") } let(:paths) { {"/foo" => [machine: machine, opts: opts]} } let(:args) { [paths, ["/foo/bar"], [], []] } before do allow_any_instance_of(Vagrant::Errors::VagrantError). to receive(:translate_error) end context "when rsync command fails" do before do expect(helper_class).to receive(:rsync_single).with(machine, machine.ssh_info, opts). and_raise(Vagrant::Errors::RSyncError) end it "should notify on error" do expect(machine.ui).to receive(:error).and_call_original subject.callback(*args) end it "should not raise error" do expect { subject.callback(*args) }.not_to raise_error end end context "when rsync post command capability fails" do before do expect(helper_class).to receive(:rsync_single).with(machine, machine.ssh_info, opts). and_raise(Vagrant::Errors::RSyncPostCommandError) end it "should notify on error" do expect(machine.ui).to receive(:error).and_call_original subject.callback(*args) end it "should not raise error" do expect { subject.callback(*args) }.not_to raise_error end end end end end ================================================ FILE: test/unit/plugins/synced_folders/rsync/command/rsync_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require Vagrant.source_root.join("plugins/synced_folders/rsync/command/rsync") describe VagrantPlugins::SyncedFolderRSync::Command::Rsync do include_context "unit" let(:argv) { [] } 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(:communicator) { double("comm") } let(:synced_folders) { {} } let(:helper_class) { VagrantPlugins::SyncedFolderRSync::RsyncHelper } subject do described_class.new(argv, iso_env).tap do |s| allow(s).to receive(:synced_folders).and_return(synced_folders) end end before do iso_env.machine_names.each do |name| m = iso_env.machine(name, iso_env.default_provider) allow(m).to receive(:communicate).and_return(communicator) end end describe "#execute" do context "with a single machine" do let(:ssh_info) {{ private_key_path: [], }} let(:machine) { iso_env.machine(iso_env.machine_names[0], iso_env.default_provider) } before do allow(communicator).to receive(:ready?).and_return(true) allow(machine).to receive(:ssh_info).and_return(ssh_info) synced_folders[:rsync] = [ [:one, {}], [:two, {}], ] end it "doesn't sync if communicator isn't ready and exits with 1" do allow(communicator).to receive(:ready?).and_return(false) expect(helper_class).to receive(:rsync_single).never expect(subject.execute).to eql(1) end it "rsyncs each folder and exits successfully" do synced_folders[:rsync].each do |_, opts| expect(helper_class).to receive(:rsync_single). with(machine, ssh_info, opts). ordered end expect(subject.execute).to eql(0) end context "with --rsync-chown option" do let(:argv) { ["--rsync-chown"] } it "should enable rsync_ownership on folder options" do synced_folders[:rsync].each do |_, opts| expect(helper_class).to receive(:rsync_single). with(machine, ssh_info, hash_including(rsync_ownership: true)). ordered end subject.execute end end end end end ================================================ FILE: test/unit/plugins/synced_folders/rsync/default_unix_cap_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/synced_folders/rsync/default_unix_cap") describe VagrantPlugins::SyncedFolderRSync::DefaultUnixCap 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } let(:subject) { Class.new { extend VagrantPlugins::SyncedFolderRSync::DefaultUnixCap } } describe "#rsync_installed" do it "tests if rsync is on the path" do expect(machine.communicate).to receive(:test).with("which rsync"). and_return(true) subject.rsync_installed(machine) end end describe "#rsync_command" do it "returns the rsync command" do expect( subject.rsync_command(machine) ).to eq("sudo rsync") end end describe "#rsync_post" do let(:opts) {{:type=>:rsync, :guestpath=>"/vagrant", :hostpath=>"/home/user/syncfolder", :disabled=>false, :__vagrantfile=>true, :exclude=>[".vagrant"], :owner=>"vagrant", :group=>"vagrant"}} let(:cmd) { "find /vagrant -path /vagrant/.vagrant -prune -o '!' -type l -a '(' ! -user vagrant -or ! -group vagrant ')' -exec chown vagrant:vagrant '{}' +" } it "executes the rsync post command" do expect(machine.communicate).to receive(:sudo). with(cmd) subject.rsync_post(machine, opts) end end describe "#build_rsync_chown" do let(:opts) {{:type=>:rsync, :guestpath=>"/vagrant", :hostpath=>"/home/user/syncfolder", :disabled=>false, :__vagrantfile=>true, :exclude=>[".vagrant"], :owner=>"vagrant", :group=>"vagrant"}} let(:cmd) { "find /vagrant -path /vagrant/.vagrant -prune -o '!' -type l -a '(' ! -user vagrant -or ! -group vagrant ')' -exec chown vagrant:vagrant '{}' +" } let(:no_exclude_cmd) { "find /vagrant '!' -type l -a '(' ! -user vagrant -or ! -group vagrant ')' -exec chown vagrant:vagrant '{}' +" } let(:empty_opts) {{:type=>:rsync, :guestpath=>"/vagrant", :hostpath=>"/home/user/syncfolder", :disabled=>false, :__vagrantfile=>true, :exclude=>[], :owner=>"vagrant", :group=>"vagrant"}} it "builds up a command to properly chown folders" do command = subject.build_rsync_chown(opts) expect(command).to eq(cmd) end it "does not include any excludes if the array is empty" do command = subject.build_rsync_chown(empty_opts) expect(command).to eq(no_exclude_cmd) end end end ================================================ FILE: test/unit/plugins/synced_folders/rsync/helper_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/platform" require Vagrant.source_root.join("plugins/synced_folders/rsync/helper") describe VagrantPlugins::SyncedFolderRSync::RsyncHelper 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(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } subject { described_class } before do allow(machine).to receive(:guest).and_return(guest) # Don't do all the crazy Cygwin stuff allow(Vagrant::Util::Platform).to receive(:cygwin_path) do |path, **opts| path end end describe "#exclude_to_regexp" do let(:path) { "/foo/bar" } it "converts a directory match" do expected_regex = /foo\/.*/ expect(described_class.exclude_to_regexp("foo/")). to eq(/foo\/.*/) expect(path).to match(expected_regex) end it "converts the start anchor" do expected_regex = /^\/foo\// expect(described_class.exclude_to_regexp("/foo")). to eq(expected_regex) expect(path).to match(expected_regex) end it "converts the **" do expected_regex = /fo.*o.*/ expect(described_class.exclude_to_regexp("fo**o")). to eq(expected_regex) expect(path).to match(expected_regex) end it "converts the *" do expected_regex = /fo*o.*/ expect(described_class.exclude_to_regexp("fo*o")). to eq(expected_regex) expect(path).to match(expected_regex) end end describe "#rsync_single" do let(:result) { Vagrant::Util::Subprocess::Result.new(0, "", "") } let(:ssh_info) {{ private_key_path: [], }} let(:opts) {{ hostpath: "/foo", }} let(:ui) { machine.ui } before do allow(Vagrant::Util::Subprocess).to receive(:execute){ result } allow(guest).to receive(:capability?){ false } end it "doesn't raise an error if it succeeds" do subject.rsync_single(machine, ssh_info, opts) end it "doesn't call cygwin_path on non-Windows" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(false) expect(Vagrant::Util::Platform).not_to receive(:cygwin_path) subject.rsync_single(machine, ssh_info, opts) end it "calls cygwin_path on Windows" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) expect(Vagrant::Util::Platform).to receive(:cygwin_path).and_return("foo") expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[args.length - 3]).to eql("foo/") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "raises an error if the exit code is non-zero" do allow(Vagrant::Util::Subprocess).to receive(:execute) .and_return(Vagrant::Util::Subprocess::Result.new(1, "", "")) expect {subject.rsync_single(machine, ssh_info, opts) }. to raise_error(Vagrant::Errors::RSyncError) end context "host and guest paths" do it "syncs the hostpath to the guest path" do opts[:hostpath] = "/foo" opts[:guestpath] = "/bar" expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expected = Vagrant::Util::Platform.fs_real_path("/foo").to_s expect(args[args.length - 3]).to eql("#{expected}/") expect(args[args.length - 2]).to include("/bar") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "expands the hostpath relative to the root path" do opts[:hostpath] = "foo" opts[:guestpath] = "/bar" hostpath_expanded = File.expand_path(opts[:hostpath], machine.env.root_path) expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[args.length - 3]).to eql("#{hostpath_expanded}/") expect(args[args.length - 2]).to include("/bar") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end end it "executes within the root path" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args.last).to be_kind_of(Hash) opts = args.last expect(opts[:workdir]).to eql(machine.env.root_path.to_s) }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "executes the rsync_pre capability first if it exists" do expect(guest).to receive(:capability?).with(:rsync_pre).and_return(true) expect(guest).to receive(:capability).with(:rsync_pre, opts).ordered expect(Vagrant::Util::Subprocess).to receive(:execute).ordered.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "executes the rsync_post capability after if it exists" do expect(guest).to receive(:capability?).with(:rsync_post).and_return(true) expect(Vagrant::Util::Subprocess).to receive(:execute).ordered.and_return(result) expect(guest).to receive(:capability).with(:rsync_post, opts).ordered subject.rsync_single(machine, ssh_info, opts) end context "with rsync_post capability" do before do allow(guest).to receive(:capability?).with(:rsync_post).and_return(true) allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(result) end it "should raise custom error when capability errors" do expect(guest).to receive(:capability).with(:rsync_post, opts). and_raise(Vagrant::Errors::VagrantError) expect { subject.rsync_single(machine, ssh_info, opts) }. to raise_error(Vagrant::Errors::RSyncPostCommandError) end it "should populate :owner and :group from ssh_info[:username] when values are nil" do opts[:owner] = nil opts[:group] = nil ssh_info[:username] = "userfromssh" expect(guest).to receive(:capability).with(:rsync_post, a_hash_including( owner: "userfromssh", group: "userfromssh", )) subject.rsync_single(machine, ssh_info, opts) end end context "with rsync_ownership option" do let(:rsync_local_version) { "3.1.1" } let(:rsync_remote_version) { "3.1.1" } let(:rsync_result) { Vagrant::Util::Subprocess::Result.new(0, "", "") } before do expect(Vagrant::Util::Subprocess).to receive(:execute). with("rsync", "--version").and_return(Vagrant::Util::Subprocess::Result.new(0, " version #{rsync_local_version} ", "")) allow(machine.communicate).to receive(:execute).with(/--version/).and_yield(:stdout, " version #{rsync_remote_version} ") allow(Vagrant::Util::Subprocess).to receive(:execute).with("rsync", any_args).and_return(rsync_result) opts[:rsync_ownership] = true end after { subject.reset! } it "should use the rsync --chown flag" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args.detect{|a| a.include?("--chown")}).to be_truthy rsync_result } subject.rsync_single(machine, ssh_info, opts) end it "should set the chown option to false" do expect(opts.has_key?(:chown)).to eq(false) subject.rsync_single(machine, ssh_info, opts) expect(opts[:chown]).to eq(false) end context "when local rsync version does not support --chown" do let(:rsync_local_version) { "2.0" } it "should not use the --chown flag" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args.detect{|a| a.include?("--chown")}).to be_falsey rsync_result } subject.rsync_single(machine, ssh_info, opts) end end context "when remote rsync version does not support --chown" do let(:rsync_remote_version) { "2.0" } it "should not use the --chown flag" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| expect(args.detect{|a| a.include?("--chown")}).to be_falsey rsync_result } subject.rsync_single(machine, ssh_info, opts) end end end context "excluding files" do it "excludes files if given as a string" do opts[:exclude] = "foo" expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| index = args.find_index("foo") expect(index).to be > 0 expect(args[index-1]).to eql("--exclude") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "excludes multiple files" do opts[:exclude] = ["foo", "bar"] expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| index = args.find_index("foo") expect(index).to be > 0 expect(args[index-1]).to eql("--exclude") index = args.find_index("bar") expect(index).to be > 0 expect(args[index-1]).to eql("--exclude") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end end context "custom arguments" do it "uses the default arguments if not given" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[1]).to eq("--verbose") expect(args[2]).to eq("--archive") expect(args[3]).to eq("--delete") expected = Vagrant::Util::Platform.fs_real_path("/foo").to_s expect(args[args.length - 3]).to eql("#{expected}/") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "uses the custom arguments if given" do opts[:args] = ["--verbose", "-z"] expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[1]).to eq("--verbose") expect(args[2]).to eq("-z") expected = Vagrant::Util::Platform.fs_real_path("/foo").to_s expect(args[args.length - 3]).to eql("#{expected}/") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end end context "control sockets" do it "creates a tmp dir" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(false) allow(Dir).to receive(:mktmpdir).with("vagrant-rsync-"). and_return("/tmp/vagrant-rsync-12345") expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[9]).to include("ControlPath=/tmp/vagrant-rsync-12345") }.and_return(result) expect(FileUtils).to receive(:remove_entry_secure).with("/tmp/vagrant-rsync-12345", true).and_return(true) subject.rsync_single(machine, ssh_info, opts) end it "does not create tmp dir on windows platforms" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) allow(Dir).to receive(:mktmpdir).with("vagrant-rsync-"). and_return("/tmp/vagrant-rsync-12345") expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args).not_to include("ControlPath=/tmp/vagrant-rsync-12345") }.and_return(result) expect(FileUtils).not_to receive(:remove_entry_secure).with("/tmp/vagrant-rsync-12345", true) subject.rsync_single(machine, ssh_info, opts) end end end describe "#rsync_single with custom ssh_info" do let(:result) { Vagrant::Util::Subprocess::Result.new(0, "", "") } let(:ssh_info) {{ :private_key_path => ['/path/to/key'], :keys_only => true, :verify_host_key => false, }} let(:opts) {{ hostpath: "/foo", }} let(:ui) { machine.ui } before do allow(Vagrant::Util::Subprocess).to receive(:execute){ result } allow(guest).to receive(:capability?){ false } end context "with extra args defined" do before { ssh_info[:extra_args] = ["-o", "Compression=yes"] } it "appends the extra arguments from ssh_info" do expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args| cmd = args.detect { |a| a.is_a?(String) && a.start_with?("ssh") } expect(cmd).to be expect(cmd).to include("-o Compression=yes") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end end context "with an IPv6 address" do before { ssh_info[:host] = "fe00::0" } it "formats the address correctly" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args, "@[#{ssh_info[:host]}]:''", instance_of(Hash)) subject.rsync_single(machine, ssh_info, opts) end end context "with an IPv4 address" do before { ssh_info[:host] = "127.0.0.1" } it "formats the address correctly" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args, "@#{ssh_info[:host]}:''", instance_of(Hash)) subject.rsync_single(machine, ssh_info, opts) end end it "includes IdentitiesOnly, StrictHostKeyChecking, and UserKnownHostsFile with defaults" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[9]).to include('IdentitiesOnly') expect(args[9]).to include('StrictHostKeyChecking') expect(args[9]).to include('UserKnownHostsFile') expect(args[9]).to include("-i '/path/to/key'") }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "includes StrictHostKeyChecking, and UserKnownHostsFile when verify_host_key is false" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[9]).to include('StrictHostKeyChecking') expect(args[9]).to include('UserKnownHostsFile') }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "includes StrictHostKeyChecking, and UserKnownHostsFile when verify_host_key is :never" do ssh_info[:verify_host_key] = :never expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| expect(args[9]).to include('StrictHostKeyChecking') expect(args[9]).to include('UserKnownHostsFile') }.and_return(result) subject.rsync_single(machine, ssh_info, opts) end it "omits IdentitiesOnly with keys_only = false" do ssh_info[:keys_only] = false expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| expect(args[9]).not_to include('IdentitiesOnly') result end subject.rsync_single(machine, ssh_info, opts) end it "omits StrictHostKeyChecking and UserKnownHostsFile with paranoid = true" do ssh_info[:keys_only] = false expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| expect(args[9]).not_to include('StrictHostKeyChecking ') expect(args[9]).not_to include('UserKnownHostsFile ') result end subject.rsync_single(machine, ssh_info, opts) end it "includes custom ssh config when set" do ssh_info[:config] = "/path/to/ssh/config" expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| ssh_config_args = "-F /path/to/ssh/config" expect(args.any?{|a| a.include?(ssh_config_args)}).to be_truthy result end subject.rsync_single(machine, ssh_info, opts) end end describe ".rsync_chown_support?" do let(:local_version) { "3.1.1" } let(:remote_version) { "3.1.1" } before do allow(subject).to receive(:local_rsync_version).and_return(local_version) allow(subject).to receive(:machine_rsync_version).and_return(remote_version) end it "should return when local and remote versions support chown" do expect(subject.rsync_chown_support?(machine)).to be_truthy end context "when local version does not support chown" do let(:local_version) { "2.0" } it "should return false" do expect(subject.rsync_chown_support?(machine)).to be_falsey end end context "when remote version does not support chown" do let(:remote_version) { "2.0" } it "should return false" do expect(subject.rsync_chown_support?(machine)).to be_falsey end end context "when both local and remote versions do not support chown" do let(:local_version) { "2.0" } let(:remote_version) { "2.0" } it "should return false" do expect(subject.rsync_chown_support?(machine)).to be_falsey end end end describe ".machine_rsync_version" do let(:version_output) { <<-EOV rsync version 3.1.3 protocol version 31 Copyright (C) 1996-2018 by Andrew Tridgell, Wayne Davison, and others. Web site: http://rsync.samba.org/ Capabilities: 64-bit files, 64-bit inums, 64-bit timestamps, 64-bit long ints, socketpairs, hardlinks, symlinks, IPv6, batchfiles, inplace, append, ACLs, xattrs, iconv, symtimes, prealloc rsync comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the GNU General Public Licence for details. EOV } before do allow(machine.communicate).to receive(:execute).with(/--version/). and_yield(:stdout, version_output) allow(guest).to receive(:capability?).and_return(false) end it "should extract the version string" do expect(subject.machine_rsync_version(machine)).to eq("3.1.3") end context "when version output is an unknown format" do let(:version_output) { "unknown" } it "should return nil value" do expect(subject.machine_rsync_version(machine)).to be_nil end end context "with guest rsync_command capability" do let(:rsync_path) { "custom_rsync" } before do allow(guest).to receive(:capability?).with(:rsync_command). and_return(true) allow(guest).to receive(:capability).with(:rsync_command). and_return(rsync_path) end it "should use custom rsync_path" do expect(machine.communicate).to receive(:execute). with("#{rsync_path} --version").and_yield(:stdout, version_output) subject.machine_rsync_version(machine) end end end describe ".local_rsync_version" do let(:version_output) { <<-EOV rsync version 3.1.3 protocol version 31 Copyright (C) 1996-2018 by Andrew Tridgell, Wayne Davison, and others. Web site: http://rsync.samba.org/ Capabilities: 64-bit files, 64-bit inums, 64-bit timestamps, 64-bit long ints, socketpairs, hardlinks, symlinks, IPv6, batchfiles, inplace, append, ACLs, xattrs, iconv, symtimes, prealloc rsync comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. See the GNU General Public Licence for details. EOV } let(:result) { Vagrant::Util::Subprocess::Result.new(0, version_output, "") } before do allow(Vagrant::Util::Subprocess).to receive(:execute).with("rsync", "--version"). and_return(result) end after { subject.reset! } it "should extract the version string" do expect(subject.local_rsync_version).to eq("3.1.3") end it "should cache the version lookup" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("rsync", "--version"). and_return(result).once expect(subject.local_rsync_version).to eq("3.1.3") expect(subject.local_rsync_version).to eq("3.1.3") end context "when version output is an unknown format" do let(:version_output) { "unknown" } it "should return nil value" do expect(subject.local_rsync_version).to be_nil end end end end ================================================ FILE: test/unit/plugins/synced_folders/rsync/synced_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/synced_folders/rsync/synced_folder") describe VagrantPlugins::SyncedFolderRSync::SyncedFolder 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(:helper_class) { VagrantPlugins::SyncedFolderRSync::RsyncHelper } before do allow(machine.env).to receive(:host).and_return(host) allow(machine).to receive(:guest).and_return(guest) end describe "#usable?" do it "is usable if rsync can be found" do expect(Vagrant::Util::Which).to receive(:which).with("rsync").and_return(true) expect(subject.usable?(machine)).to be(true) end it "is not usable if rsync cant be found" do expect(Vagrant::Util::Which).to receive(:which).with("rsync").and_return(false) expect(subject.usable?(machine)).to be(false) end it "raises an exception if asked to" do expect(Vagrant::Util::Which).to receive(:which).with("rsync").and_return(false) expect { subject.usable?(machine, true) }. to raise_error(Vagrant::Errors::RSyncNotFound) end end describe "#enable" do let(:ssh_info) {{ private_key_path: [], }} before do allow(machine).to receive(:ssh_info).and_return(ssh_info) allow(guest).to receive(:capability?).with(:rsync_installed) end it "rsyncs each folder" do folders = [ [:one, {}], [:two, {}], ] folders.each do |_, opts| expect(helper_class).to receive(:rsync_single). with(machine, ssh_info, opts). ordered end subject.enable(machine, folders, {}) end it "installs rsync if capable" do folders = [ [:foo, {}] ] allow(helper_class).to receive(:rsync_single) allow(guest).to receive(:capability?).with(:rsync_installed).and_return(true) allow(guest).to receive(:capability?).with(:rsync_install).and_return(true) expect(guest).to receive(:capability).with(:rsync_installed).and_return(false) expect(guest).to receive(:capability).with(:rsync_install) subject.enable(machine, folders, {}) end it "errors if rsync not installable" do folders = [ [:foo, {}] ] allow(helper_class).to receive(:rsync_single) allow(guest).to receive(:capability?).with(:rsync_installed).and_return(true) allow(guest).to receive(:capability?).with(:rsync_install).and_return(false) expect(guest).to receive(:capability).with(:rsync_installed).and_return(false) expect { subject.enable(machine, folders, {}) }. to raise_error(Vagrant::Errors::RSyncNotInstalledInGuest) end end end ================================================ FILE: test/unit/plugins/synced_folders/smb/caps/mount_options_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require_relative "../../../../../../plugins/synced_folders/smb/cap/mount_options" describe VagrantPlugins::SyncedFolderSMB::Cap::MountOptions do let(:caps) do VagrantPlugins::SyncedFolderSMB::Plugin .components .synced_folder_capabilities[:smb] end let(:cap){ caps.get(:mount_options) } let(:dummy_smb_host) { "my.smb.host" } let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:env) { double("env") } let(:guest) { double("guest") } let(:host) { double("host") } let(:mount_owner){ "vagrant" } let(:mount_group){ "vagrant" } let(:mount_uid){ "1000" } let(:mount_gid){ "1000" } let(:mount_name){ "vagrant" } let(:mount_guest_path){ "/vagrant" } let(:folder_options) do { owner: mount_owner, group: mount_group, hostpath: "/host/directory/path" } end before do allow(guest).to receive(:capability).with(:choose_addressable_ip_addr, any_args).and_return(dummy_smb_host) allow(host).to receive(:capability).with(:configured_ip_addresses) allow(env).to receive(:host).and_return(host) allow(machine).to receive(:env).and_return(env) allow(machine).to receive(:communicate).and_return(comm) allow(machine).to receive(:guest).and_return(guest) allow(machine).to receive_message_chain(:env, :host, :capability?).with(:smb_mount_options).and_return(false) allow(ENV).to receive(:[]).with("VAGRANT_DISABLE_SMBMFSYMLINKS").and_return(true) allow(ENV).to receive(:[]).with("GEM_SKIP").and_return(false) end describe ".mount_name" do it "generates the mount name when smb_host and smb_id are set" do folder_options[:smb_host] = "smb_host" folder_options[:smb_id] = "smbid" mn = cap.mount_name(machine, "", folder_options) expect(mn).to eq("//smb_host/smbid") end it "generates the mount name when smb_host is not set" do folder_options[:smb_id] = "smbid" mn = cap.mount_name(machine, "", folder_options) expect(mn).to eq("//#{dummy_smb_host}/smbid") end end describe ".mount_options" do context "with valid existent owner group" do before do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_yield(:stdout, "vagrant:x:#{mount_gid}:") end it "generates the expected default mount command" do out_opts, out_uid, out_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options) expect(out_opts).to eq("sec=ntlmssp,credentials=/etc/smb_creds_vagrant,uid=1000,gid=1000,_netdev") expect(out_uid).to eq(mount_uid) expect(out_gid).to eq(mount_gid) end it "includes provided mount options" do folder_options[:mount_options] =["ro"] out_opts, out_uid, out_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options) expect(out_opts).to eq("sec=ntlmssp,credentials=/etc/smb_creds_vagrant,uid=1000,gid=1000,_netdev,ro") expect(out_uid).to eq(mount_uid) expect(out_gid).to eq(mount_gid) end it "overwrites default mount options" do folder_options[:mount_options] =["ro", "sec=custom"] out_opts, out_uid, out_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options) expect(out_opts).to eq("sec=custom,credentials=/etc/smb_creds_vagrant,uid=1000,gid=1000,_netdev,ro") expect(out_uid).to eq(mount_uid) expect(out_gid).to eq(mount_gid) end it "does not add mfsymlinks option if env var VAGRANT_DISABLE_SMBMFSYMLINKS exists" do expect(ENV).to receive(:[]).with("VAGRANT_DISABLE_SMBMFSYMLINKS").and_return(false) out_opts, out_uid, out_gid = cap.mount_options(machine, mount_name, mount_guest_path, folder_options) expect(out_opts).to eq("sec=ntlmssp,credentials=/etc/smb_creds_vagrant,uid=1000,gid=1000,mfsymlinks,_netdev") expect(out_uid).to eq(mount_uid) expect(out_gid).to eq(mount_gid) end end context "with non-existent owner group" do it "raises an error" do expect(comm).to receive(:execute).with("id -u #{mount_owner}", anything).and_yield(:stdout, mount_uid) expect(comm).to receive(:execute).with("id -g #{mount_group}", anything).and_yield(:stdout, mount_gid) expect(comm).to receive(:execute).with("getent group #{mount_group}", anything).and_raise(Vagrant::Errors::VirtualBoxMountFailed, {command: '', output: ''}) expect do cap.mount_options(machine, mount_name, mount_guest_path, folder_options) end.to raise_error Vagrant::Errors::VirtualBoxMountFailed end end end end ================================================ FILE: test/unit/plugins/synced_folders/smb/synced_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require Vagrant.source_root.join("plugins/synced_folders/smb/synced_folder") describe VagrantPlugins::SyncedFolderSMB::SyncedFolder do include_context "unit" let(:iso_env) do 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(:host_caps){ [] } let(:guest_caps){ [] } let(:folders){ {"/first/path" => {}, "/second/path" => {}} } let(:options){ {} } before do allow(machine.env).to receive(:host).and_return(host) allow(machine).to receive(:guest).and_return(guest) allow(machine).to receive(:ssh_info).and_return(username: 'sshuser') allow(guest).to receive(:name).and_return("guest_name") allow(host).to receive(:capability?).and_return(false) host_caps.each do |cap| allow(host).to receive(:capability?).with(cap).and_return(true) allow(host).to receive(:capability).with(cap, any_args).and_return(true) end allow(guest).to receive(:capability?).and_return(false) guest_caps.each do |cap| allow(guest).to receive(:capability?).with(cap).and_return(true) allow(guest).to receive(:capability).with(cap, any_args).and_return(true) end end describe "#usable?" do context "without supporting capabilities" do it "is not usable" do expect(subject.usable?(machine)).to be(false) end it "raises exception when raise_error enabled" do expect{subject.usable?(machine, true)}.to raise_error( VagrantPlugins::SyncedFolderSMB::Errors::SMBNotSupported) end end context "with smb not installed" do let(:host_caps){ [:smb_installed] } it "is not usable" do expect(host).to receive(:capability).with(:smb_installed).and_return(false) expect(subject.usable?(machine)).to be(false) end end context "with smb installed" do let(:host_caps){ [:smb_installed] } it "is usable" do expect(subject.usable?(machine)).to be(true) end end end describe "#prepare" do let(:host_caps){ [:smb_start, :smb_prepare] } context "with username credentials provided" do let(:folders){ {'/first/path' => {smb_username: 'smbuser'}} } it "should prompt for credentials" do expect(machine.env.ui).to receive(:ask).with(/name/, any_args).and_return('username').at_least(1) expect(machine.env.ui).to receive(:ask).with(/word/, any_args).and_return('password').at_least(1) subject.prepare(machine, folders, options) end it "should set credential information into all folder options and override username" do expect(machine.env.ui).to receive(:ask).with(/name/, any_args).and_return('username').at_least(1) expect(machine.env.ui).to receive(:ask).with(/word/, any_args).and_return('password').at_least(1) subject.prepare(machine, folders, options) expect(folders['/first/path'][:smb_username]).to eq('username') expect(folders['/first/path'][:smb_password]).to eq('password') end it "will use configured default with no input" do expect(machine.env.ui).to receive(:ask).with(/name/, any_args).and_return('').at_least(1) expect(machine.env.ui).to receive(:ask).with(/word/, any_args).and_return('password').at_least(1) subject.prepare(machine, folders, options) expect(folders['/first/path'][:smb_username]).to eq('smbuser') expect(folders['/first/path'][:smb_password]).to eq('password') end end context "without credentials provided" do before do expect(machine.env.ui).to receive(:ask).with(/name/, any_args).and_return('username').at_least(1) expect(machine.env.ui).to receive(:ask).with(/word/, any_args).and_return('password').at_least(1) end it "should prompt for credentials" do subject.prepare(machine, folders, options) end it "should set credential information into all folder options" do subject.prepare(machine, folders, options) expect(folders['/first/path'][:smb_username]).to eq('username') expect(folders['/first/path'][:smb_password]).to eq('password') expect(folders['/second/path'][:smb_username]).to eq('username') expect(folders['/second/path'][:smb_password]).to eq('password') end it "should start the SMB service if capability is available" do expect(host).to receive(:capability).with(:smb_start, any_args) subject.prepare(machine, folders, options) end context "with host smb_validate_password capability" do let(:host_caps){ [:smb_start, :smb_prepare, :smb_validate_password] } it "should validate the password" do expect(host).to receive(:capability).with(:smb_validate_password, machine, 'username', 'password').and_return(true) subject.prepare(machine, folders, options) end it "should retry when validation fails" do expect(host).to receive(:capability).with(:smb_validate_password, machine, 'username', 'password').and_return(false) expect(host).to receive(:capability).with(:smb_validate_password, machine, 'username', 'password').and_return(true) subject.prepare(machine, folders, options) end it "should raise an error if it exceeds the maximum number of retries" do expect(host).to receive(:capability).with(:smb_validate_password, machine, 'username', 'password').and_return(false). exactly(VagrantPlugins::SyncedFolderSMB::SyncedFolder::CREDENTIAL_RETRY_MAX).times expect{ subject.prepare(machine, folders, options) }.to raise_error(VagrantPlugins::SyncedFolderSMB::Errors::CredentialsRequestError) end end end context "with credentials provided" do context "in single share entry" do let(:folders){ {'/first/path' => {}, '/second/path' => {smb_username: 'smbuser', smb_password: 'smbpass'}} } it "should not prompt for credentials" do expect(machine.env.ui).not_to receive(:ask) subject.prepare(machine, folders, options) end it "should add existing credentials to folder options without" do subject.prepare(machine, folders, options) expect(folders['/first/path'][:smb_username]).to eq('smbuser') expect(folders['/first/path'][:smb_password]).to eq('smbpass') end end context "in both entries" do let(:folders){ {'/first/path' => {smb_username: 'user', smb_password: 'pass'}, '/second/path' => {smb_username: 'smbuser', smb_password: 'smbpass'}} } it "should not modify existing credentials" do subject.prepare(machine, folders, options) expect(folders['/first/path'][:smb_username]).to eq('user') expect(folders['/first/path'][:smb_password]).to eq('pass') expect(folders['/second/path'][:smb_username]).to eq('smbuser') expect(folders['/second/path'][:smb_password]).to eq('smbpass') end it "should register passwords with scrubber" do expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with('pass') expect(Vagrant::Util::CredentialScrubber).to receive(:sensitive).with('smbpass') subject.prepare(machine, folders, options) end end end end describe "#enable" do it "fails when guest does not support capability" do expect{ subject.enable(machine, folders, options) }.to raise_error(Vagrant::Errors::GuestCapabilityNotFound) end context "with guest capability supported" do let(:guest_caps){ [:mount_smb_shared_folder, :choose_addressable_ip_addr] } let(:host_caps){ [:configured_ip_addresses] } it "should attempt to install smb on guest" do expect(guest).to receive(:capability?).with(:smb_install).and_return(true) expect(guest).to receive(:capability).with(:smb_install, any_args) subject.enable(machine, folders, options) end it "should request host IP addresses" do expect(host).to receive(:capability).with(:configured_ip_addresses) subject.enable(machine, folders, options) end it "should determine guest accessible address" do expect(guest).to receive(:capability).with(:choose_addressable_ip_addr, any_args) subject.enable(machine, folders, options) end it "should error if no guest accessible address is available" do expect(guest).to receive(:capability).with(:choose_addressable_ip_addr, any_args).and_return(nil) expect{ subject.enable(machine, folders, options) }.to raise_error( VagrantPlugins::SyncedFolderSMB::Errors::NoHostIPAddr) end it "should default owner and group to ssh username" do subject.enable(machine, folders, options) expect(folders["/first/path"][:owner]).to eq("sshuser") expect(folders["/first/path"][:group]).to eq("sshuser") expect(folders["/second/path"][:owner]).to eq("sshuser") expect(folders["/second/path"][:group]).to eq("sshuser") end it "should set the host address in folder options" do expect(guest).to receive(:capability).with(:choose_addressable_ip_addr, any_args).and_return("ADDR") subject.enable(machine, folders, options) expect(folders["/first/path"][:smb_host]).to eq("ADDR") expect(folders["/second/path"][:smb_host]).to eq("ADDR") end it "should scrub folder configuration" do expect(subject).to receive(:clean_folder_configuration).at_least(:once) subject.enable(machine, folders, options) end context "with smb_host option set" do let(:folders){ {"/first/path" => {smb_host: "ADDR"}, "/second/path" => {}} } it "should not update the value" do expect(guest).to receive(:capability).with(:choose_addressable_ip_addr, any_args).and_return("OTHER") subject.enable(machine, folders, options) expect(folders["/first/path"][:smb_host]).to eq("ADDR") expect(folders["/second/path"][:smb_host]).to eq("OTHER") end end context "with owner and group set" do let(:folders){ {"/first/path" => {owner: "smbowner"}, "/second/path" => {group: "smbgroup"}} } it "should not update set owner or group" do subject.enable(machine, folders, options) expect(folders["/first/path"][:owner]).to eq("smbowner") expect(folders["/first/path"][:group]).to eq("sshuser") expect(folders["/second/path"][:owner]).to eq("sshuser") expect(folders["/second/path"][:group]).to eq("smbgroup") end end context "with smb_username and smb_password set" do let(:folders){ { "/first/path" => {owner: "smbowner", smb_username: "user", smb_password: "pass"}, "/second/path" => {group: "smbgroup", smb_username: "user", smb_password: "pass"} } } it "should retain non password configuration options" do subject.enable(machine, folders, options) folder1 = folders["/first/path"] folder2 = folders["/second/path"] expect(folder1.key?(:owner)).to be_truthy expect(folder1.key?(:smb_username)).to be_truthy expect(folder2.key?(:group)).to be_truthy expect(folder2.key?(:smb_username)).to be_truthy end it "should remove the smb_password option when set" do subject.enable(machine, folders, options) expect(folders["/first/path"].key?(:smb_password)).to be_falsey expect(folders["/second/path"].key?(:smb_password)).to be_falsey end end end end describe "#disable" do it "should scrub folder configuration" do expect(subject).to receive(:clean_folder_configuration).at_least(:once) subject.disable(machine, folders, options) end context "with smb_username and smb_password set" do let(:folders){ { "/first/path" => {owner: "smbowner", smb_username: "user", smb_password: "pass"}, "/second/path" => {group: "smbgroup", smb_username: "user", smb_password: "pass"} } } it "should retain non password configuration options" do subject.disable(machine, folders, options) folder1 = folders["/first/path"] folder2 = folders["/second/path"] expect(folder1.key?(:owner)).to be_truthy expect(folder1.key?(:smb_username)).to be_truthy expect(folder2.key?(:group)).to be_truthy expect(folder2.key?(:smb_username)).to be_truthy end it "should remove the smb_password option when set" do subject.disable(machine, folders, options) expect(folders["/first/path"].key?(:smb_password)).to be_falsey expect(folders["/second/path"].key?(:smb_password)).to be_falsey end end end describe "#cleanup" do context "without supporting capability" do it "does nothing" do subject.cleanup(machine, options) end end context "with supporting capability" do let(:host_caps){ [:smb_cleanup] } it "runs cleanup" do expect(host).to receive(:capability).with(:smb_cleanup, any_args) subject.cleanup(machine, options) end end end describe "#clean_folder_configuration" do it "should remove smb_password if defined" do data = {smb_password: "password"} subject.send(:clean_folder_configuration, data) expect(data.key?(:smb_password)).to be_falsey end it "should not error if non-hash value provided" do expect { subject.send(:clean_folder_configuration, nil) }. not_to raise_error end end end ================================================ FILE: test/unit/plugins/synced_folders/unix_mount_helpers_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../base" require Vagrant.source_root.join("plugins/synced_folders/unix_mount_helpers") describe VagrantPlugins::SyncedFolder::UnixMountHelpers do include_context "unit" subject{ Class.new do |c| def self.name; "UnixMountHelpersTest"; end extend VagrantPlugins::SyncedFolder::UnixMountHelpers end } describe ".merge_mount_options" do let(:base){ ["opt1", "opt2=on", "opt3", "opt4,opt5=off"] } let(:override){ ["opt8", "opt4=on,opt6,opt7=true"] } context "with no override" do it "should split options into individual options" do result = subject.merge_mount_options(base, []) expect(result.size).to eq(5) end end context "with overrides" do it "should merge all options" do result = subject.merge_mount_options(base, override) expect(result.size).to eq(8) end it "should override options defined in base" do result = subject.merge_mount_options(base, override) expect(result).to include("opt4=on") end end end end ================================================ FILE: test/unit/support/dummy_communicator.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantTests module DummyCommunicator class Communicator < Vagrant.plugin("2", :communicator) def ready? true end attr_reader :known_commands def initialize(machine) @known_commands = Hash.new do |hash, key| hash[key] = { expected: 0, received: 0, response: nil } end end def expected_commands known_commands.select do |command, info| info[:expected] > 0 end end def received_commands known_commands.select do |command, info| info[:received] > 0 end.keys end def stub_command(command, response) known_commands[command][:response] = response end def expect_command(command) known_commands[command][:expected] += 1 end def received_summary received_commands.map { |cmd| " - #{cmd}" }.unshift('received:').join("\n") end def verify_expectations! expected_commands.each do |command, info| if info[:expected] != info[:received] fail([ "expected to receive '#{command}' #{info[:expected]} times", "got #{info[:received]} times instead", received_summary ].join("\n")) end end end def execute(command, opts=nil) known = known_commands[command] known[:received] += 1 response = known[:response] return unless response if block_given? [:stdout, :stderr].each do |type| Array(response[type]).each do |line| yield type, line end end end if response[:raise] raise response[:raise] end response[:exit_code] end def sudo(command, opts=nil, &block) execute(command, opts, &block) end def test(command, opts=nil) execute(command, opts) == 0 end end end end ================================================ FILE: test/unit/support/dummy_provider.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 module VagrantTests class DummyProviderPlugin < Vagrant.plugin("2") name "Dummy Provider" description <<-EOF This creates a provider named "dummy" which does nothing, so that the unit tests aren't reliant on VirtualBox (or any other real provider for that matter). EOF provider(:dummy) { DummyProvider } end class DummyProvider < Vagrant.plugin("2", :provider) def initialize(machine) @machine = machine end def state=(id) state_file.open("w+") do |f| f.write(id.to_s) end end def state if !state_file.file? new_state = @machine.id new_state = Vagrant::MachineState::NOT_CREATED_ID if !new_state self.state = new_state end state_id = state_file.read.to_sym Vagrant::MachineState.new(state_id, state_id.to_s, state_id.to_s) end protected def state_file @machine.data_dir.join("dummy_state") end end end ================================================ FILE: test/unit/support/isolated_environment.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "fileutils" require "pathname" require "tempfile" require "tmpdir" require "json" require "log4r" require "vagrant/util/platform" require "vagrant/util/subprocess" require "support/isolated_environment" module Unit class IsolatedEnvironment < ::IsolatedEnvironment def create_vagrant_env(options=nil) options = { cwd: @workdir, home_path: @homedir }.merge(options || {}) Vagrant::Environment.new(options) end # This creates a file in the isolated environment. By default this file # will be created in the working directory of the isolated environment. def file(name, contents) @workdir.join(name).open("w+") do |f| f.write(contents) end end def vagrantfile(contents, root=nil) root ||= @workdir root.join("Vagrantfile").open("w+") do |f| f.write(contents) end end def box(name, vagrantfile_contents="") # Create the box directory box_dir = boxes_dir.join(name) box_dir.mkpath # Create the "box.ovf" file because that is how Vagrant heuristically # determines a box is a V1 box. box_dir.join("box.ovf").open("w") { |f| f.write("") } # Populate the vagrantfile vagrantfile(vagrantfile_contents, box_dir) # Return the directory box_dir end # Create an alias because "box" makes a V1 box, so "box1" alias :box1 :box # Creates a fake box to exist in this environment. # # @param [String] name Name of the box # @param [Symbol] provider Provider the box was built for. # @return [Pathname] Path to the box directory. def box2(name, provider, options=nil) # Default options options = { vagrantfile: "" }.merge(options || {}) # Make the box directory box_dir = boxes_dir.join(name, provider.to_s) box_dir.mkpath # Create a metadata.json file box_metadata_file = box_dir.join("metadata.json") box_metadata_file.open("w") do |f| f.write(JSON.generate({ provider: provider.to_s })) end # Create a Vagrantfile box_vagrantfile = box_dir.join("Vagrantfile") box_vagrantfile.open("w") do |f| f.write(options[:vagrantfile]) end # Return the box directory box_dir end # Creates a fake box to exist in this environment according # to the "gen-3" box format. # # @param [String] name # @param [String] version # @param [String] provider # @return [Pathname] def box3(name, version, provider, **opts) args = [name, version] args << opts[:architecture].to_s if opts[:architecture] args << provider.to_s # Create the directory for the box box_dir = boxes_dir.join(*args) box_dir.mkpath # Create the metadata.json for it box_metadata_file = box_dir.join("metadata.json") box_metadata_file.open("w") do |f| f.write(JSON.generate({ provider: provider.to_s })) end # Create a Vagrantfile if opts[:vagrantfile] box_vagrantfile = box_dir.join("Vagrantfile") box_vagrantfile.open("w") do |f| f.write(opts[:vagrantfile]) end end # Create the metadata URL if opts[:metadata_url] boxes_dir.join(name, "metadata_url").open("w") do |f| f.write(opts[:metadata_url]) end end box_dir end # This creates a "box" file that is a valid V1 box. # # @return [Pathname] Path to the newly created box. def box1_file # Create a temporary directory to store our data we will tar up td_source = Dir.mktmpdir("vagrant-box1-source") td_dest = Dir.mktmpdir("vagrant-box-1-dest") # Store the temporary directory so it is not deleted until # this instance is garbage collected. @_box2_file_temp ||= [] @_box2_file_temp << td_dest # The source as a Pathname, which is easier to work with source = Pathname.new(td_source) # The destination file result = Pathname.new(td_dest).join("temporary.box") # Put a "box.ovf" in there. source.join("box.ovf").open("w") do |f| f.write("FOO!") end Dir.chdir(source) do # Find all the files in our current directory and tar it up! files = Dir.glob(File.join(".", "**", "*")) # Package! Vagrant::Util::Subprocess.execute("bsdtar", "-czf", result.to_s, *files) end # Resulting box result end # This creates a "box" file with the given provider. # # @param [Symbol] provider Provider for the box. # @return [Pathname] Path to the newly created box. def box2_file(provider, options=nil) options ||= {} # This is the metadata we want to store in our file metadata = { "type" => "v2_box", "provider" => provider }.merge(options[:metadata] || {}) # Create a temporary directory to store our data we will tar up td_source = Dir.mktmpdir("vagrant-box-2-source") td_dest = Dir.mktmpdir("vagrant-box-2-dest") # Store the temporary directory so it is not deleted until # this instance is garbage collected. @_box2_file_temp ||= [] @_box2_file_temp << td_dest # The source as a Pathname, which is easier to work with source = Pathname.new(td_source) # The destination file result = Pathname.new(td_dest).join("temporary.box") # Put the metadata.json in here. source.join("metadata.json").open("w") do |f| f.write(JSON.generate(metadata)) end Dir.chdir(source) do # Find all the files in our current directory and tar it up! files = Dir.glob(File.join(".", "**", "*")) # Package! Vagrant::Util::Subprocess.execute("bsdtar", "-czf", result.to_s, *files) end # Resulting box result end def boxes_dir dir = @homedir.join("boxes") dir.mkpath dir end end end ================================================ FILE: test/unit/support/shared/action_synced_folders_context.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_context "synced folder actions" do # This creates a synced folder implementation. def impl(usable, name) Class.new(Vagrant.plugin("2", :synced_folder)) do define_method(:name) do name end define_method(:usable?) do |machine, raise_error=false| raise "#{name}: usable" if raise_error && !usable usable end define_method(:_initialize) do |machine, type| true end end end end ================================================ FILE: test/unit/support/shared/base_context.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tempfile" require "tmpdir" require "vagrant/util/platform" require "unit/support/isolated_environment" shared_context "unit" do before(:each) do # State to store the list of registered plugins that we have to # unregister later. @_plugins = [] # Create a thing to store our temporary files so that they aren't # unlinked right away. @_temp_files = [] # Roughly simulate the embedded Bundler availability $vagrant_bundler_runtime = Object.new end after(:each) do # Unregister each of the plugins we have may have temporarily # registered for the duration of this test. @_plugins.each do |plugin| Vagrant.plugin("1").manager.unregister(plugin) Vagrant.plugin("2").manager.unregister(plugin) end end # This creates an isolated environment so that Vagrant doesn't # muck around with your real system during unit tests. # # The returned isolated environment has a variety of helper # methods on it to easily create files, Vagrantfiles, boxes, # etc. def isolated_environment env = Unit::IsolatedEnvironment.new yield env if block_given? env end # This registers a Vagrant plugin for the duration of a single test. # This will yield a new plugin class that you can then call the # public plugin methods on. # # @yield [plugin] Yields the plugin class for you to call the public # API that you need to. def register_plugin(version=nil) version ||= Vagrant::Config::CURRENT_VERSION plugin = Class.new(Vagrant.plugin(version)) plugin.name("Test Plugin #{plugin.inspect}") yield plugin if block_given? @_plugins << plugin plugin end # This helper creates a temporary file and returns a Pathname # object pointed to it. # # @return [Pathname] def temporary_file(contents=nil) dir = temporary_dir f = dir.join("tempfile") contents ||= "" f.open("w") do |f| f.write(contents) f.flush end return Pathname.new(Vagrant::Util::Platform.fs_real_path(f.to_s)) end # This creates a temporary directory and returns a {Pathname} # pointing to it. If a block is given, the pathname is yielded and the # temporary directory is removed at the end of the block. # # @return [Pathname] def temporary_dir # Create a temporary directory and append it to the instance # variable so that it isn't garbage collected and deleted d = Dir.mktmpdir("vagrant-temporary-dir") @_temp_files ||= [] @_temp_files << d # Return the pathname result = Pathname.new(Vagrant::Util::Platform.fs_real_path(d)) if block_given? begin yield result ensure FileUtils.rm_rf(result) end end return result end # Stub the given environment in ENV, without actually touching ENV. Keys and # values are converted to strings because that's how the real ENV works. def stub_env(hash) allow(ENV).to receive(:[]).and_call_original hash.each do |key, value| v = value.nil? ? nil : value.to_s allow(ENV).to receive(:[]) .with(key.to_s) .and_return(v) end end # This helper provides temporary environmental variable changes. def with_temp_env(environment) # Build up the new environment, preserving the old values so we # can replace them back in later. old_env = {} environment.each do |key, value| key = key.to_s old_env[key] = ENV[key] ENV[key] = value end # Call the block, returning its return value return yield ensure # Reset the environment no matter what old_env.each do |key, value| ENV[key] = value end end # This helper provides a randomly available port(s) for each argument to the # block. def with_random_port(&block) ports = [] block.arity.times do server = TCPServer.new('127.0.0.1', 0) ports << server.addr[1] server.close end block.call(*ports) end end ================================================ FILE: test/unit/support/shared/capability_helpers_context.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_context "capability_helpers" do def detect_class(result) Class.new do define_method(:detect?) do |*args| result end end end def provider_usable_class(result) Class.new do define_singleton_method(:usable?) do |*args| result end end end def cap_instance(name, options=nil) options ||= {} Class.new do if !options[:corrupt] define_method(name) do |*args| raise "cap: #{name} #{args.inspect}" end end end.new end end ================================================ FILE: test/unit/support/shared/plugin_command_context.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_context "command plugin helpers" do def command_lambda(name, result, **opts) lambda do Class.new(Vagrant.plugin("2", "command")) do define_method(:execute) do raise opts[:exception] if opts[:exception] result end end end end end ================================================ FILE: test/unit/support/shared/virtualbox_context.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 shared_context "virtualbox" do include_context "unit" let(:vbox_context) { true } let(:uuid) { "1234-abcd-5678-efgh" } let(:vbox_version) { "4.3.4" } let(:subprocess) { double("Vagrant::Util::Subprocess") } # this is a helper that returns a duck type suitable from a system command # execution; allows setting exit_code, stdout, and stderr in stubs. def subprocess_result(options={}) defaults = {exit_code: 0, stdout: "", stderr: ""} double("subprocess_result", defaults.merge(options)) end before do # we don't want unit tests to ever run commands on the system; so we wire # in a double to ensure any unexpected messages raise exceptions stub_const("Vagrant::Util::Subprocess", subprocess) # drivers will blow up on instantiation if they cannot determine the # virtualbox version, so wire this stub in automatically allow(subprocess).to receive(:execute). with("VBoxManage", "--version", an_instance_of(Hash)). and_return(subprocess_result(stdout: vbox_version)) # drivers also call vm_exists? during init; allow(subprocess).to receive(:execute). with("VBoxManage", "showvminfo", kind_of(String), kind_of(Hash)). and_return(subprocess_result(exit_code: 0)) allow(Vagrant::Util::Which).to receive(:which).and_call_original allow(Vagrant::Util::Which).to receive(:which).with("locale").and_return(false) end around do |example| # On Windows, we don't want to accidentally call the actual VirtualBox with_temp_env("VBOX_INSTALL_PATH" => nil) do example.run end end end ================================================ FILE: test/unit/templates/commands/init/Vagrantfile.erb ================================================ # -*- mode: ruby -*- # vi: set ft=ruby : # All Vagrant configuration is done below. The "2" in Vagrant.configure # configures the configuration version (we support older styles for # backwards compatibility). Please don't change it unless you know what # you're doing. Vagrant.configure("2") do |config| config.vm.hostname = "vagrant.dev" config.vm.box = "<%= box_name %>" <% if box_version -%> config.vm.box_version = "<%= box_version %>" <% end -%> <% if box_url -%> # The url from where the 'config.vm.box' box will be fetched if it # doesn't already exist on the user's system. config.vm.box_url = "<%= box_url %>" <% end -%> end ================================================ FILE: test/unit/templates/guests/arch/default_network/network_dhcp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require "vagrant/util/template_renderer" describe "templates/guests/arch/default_network/network_dhcp" do let(:template) { "guests/arch/default_network/network_dhcp" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") Description='A basic dhcp ethernet connection' Interface=eth1 Connection=ethernet IP=dhcp EOH end end ================================================ FILE: test/unit/templates/guests/arch/default_network/network_static_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require "vagrant/util/template_renderer" describe "templates/guests/arch/default_network/network_static" do let(:template) { "guests/arch/default_network/network_static" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", netmask: "24", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") Connection=ethernet Description='A basic static ethernet connection' Interface=eth1 IP=static Address=('1.1.1.1/24') EOH end it "includes the gateway" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", gateway: "1.2.3.4", netmask: "24", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") Connection=ethernet Description='A basic static ethernet connection' Interface=eth1 IP=static Address=('1.1.1.1/24') Gateway='1.2.3.4' EOH end end ================================================ FILE: test/unit/templates/guests/arch/systemd_networkd/network_dhcp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require "vagrant/util/template_renderer" describe "templates/guests/arch/systemd_networkd/network_dhcp" do let(:template) { "guests/arch/systemd_networkd/network_dhcp" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") [Match] Name=eth1 [Network] Description=A basic DHCP ethernet connection DHCP=ipv4 EOH end end ================================================ FILE: test/unit/templates/guests/arch/systemd_networkd/network_static_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../../base" require "vagrant/util/template_renderer" describe "templates/guests/arch/systemd_networkd/network_static" do let(:template) { "guests/arch/systemd_networkd/network_static" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", netmask: "24", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") [Match] Name=eth1 [Network] Description=A basic static ethernet connection Address=1.1.1.1/24 EOH end it "includes the gateway" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", gateway: "1.2.3.4", netmask: "24", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") [Match] Name=eth1 [Network] Description=A basic static ethernet connection Address=1.1.1.1/24 Gateway=1.2.3.4 EOH end end ================================================ FILE: test/unit/templates/guests/debian/network_dhcp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/debian/network_dhcp" do let(:template) { "guests/debian/network_dhcp" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. auto eth1 iface eth1 inet dhcp post-up ip route del default dev $IFACE || true #VAGRANT-END EOH end context "when use_dhcp_assigned_default_route is set" do it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", root_device: "eth0", use_dhcp_assigned_default_route: true, }) expect(result).to eq <<-EOH.gsub(/^ {8}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. auto eth1 iface eth1 inet dhcp # We need to disable eth0, see GH-2648 post-up ip route del default dev eth0 || true post-up dhclient $IFACE pre-down ip route add default dev eth0 #VAGRANT-END EOH end end end ================================================ FILE: test/unit/templates/guests/debian/network_static_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/debian/network_static" do let(:template) { "guests/debian/network_static" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", netmask: "255.255.0.0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. auto eth1 iface eth1 inet static address 1.1.1.1 netmask 255.255.0.0 #VAGRANT-END EOH end it "includes the gateway" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", netmask: "255.255.0.0", gateway: "1.2.3.4", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. auto eth1 iface eth1 inet static address 1.1.1.1 netmask 255.255.0.0 gateway 1.2.3.4 #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/freebsd/network_dhcp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/freebsd/network_dhcp" do let(:template) { "guests/freebsd/network_dhcp" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN ifconfig_eth1="DHCP" synchronous_dhclient="YES" #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/freebsd/network_static_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/freebsd/network_static" do let(:template) { "guests/freebsd/network_static" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", netmask: "255.255.0.0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN ifconfig_eth1="inet 1.1.1.1 netmask 255.255.0.0" #VAGRANT-END EOH end it "includes the gateway" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", netmask: "255.255.0.0", gateway: "1.2.3.4", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN ifconfig_eth1="inet 1.1.1.1 netmask 255.255.0.0" defaultrouter="1.2.3.4" #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/funtoo/network_dhcp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/funtoo/network_dhcp" do let(:template) { "guests/funtoo/network_dhcp" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='dhcp' #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/funtoo/network_static_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/funtoo/network_static" do let(:template) { "guests/funtoo/network_static" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='interface' ipaddr='1.1.1.1/255.255.0.0' #VAGRANT-END EOH end it "includes the gateway" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", gateway: "1.2.3.4", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='interface' ipaddr='1.1.1.1/255.255.0.0' gateway='1.2.3.4' #VAGRANT-END EOH end it "includes the nameservers" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", nameservers: "ns1.company.com", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='interface' ipaddr='1.1.1.1/255.255.0.0' nameservers='ns1.company.com' #VAGRANT-END EOH end it "includes the domain" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", domain: "company.com", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='interface' ipaddr='1.1.1.1/255.255.0.0' domain='company.com' #VAGRANT-END EOH end it "includes the route" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", route: "5.6.7.8", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='interface' ipaddr='1.1.1.1/255.255.0.0' route='5.6.7.8' #VAGRANT-END EOH end it "includes the gateway6" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", gateway6: "aaaa:0000", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='interface' ipaddr='1.1.1.1/255.255.0.0' gateway6='aaaa:0000' #VAGRANT-END EOH end it "includes the route6" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", route6: "bbbb:1111", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='interface' ipaddr='1.1.1.1/255.255.0.0' route6='bbbb:1111' #VAGRANT-END EOH end it "includes the mtu" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", mtu: "1", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. template='interface' ipaddr='1.1.1.1/255.255.0.0' mtu='1' #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/gentoo/network_dhcp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/gentoo/network_dhcp" do let(:template) { "guests/gentoo/network_dhcp" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. config_en0="dhcp" #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/gentoo/network_static_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/gentoo/network_static" do let(:template) { "guests/gentoo/network_static" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. config_en0=("1.1.1.1 netmask 255.255.0.0") modules_en0=("!plug") #VAGRANT-END EOH end it "includes the gateway" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", gateway: "1.2.3.4", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. config_en0=("1.1.1.1 netmask 255.255.0.0") modules_en0=("!plug") gateways_en0="1.2.3.4" #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/gentoo/systemd_network_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/gentoo/network_systemd" do let(:template) { "guests/gentoo/network_systemd" } it "renders the template with a static ip" do result = Vagrant::Util::TemplateRenderer.render(template, networks: [{ device: "eth0", type: "dhcp", }]) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") [Match] Name=eth0 [Network] DHCP=yes EOH end it "renders the template with multiple ips" do result = Vagrant::Util::TemplateRenderer.render(template, networks: [{ device: "eth0", ip: "1.1.1.1", netmask: "16", },{ device: "eth0", ip: "1.1.2.2", netmask: "16", }]) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") [Match] Name=eth0 [Network] Address=1.1.1.1/16 Address=1.1.2.2/16 EOH end it "renders the template with a static ip" do result = Vagrant::Util::TemplateRenderer.render(template, networks: [{ device: "eth0", ip: "1.1.1.1", netmask: "16", }]) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") [Match] Name=eth0 [Network] Address=1.1.1.1/16 EOH end it "includes the gateway" do result = Vagrant::Util::TemplateRenderer.render(template, networks: [{ device: "eth0", ip: "1.1.1.1", netmask: "16", gateway: "1.2.3.4", }]) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") [Match] Name=eth0 [Network] Address=1.1.1.1/16 Gateway=1.2.3.4 EOH end end ================================================ FILE: test/unit/templates/guests/netbsd/network_dhcp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/netbsd/network_dhcp" do let(:template) { "guests/netbsd/network_dhcp" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { interface: "en0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN ifconfig_wmen0=dhcp #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/netbsd/network_static_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/netbsd/network_static" do let(:template) { "guests/netbsd/network_static" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { interface: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN ifconfig_wmen0="media autoselect up;inet 1.1.1.1 netmask 255.255.0.0" #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/nixos/network_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/nixos/network" do let(:template) { "guests/nixos/network" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, networks: [{ device: "en0", ip: "1.1.1.1", prefix_length: "24", type: :static, }]) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") { config, pkgs, ... }: { networking.interfaces = { en0.ipv4.addresses = [{ address = "1.1.1.1"; prefixLength = 24; }]; }; } EOH end end ================================================ FILE: test/unit/templates/guests/redhat/network_dhcp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/redhat/network_dhcp" do let(:template) { "guests/redhat/network_dhcp" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", }) 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=en0 NM_CONTROLLED=no #VAGRANT-END EOH end it "renders the template with NetworkManager enabled" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", nm_controlled: "yes" }) 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=en0 NM_CONTROLLED=yes #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/redhat/network_static_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/redhat/network_static" do let(:template) { "guests/redhat/network_static" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", netmask: "255.255.0.0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. NM_CONTROLLED=no BOOTPROTO=none ONBOOT=yes IPADDR=1.1.1.1 NETMASK=255.255.0.0 DEVICE=en0 PEERDNS=no #VAGRANT-END EOH end it "includes the gateway" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "en0", ip: "1.1.1.1", gateway: "1.2.3.4", netmask: "255.255.0.0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. NM_CONTROLLED=no BOOTPROTO=none ONBOOT=yes IPADDR=1.1.1.1 NETMASK=255.255.0.0 DEVICE=en0 GATEWAY=1.2.3.4 PEERDNS=no #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/suse/network_dhcp_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/suse/network_dhcp" do let(:template) { "guests/suse/network_dhcp" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth0" }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. BOOTPROTO='dhcp' STARTMODE='auto' DEVICE='eth0' #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/suse/network_static6_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/suse/network_static6" do let(:template) { "guests/suse/network_static6" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth2", ip: "fde4:8dba:82e1::c4", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. STARTMODE='auto' BOOTPROTO='static' IPADDR=fde4:8dba:82e1::c4 DEVICE=eth2 #VAGRANT-END EOH end it "includes the prefix-length and gateway" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "fde4:8dba:82e1::c4", gateway: "fde4:8dba:82e1::01", prefix_length: "64", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. STARTMODE='auto' BOOTPROTO='static' IPADDR=fde4:8dba:82e1::c4 DEVICE=eth1 GATEWAY=fde4:8dba:82e1::01 PREFIXLEN=64 #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/guests/suse/network_static_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../../base" require "vagrant/util/template_renderer" describe "templates/guests/suse/network_static" do let(:template) { "guests/suse/network_static" } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", netmask: "255.255.0.0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. BOOTPROTO='static' IPADDR='1.1.1.1' NETMASK='255.255.0.0' DEVICE='eth1' PEERDNS='no' STARTMODE='auto' USERCONTROL='no' #VAGRANT-END EOH end it "includes the gateway" do result = Vagrant::Util::TemplateRenderer.render(template, options: { device: "eth1", ip: "1.1.1.1", gateway: "1.2.3.4", netmask: "255.255.0.0", }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") #VAGRANT-BEGIN # The contents below are automatically generated by Vagrant. Do not modify. BOOTPROTO='static' IPADDR='1.1.1.1' NETMASK='255.255.0.0' DEVICE='eth1' GATEWAY='1.2.3.4' PEERDNS='no' STARTMODE='auto' USERCONTROL='no' #VAGRANT-END EOH end end ================================================ FILE: test/unit/templates/nfs/exports_darwin_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../base" require "vagrant/util/template_renderer" describe "templates/nfs/exports_darwin" do let(:template) { "nfs/exports_darwin" } let(:user) { "501" } let(:uuid) { "UUID" } let(:opts) { {:bsd__compiled_nfs_options => "-alldirs -mapall=501:80"} } let(:ips) { ["172.16.0.2"] } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, { user: user, uuid: uuid, folders: [] }) expect(result).to eq <<-EOH.gsub(/^ {6}/, "") # VAGRANT-BEGIN: 501 UUID # VAGRANT-END: 501 UUID EOH end context "one nfs mount" do let(:folders) { { ["/vagrant"] => opts } } it "renders the template" do result = Vagrant::Util::TemplateRenderer.render(template, { user: user, uuid: uuid, folders: folders, ips: ips }) expect(result).to eq <<-EOH.gsub(/^ {8}/, "") # VAGRANT-BEGIN: 501 UUID "/vagrant" -alldirs -mapall=501:80 172.16.0.2 # VAGRANT-END: 501 UUID EOH end end context "subdirectory that should also be exported" do let(:folders) { { ["/vagrant", "/vagrant/other"] => opts } } it "puts each directory on its own line" do result = Vagrant::Util::TemplateRenderer.render(template, { user: user, uuid: uuid, folders: folders, ips: ips }) expect(result).to eq <<-EOH.gsub(/^ {8}/, "") # VAGRANT-BEGIN: 501 UUID "/vagrant" -alldirs -mapall=501:80 172.16.0.2 "/vagrant/other" -alldirs -mapall=501:80 172.16.0.2 # VAGRANT-END: 501 UUID EOH end end end ================================================ FILE: test/unit/vagrant/action/builder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) describe Vagrant::Action::Builder do let(:data) { { data: [] } } let(:primary) { true } let(:subject) do described_class.new.tap do |b| b.primary = primary end end # This returns a proc that can be used with the builder # that simply appends data to an array in the env. def appender_proc(data) result = Proc.new { |env| env[:data] << data } # Define a to_s on it for helpful output result.define_singleton_method(:to_s) do "" end result end def wrapper_proc(data) Class.new do def initialize(app, env) @app = app end def self.name "TestAction" end define_method(:call) do |env| env[:data] << "#{data}_in" @app.call(env) env[:data] << "#{data}_out" end end end context "copying" do it "should copy the stack" do copy = subject.dup expect(copy.stack.object_id).not_to eq(subject.stack.object_id) end end context "build" do it "should provide build as a shortcut for basic sequences" do data = {} proc = Proc.new { |env| env[:data] = true } subject = described_class.build(proc) subject.call(data) expect(data[:data]).to eq(true) end end context "basic `use`" do it "should add items to the stack and make them callable" do data = {} proc = Proc.new { |env| env[:data] = true } subject.use proc subject.call(data) expect(data[:data]).to eq(true) end it "should be able to add multiple items" do data = {} proc1 = Proc.new { |env| env[:one] = true } proc2 = Proc.new { |env| env[:two] = true } subject.use proc1 subject.use proc2 subject.call(data) expect(data[:one]).to eq(true) expect(data[:two]).to eq(true) end it "should be able to add another builder" do data = {} proc1 = Proc.new { |env| env[:one] = true } # Build the first builder one = described_class.new one.use proc1 # Add it to this builder two = described_class.new two.use one # Call the 2nd and verify results two.call(data) expect(data[:one]).to eq(true) end end context "inserting" do it "can insert at an index" do subject.use appender_proc(1) subject.insert(0, appender_proc(2)) subject.call(data) expect(data[:data]).to eq([2, 1]) end it "can insert by name" do # Create the proc then make sure it has a name bar_proc = appender_proc(2) def bar_proc.name; :bar; end subject.use appender_proc(1) subject.use bar_proc subject.insert_before :bar, appender_proc(3) subject.call(data) expect(data[:data]).to eq([1, 3, 2]) end it "can insert next to a previous object" do proc2 = appender_proc(2) subject.use appender_proc(1) subject.use proc2 subject.insert(proc2, appender_proc(3)) subject.call(data) expect(data[:data]).to eq([1, 3, 2]) end it "can insert before" do subject.use appender_proc(1) subject.insert_before 0, appender_proc(2) subject.call(data) expect(data[:data]).to eq([2, 1]) end it "can insert after" do subject.use appender_proc(1) subject.use appender_proc(3) subject.insert_after 0, appender_proc(2) subject.call(data) expect(data[:data]).to eq([1, 2, 3]) end it "merges middleware stacks of other builders" do wrapper_class = Proc.new do |letter| Class.new do def initialize(app, env) @app = app end def self.name "TestAction" end define_method(:call) do |env| env[:data] << "#{letter}1" @app.call(env) env[:data] << "#{letter}2" end end end proc2 = appender_proc(2) subject.use appender_proc(1) subject.use proc2 builder = described_class.new builder.use wrapper_class.call("A") builder.use wrapper_class.call("B") subject.insert(proc2, builder) subject.call(data) expect(data[:data]).to eq([1, "A1", "B1", 2, "B2", "A2"]) end it "raises an exception if an invalid object given for insert" do expect { subject.insert "object", appender_proc(1) }. to raise_error(RuntimeError) end it "raises an exception if an invalid object given for insert_after" do expect { subject.insert_after "object", appender_proc(1) }. to raise_error(RuntimeError) end end context "replace" do it "can replace an object" do proc1 = appender_proc(1) proc2 = appender_proc(2) subject.use proc1 subject.replace proc1, proc2 subject.call(data) expect(data[:data]).to eq([2]) end it "can replace by index" do proc1 = appender_proc(1) proc2 = appender_proc(2) subject.use proc1 subject.replace 0, proc2 subject.call(data) expect(data[:data]).to eq([2]) end end context "deleting" do it "can delete by object" do proc1 = appender_proc(1) subject.use proc1 subject.use appender_proc(2) subject.delete proc1 subject.call(data) expect(data[:data]).to eq([2]) end it "can delete by index" do proc1 = appender_proc(1) subject.use proc1 subject.use appender_proc(2) subject.delete 0 subject.call(data) expect(data[:data]).to eq([2]) end end describe "action hooks" do let(:hook) { double("hook") } let(:manager) { Vagrant.plugin("2").manager } before do allow(manager).to receive(:action_hooks).and_return([]) end it "applies them properly" do hook_proc = proc{ |h| h.append(appender_proc(:hook)) } allow(manager).to receive(:action_hooks).with(:test_action). and_return([hook_proc]) data[:action_name] = :test_action subject.use appender_proc(1) subject.call(data) expect(data[:data]).to eq([1, :hook]) end it "applies them properly even with nested stacks" do hook_proc = proc{ |h| h.append(appender_proc(:hook)) } allow(manager).to receive(:action_hooks).with(:test_action). and_return([hook_proc]) data[:action_name] = :test_action subject.use appender_proc(1) subject.use Vagrant::Action::Builtin::Call, proc {} do |env, b2| b2.use appender_proc(2) end subject.call(data) expect(data[:data]).to eq([1, 2, :hook]) end end describe "calling another app later" do it "calls in the proper order" do # We have to do this because inside the Class.new, it can't see these # rspec methods... described_klass = described_class wrapper_proc = self.method(:wrapper_proc) wrapper = Class.new do def initialize(app, env) @app = app end def self.name "TestAction" end define_method(:call) do |env| inner = described_klass.new inner.use wrapper_proc[2] inner.use @app inner.call(env) end end subject.use wrapper_proc(1) subject.use wrapper subject.use wrapper_proc(3) subject.call(data) expect(data[:data]).to eq([ "1_in", "2_in", "3_in", "3_out", "2_out", "1_out"]) end end describe "dynamic action hooks" do class ActionOne def initialize(app, env) @app = app end def call(env) env[:data] << 1 if env[:data] @app.call(env) end def recover(env) env[:recover] << 1 end end class ActionTwo def initialize(app, env) @app = app end def call(env) env[:data] << 2 if env[:data] @app.call(env) end def recover(env) env[:recover] << 2 end end let(:data) { {data: []} } let(:hook_action_name) { :action_two } let(:plugin) do h_name = hook_action_name @plugin ||= Class.new(Vagrant.plugin("2")) do name "Test Plugin" action_hook(:before_test, h_name) do |hook| hook.prepend(proc{ |env| env[:data] << :first }) end end end before { plugin } after do Vagrant.plugin("2").manager.unregister(@plugin) if @plugin @plugin = nil end it "should call hook before running action" do instance = described_class.build(ActionTwo).tap { |b| b.primary = true } instance.call(data) expect(data[:data].first).to eq(:first) expect(data[:data].last).to eq(2) end context "when hook matches action in subsequent builder" do let(:hook_action_name) { ActionOne } before do data[:action_name] = :test_action_name data[:raw_action_name] = :machine_test_action_name end it "should execute the hook" do described_class.build(ActionTwo).tap { |b| b.primary = true }.call(data) described_class.build(ActionOne).tap { |b| b.primary = true }.call(data) expect(data[:data]).to include(:first) end end context "when hook matches action name in subsequent builder" do let(:hook_action_name) { :test_action_name } before do data[:action_name] = :test_action_name data[:raw_action_name] = :machine_test_action_name end it "should execute the hook" do described_class.build(ActionTwo).tap { |b| b.primary = true }.call(data) described_class.build(ActionOne).tap { |b| b.primary = true }.call(data) expect(data[:data]).to include(:first) end it "should execute the hook multiple times" do described_class.build(ActionTwo).tap { |b| b.primary = true }.call(data) described_class.build(ActionOne).tap { |b| b.primary = true }.call(data) expect(data[:data].count{|d| d == :first}).to eq(2) end end context "when applying triggers" do let(:triggers) { double("triggers") } before do data[:action_name] = :test_action_name data[:raw_action_name] = :machine_test_action_name data[:triggers] = triggers allow(triggers).to receive(:find).and_return([]) end it "should attempt to find triggers based on raw action" do expect(triggers).to receive(:find).with(data[:raw_action_name], any_args).and_return([]) described_class.build(ActionOne).call(data) end it "should only attempt to find triggers based on raw action once" do expect(triggers).to receive(:find).with(data[:raw_action_name], :before, any_args).once.and_return([]) expect(triggers).to receive(:find).with(data[:raw_action_name], :after, any_args).once.and_return([]) described_class.build(ActionOne).call(data) described_class.build(ActionOne).call(data) end end context "when hook is appending to action" do let(:plugin) do @plugin ||= Class.new(Vagrant.plugin("2")) do name "Test Plugin" action_hook(:before_test, :action_two) do |hook| hook.append(proc{ |env| env[:data] << :first }) end end end it "should call hook after action when action is nested" do instance = described_class.build(ActionTwo).use(described_class.build(ActionOne)) instance.call(data) expect(data[:data][0]).to eq(2) expect(data[:data][1]).to eq(:first) expect(data[:data][2]).to eq(1) end end context "when hook uses class name" do let(:hook_action_name) { "ActionTwo" } it "should execute the hook" do instance = described_class.build(ActionTwo) instance.call(data) expect(data[:data]).to include(:first) end end context "when action includes a namespace" do module Vagrant module Test class ActionTest def initialize(app, env) @app = app end def call(env) env[:data] << :test if env[:data] @app.call(env) end end end end let(:instance) { described_class.build(Vagrant::Test::ActionTest) } context "when hook uses short snake case name" do let(:hook_action_name) { :action_test } it "should execute the hook" do instance.call(data) expect(data[:data]).to include(:first) end end context "when hook uses partial snake case name" do let(:hook_action_name) { :test_action_test } it "should execute the hook" do instance.call(data) expect(data[:data]).to include(:first) end end context "when hook uses full snake case name" do let(:hook_action_name) { :vagrant_test_action_test } it "should execute the hook" do instance.call(data) expect(data[:data]).to include(:first) end end context "when hook uses short class name" do let(:hook_action_name) { "ActionTest" } it "should execute the hook" do instance.call(data) expect(data[:data]).to include(:first) end end context "when hook uses partial namespace class name" do let(:hook_action_name) { "Test::ActionTest" } it "should execute the hook" do instance.call(data) expect(data[:data]).to include(:first) end end context "when hook uses full namespace class name" do let(:hook_action_name) { "Vagrant::Test::ActionTest" } it "should execute the hook" do instance.call(data) expect(data[:data]).to include(:first) end end end end describe "#apply_dynamic_updates" do let(:env) { {triggers: triggers, machine: machine} } let(:machine) { nil } let(:triggers) { nil } let(:subject) do @subject ||= described_class.new.tap do |b| b.primary = primary b.use Vagrant::Action::Builtin::EnvSet b.use Vagrant::Action::Builtin::Confirm end end after { @subject = nil } it "should not modify the builder stack by default" do s1 = subject.stack.dup subject.apply_dynamic_updates(env) s2 = subject.stack.dup expect(s1).to eq(s2) end context "when an action hooks is defined" do let(:plugin) do @plugin ||= Class.new(Vagrant.plugin("2")) do name "Test Plugin" action_hook(:before_action, Vagrant::Action::Builtin::Confirm) do |hook| hook.prepend(Vagrant::Action::Builtin::Call) end end end before { plugin } after do Vagrant.plugin("2").manager.unregister(@plugin) if @plugin @plugin = nil end it "should modify the builder stack" do s1 = subject.stack.dup subject.apply_dynamic_updates(env) s2 = subject.stack.dup expect(s1).not_to eq(s2) end it "should add new action to the middle of the call stack" do subject.apply_dynamic_updates(env) expect(subject.stack[1].first).to eq(Vagrant::Action::Builtin::Call) end end context "when triggers are enabled" do let(:triggers) { double("triggers") } before do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). with("typed_triggers").and_return(true) allow(triggers).to receive(:find).and_return([]) end it "should not modify the builder stack by default" do s1 = subject.stack.dup subject.apply_dynamic_updates(env) s2 = subject.stack.dup expect(s1).to eq(s2) end context "when triggers are found" do let(:action) { Vagrant::Action::Builtin::EnvSet } before { expect(triggers).to receive(:find). with(action, timing, nil, type).and_return([true]) } context "for action type" do let(:type) { :action } context "for before timing" do let(:timing) { :before } it "should add trigger action to start of stack" do subject.apply_dynamic_updates(env) expect(subject.stack[0].middleware).to eq(Vagrant::Action::Builtin::Trigger) end it "should have timing and type arguments" do subject.apply_dynamic_updates(env) args = subject.stack[0].arguments.parameters expect(args).to include(type) expect(args).to include(timing) expect(args).to include(action.to_s) end end context "for after timing" do let(:timing) { :after } it "should add trigger action to middle of stack" do subject.apply_dynamic_updates(env) expect(subject.stack[1].middleware).to eq(Vagrant::Action::Builtin::Trigger) end it "should have timing and type arguments" do subject.apply_dynamic_updates(env) args = subject.stack[1].arguments.parameters expect(args).to include(type) expect(args).to include(timing) expect(args).to include(action.to_s) end end end context "for hook type" do let(:type) { :hook } context "for before timing" do let(:timing) { :before } it "should add trigger action to start of stack" do subject.apply_dynamic_updates(env) expect(subject.stack[0].middleware).to eq(Vagrant::Action::Builtin::Trigger) end it "should have timing and type arguments" do subject.apply_dynamic_updates(env) args = subject.stack[0].arguments.parameters expect(args).to include(type) expect(args).to include(timing) expect(args).to include(action.to_s) end end context "for after timing" do let(:timing) { :after } it "should add trigger action to middle of stack" do subject.apply_dynamic_updates(env) expect(subject.stack[1].first).to eq(Vagrant::Action::Builtin::Trigger) end it "should have timing and type arguments" do subject.apply_dynamic_updates(env) args = subject.stack[1].arguments.parameters expect(args).to include(type) expect(args).to include(timing) expect(args).to include(action.to_s) end end end end end end describe "#apply_action_name" do let(:env) { {triggers: triggers, machine: machine, action_name: action_name, raw_action_name: raw_action_name} } let(:raw_action_name) { :up } let(:action_name) { "machine_#{raw_action_name}".to_sym } let(:machine) { nil } let(:triggers) { double("triggers") } let(:subject) do @subject ||= described_class.new.tap do |b| b.primary = primary b.use Vagrant::Action::Builtin::EnvSet b.use Vagrant::Action::Builtin::Confirm end end before { allow(triggers).to receive(:find).and_return([]) } after { @subject = nil } context "when a plugin has added an action hook using prepend" do let(:plugin) do @plugin ||= Class.new(Vagrant.plugin("2")) do name "Test Plugin" action_hook(:before_action, :machine_up) do |hook| hook.prepend(Vagrant::Action::Builtin::Call) end end end before { plugin } after do Vagrant.plugin("2").manager.unregister(@plugin) if @plugin @plugin = nil end it "should add new action to the beginning of the call stack" do subject.apply_action_name(env) expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Call) end end context "when trigger has been defined for raw action" do before do expect(triggers).to receive(:find).with(raw_action_name, timing, nil, :action, all: true). and_return([true]) end context "when timing is before" do let(:timing) { :before } it "should add a trigger action to the start of the stack" do subject.apply_action_name(env) expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Trigger) end it "should include arguments to the trigger action" do subject.apply_action_name(env) args = subject.stack[0].arguments.parameters expect(args).to include(raw_action_name) expect(args).to include(timing) expect(args).to include(:action) end end context "when timing is after" do let(:timing) { :after } it "should add a trigger action to the end of the stack" do subject.apply_action_name(env) expect(subject.stack.first.first).to eq(Vagrant::Action::Builtin::Delayed) end it "should include arguments to the trigger action" do subject.apply_action_name(env) builder = subject.stack.first.arguments.parameters.first expect(builder).not_to be_nil args = builder.stack.first.arguments.parameters expect(args).to include(raw_action_name) expect(args).to include(timing) expect(args).to include(:action) end end end context "when trigger has been defined for hook" do before do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). with("typed_triggers").and_return(true) expect(triggers).to receive(:find).with(action_name, timing, nil, :hook). and_return([true]) end context "when timing is before" do let(:timing) { :before } it "should add a trigger action to the start of the stack" do subject.apply_action_name(env) expect(subject.stack[0].middleware).to eq(Vagrant::Action::Builtin::Trigger) end it "should include arguments to the trigger action" do subject.apply_action_name(env) args = subject.stack[0].arguments.parameters expect(args).to include(action_name) expect(args).to include(timing) expect(args).to include(:hook) end end context "when timing is after" do let(:timing) { :after } it "should add a trigger action to the end of the stack" do subject.apply_action_name(env) expect(subject.stack.last.first).to eq(Vagrant::Action::Builtin::Trigger) end it "should include arguments to the trigger action" do subject.apply_action_name(env) args = subject.stack.last.arguments.parameters expect(args).to include(action_name) expect(args).to include(timing) expect(args).to include(:hook) end end end end end ================================================ FILE: test/unit/vagrant/action/builtin/box_add_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "digest/sha1" require "pathname" require "tempfile" require "tmpdir" require "webrick" require "fake_ftp" require File.expand_path("../../../../base", __FILE__) require "vagrant/util/file_checksum" describe Vagrant::Action::Builtin::BoxAdd, :skip_windows, :bsdtar do include_context "unit" let(:app) { lambda { |env| } } let(:env) { { box_collection: box_collection, hook: Proc.new { |name, env| env }, tmp_path: Pathname.new(Dir.mktmpdir("vagrant-test-builtin-box-add")), ui: Vagrant::UI::Silent.new, } } subject { described_class.new(app, env) } let(:box_collection) { double("box_collection") } let(:iso_env) { isolated_environment } let(:box) do box_dir = iso_env.box3("foo", "1.0", :virtualbox) Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir) end after do FileUtils.rm_rf(env[:tmp_path]) end # Helper to quickly SHA1 checksum a path def checksum(path) FileChecksum.new(path, Digest::SHA1).checksum end def with_ftp_server(path, **opts) path = Pathname.new(path) port = nil server = nil with_random_port do |port1, port2| port = port1 server = FakeFtp::Server.new(port1, port2) end server.add_file(path.basename.to_s, path.read) server.start yield port ensure server.stop rescue nil end def with_web_server(path, **opts) tf = Tempfile.new("vagrant-web-server") tf.close opts[:json_type] ||= "application/json" mime_types = WEBrick::HTTPUtils::DefaultMimeTypes mime_types.store "json", opts[:json_type] port = 3838 server = WEBrick::HTTPServer.new( AccessLog: [], Logger: WEBrick::Log.new(tf.path, 7), Port: port, DocumentRoot: path.dirname.to_s, MimeTypes: mime_types) thr = Thread.new { server.start } yield port ensure tf.unlink server.shutdown rescue nil thr.join rescue nil end before do allow(box_collection).to receive(:find).and_return(nil) end context "the download location is locked" do let(:box_path) { iso_env.box2_file(:virtualbox) } let(:mutex_path) { env[:tmp_path].join("box" + Digest::SHA1.hexdigest("file://" + box_path.to_s) + ".lock").to_s } before do # Lock file @f = File.open(mutex_path, "w+") @f.flock(File::LOCK_EX|File::LOCK_NB) end after { @f.close } it "raises a download error" do env[:box_name] = "foo" env[:box_url] = box_path.to_s expect { subject.call(env) }. to raise_error(Vagrant::Errors::DownloadAlreadyInProgress) end end context "with box file directly" do it "adds it" do box_path = iso_env.box2_file(:virtualbox) env[:box_name] = "foo" env[:box_url] = box_path.to_s env[:box_architecture] = "x86_64" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo") expect(version).to eq("0") expect(opts[:architecture]).to eq("x86_64") expect(opts[:metadata_url]).to be_nil true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end it "adds from multiple URLs" do box_path = iso_env.box2_file(:virtualbox) env[:box_name] = "foo" env[:box_url] = [ "/foo/bar/baz", box_path.to_s, ] expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo") expect(version).to eq("0") expect(opts[:metadata_url]).to be_nil true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end it "adds from HTTP URL" do box_path = iso_env.box2_file(:virtualbox) with_web_server(box_path) do |port| env[:box_name] = "foo" env[:box_url] = "http://127.0.0.1:#{port}/#{box_path.basename}" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo") expect(version).to eq("0") expect(opts[:metadata_url]).to be_nil true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end end it "adds from FTP URL" do box_path = iso_env.box2_file(:virtualbox) with_ftp_server(box_path) do |port| env[:box_name] = "foo" env[:box_url] = "ftp://127.0.0.1:#{port}/#{box_path.basename}" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo") expect(version).to eq("0") expect(opts[:metadata_url]).to be_nil true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end end it "raises an error if no name is given" do box_path = iso_env.box2_file(:virtualbox) env[:box_url] = box_path.to_s expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxAddNameRequired) end it "raises an error if the box already exists" do box_path = iso_env.box2_file(:virtualbox) env[:box_name] = "foo" env[:box_url] = box_path.to_s env[:box_provider] = "virtualbox" expect(box_collection).to receive(:find).with( "foo", ["virtualbox"], "0", nil).and_return(box) expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxAlreadyExists) end it "raises an error if checksum specified and doesn't match" do box_path = iso_env.box2_file(:virtualbox) env[:box_name] = "foo" env[:box_url] = box_path.to_s env[:box_checksum] = checksum(box_path) + "A" env[:box_checksum_type] = "sha1" expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxChecksumMismatch) end it "strips space if checksum specified ends or begins with blank space" do box_path = iso_env.box2_file(:virtualbox) box = double( name: "foo", version: "1.2.3", provider: "virtualbox", ) env[:box_name] = "foo" env[:box_url] = box_path.to_s env[:box_checksum] = " #{checksum(box_path)} " env[:box_checksum_type] = "sha1" expect(box_collection).to receive(:add).and_return(box) expect { subject.call(env) }.to_not raise_error end it "ignores checksums if empty string" do box_path = iso_env.box2_file(:virtualbox) with_web_server(box_path) do |port| env[:box_name] = "foo" env[:box_url] = "http://127.0.0.1:#{port}/#{box_path.basename}" env[:box_checksum] = "" env[:box_checksum_type] = "" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo") expect(version).to eq("0") expect(opts[:metadata_url]).to be_nil true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end end it "does not raise an error if the checksum has different case" do box_path = iso_env.box2_file(:virtualbox) box = double( name: "foo", version: "1.2.3", provider: "virtualbox", ) env[:box_name] = box.name env[:box_url] = box_path.to_s env[:box_checksum] = checksum(box_path) env[:box_checksum_type] = "sha1" # Convert to a different case env[:box_checksum].upcase! expect(box_collection).to receive(:add).and_return(box) expect { subject.call(env) }.to_not raise_error end it "raises an error if the box path doesn't exist" do box_path = iso_env.box2_file(:virtualbox) env[:box_name] = "foo" env[:box_url] = box_path.to_s + "nope" expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::DownloaderError) end it "raises an error if a version was specified" do box_path = iso_env.box2_file(:virtualbox) env[:box_name] = "foo" env[:box_url] = box_path.to_s env[:box_version] = "1" expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxAddDirectVersion) end it "force adds if exists and specified" do box_path = iso_env.box2_file(:virtualbox) env[:box_force] = true env[:box_name] = "foo" env[:box_url] = box_path.to_s env[:box_provider] = "virtualbox" allow(box_collection).to receive(:find).and_return(box) expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo") expect(version).to eq("0") expect(opts[:metadata_url]).to be_nil true }.and_return(box) expect(app).to receive(:call).with(env).once subject.call(env) end context "with a box name accidentally set as a URL" do it "displays a warning to the user" do box_path = iso_env.box2_file(:virtualbox) port = 9999 box_url_name = "http://127.0.0.1:#{port}/#{box_path.basename}" env[:box_name] = box_url_name expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(name).to eq(box_url_name) expect(version).to eq("0") expect(opts[:metadata_url]).to be_nil true }.and_return(box) expect(app).to receive(:call).with(env) expect(env[:ui]).to receive(:warn).and_call_original .with(/It looks like you attempted to add a box with a URL for the name/) subject.call(env) end end context "with a box name containing invalid URI characters" do it "should not raise an error" do box_path = iso_env.box2_file(:virtualbox) box_url_name = "box name with spaces" env[:box_name] = box_url_name expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(name).to eq(box_url_name) expect(version).to eq("0") expect(opts[:metadata_url]).to be_nil true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end end context "with URL containing credentials" do let(:username){ "box-username" } let(:password){ "box-password" } it "scrubs credentials in output" do box_path = iso_env.box2_file(:virtualbox) with_web_server(box_path) do |port| env[:box_name] = "foo" env[:box_url] = "http://#{username}:#{password}@127.0.0.1:#{port}/#{box_path.basename}" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo") expect(version).to eq("0") expect(opts[:metadata_url]).to be_nil true }.and_return(box) allow(env[:ui]).to receive(:detail).and_call_original expect(env[:ui]).to receive(:detail).with(/.*http:\/\/\*+(?::\*+)?@127\.0\.0\.1:#{port}\/#{box_path.basename}.*/). and_call_original expect(app).to receive(:call).with(env) subject.call(env) end end end end context "with box metadata" do it "adds from HTTP URL" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-test-box-http-url", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW f.close end md_path = Pathname.new(tf.path) with_web_server(md_path) do |port| env[:box_url] = "http://127.0.0.1:#{port}/#{md_path.basename}" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(checksum(path)).to eq(checksum(box_path)) expect(opts[:metadata_url]).to eq(env[:box_url]) true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end end it "adds from HTTP URL with complex JSON mime type" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-test-http-json", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW f.close end opts = { json_type: "application/json; charset=utf-8" } md_path = Pathname.new(tf.path) with_web_server(md_path, **opts) do |port| env[:box_url] = "http://127.0.0.1:#{port}/#{md_path.basename}" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(checksum(path)).to eq(checksum(box_path)) expect(opts[:metadata_url]).to eq(env[:box_url]) true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end end it "adds from shorthand path" do box_path = iso_env.box2_file(:virtualbox) td = Pathname.new(Dir.mktmpdir("vagrant-test-box-add-shorthand-path")) tf = td.join("mitchellh", "precise64.json") tf.dirname.mkpath tf.open("w") do |f| f.write(<<-RAW) { "name": "mitchellh/precise64", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW end with_web_server(tf.dirname) do |port| url = "http://127.0.0.1:#{port}" env[:box_url] = "mitchellh/precise64.json" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(name).to eq("mitchellh/precise64") expect(version).to eq("0.7") expect(checksum(path)).to eq(checksum(box_path)) expect(opts[:metadata_url]).to eq( "#{url}/#{env[:box_url]}") true }.and_return(box) expect(app).to receive(:call).with(env) with_temp_env("VAGRANT_SERVER_URL" => url) do subject.call(env) end end FileUtils.rm_rf(td) end it "adds from API endpoint when available" do box_path = iso_env.box2_file(:virtualbox) td = Pathname.new(Dir.mktmpdir("vagrant-test-box-api-endpoint")) tf = td.join("mitchellh", "precise64.json") tf.dirname.mkpath tf.open("w") do |f| f.write(<<-RAW) { "name": "mitchellh/precise64", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW end apif = td.join("api", "v2", "vagrant", "mitchellh", "precise64.json") apif.dirname.mkpath apif.open("w") do |f| f.write(<<-RAW) { "name": "mitchellh/precise64", "versions": [ { "version": "0.5" }, { "version": "0.8", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW end with_web_server(tf.dirname) do |port| url = "http://127.0.0.1:#{port}" env[:box_url] = "mitchellh/precise64.json" env[:box_server_url] = url expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(name).to eq("mitchellh/precise64") expect(version).to eq("0.8") expect(checksum(path)).to eq(checksum(box_path)) expect(opts[:metadata_url]).to eq( "#{url}/api/v2/vagrant/#{env[:box_url]}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end FileUtils.rm_rf(td) end it "add from shorthand path with configured server url" do box_path = iso_env.box2_file(:virtualbox) td = Pathname.new(Dir.mktmpdir("vagrant-test-box-add-server-url")) tf = td.join("mitchellh", "precise64.json") tf.dirname.mkpath tf.open("w") do |f| f.write(<<-RAW) { "name": "mitchellh/precise64", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW end with_web_server(tf.dirname) do |port| url = "http://127.0.0.1:#{port}" env[:box_url] = "mitchellh/precise64.json" env[:box_server_url] = url expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(name).to eq("mitchellh/precise64") expect(version).to eq("0.7") expect(checksum(path)).to eq(checksum(box_path)) expect(opts[:metadata_url]).to eq( "#{url}/#{env[:box_url]}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end FileUtils.rm_rf(td) end it "authenticates HTTP URLs and adds them" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-test-http", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "bar" } ] } ] } RAW f.close end md_path = Pathname.new(tf.path) with_web_server(md_path) do |port| real_url = "http://127.0.0.1:#{port}/#{md_path.basename}" # Set the box URL to something fake so we can modify it in place env[:box_url] = "foo" env[:hook] = double("hook") expect(env[:hook]).to receive(:call).with(:authenticate_box_downloader, any_args).at_least(:once) allow(env[:hook]).to receive(:call).with(:authenticate_box_url, any_args).at_least(:once) do |name, opts| if opts[:box_urls] == ["foo"] next { box_urls: [real_url] } elsif opts[:box_urls] == ["bar"] next { box_urls: [box_path.to_s] } else raise "UNKNOWN: #{opts[:box_urls].inspect}" end end expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(checksum(path)).to eq(checksum(box_path)) expect(opts[:metadata_url]).to eq("foo") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end end it "authenticates HTTP URLs and adds them directly" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-test-http", ".box"]).tap do |f| f.write() f.close end md_path = Pathname.new(tf.path) with_web_server(md_path) do |port| real_url = "http://127.0.0.1:#{port}/#{md_path.basename}" # Set the box URL to something fake so we can modify it in place env[:box_url] = "foo" env[:hook] = double("hook") env[:box_name] = "foo/bar" env[:box_provider] = "virtualbox" env[:box_checksum] = checksum(box_path) expect(env[:hook]).to receive(:call).with(:authenticate_box_downloader, any_args).at_least(:once) allow(env[:hook]).to receive(:call).with(:authenticate_box_url, any_args).at_least(:once) do |name, opts| if opts[:box_urls] == ["foo"] next { box_urls: [real_url] } else raise "UNKNOWN: #{opts[:box_urls].inspect}" end end expect(subject).to receive(:add_direct).with([real_url], anything) expect(app).to receive(:call).with(env) subject.call(env) end end it "adds from HTTP URL with a checksum" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-test-http-checksum", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}", "checksum_type": "sha1", "checksum": "#{checksum(box_path)}" } ] } ] } RAW f.close end md_path = Pathname.new(tf.path) with_web_server(md_path) do |port| env[:box_url] = "http://127.0.0.1:#{port}/#{md_path.basename}" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(checksum(path)).to eq(checksum(box_path)) expect(opts[:metadata_url]).to eq(env[:box_url]) true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end end it "raises an exception if checksum given but not correct" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-test-bad-checksum", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}", "checksum_type": "sha1", "checksum": "thisisnotcorrect" } ] } ] } RAW f.close end md_path = Pathname.new(tf.path) with_web_server(md_path) do |port| env[:box_url] = "http://127.0.0.1:#{port}/#{md_path.basename}" expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxChecksumMismatch) end end it "raises an error if no Vagrant server is set" do env[:box_url] = "mitchellh/precise64.json" expect(box_collection).to receive(:add).never expect(app).to receive(:call).never allow(Vagrant).to receive(:server_url).and_return(nil) expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxServerNotSet) end it "raises an error if shorthand is invalid" do path = Dir::Tmpname.create("vagrant-shorthand-invalid") {} with_web_server(Pathname.new(path)) do |port| env[:box_url] = "mitchellh/precise64.json" expect(box_collection).to receive(:add).never expect(app).to receive(:call).never url = "http://127.0.0.1:#{port}" with_temp_env("VAGRANT_SERVER_URL" => url) do expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxAddShortNotFound) end end end it "raises an error if downloading metadata fails" do path = Dir::Tmpname.create("vagrant-shorthand-invalid") {} with_web_server(Pathname.new(path)) do |port| env[:box_url] = "http://127.0.0.1:#{port}/bad" expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxMetadataDownloadError) end end it "raises an error if multiple metadata URLs are given" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-multi-metadata", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW f.close end env[:box_url] = [ "/foo/bar/baz", tf.path, ] expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxAddMetadataMultiURL) end it "adds the latest version of a box with only one provider" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW f.close end env[:box_url] = tf.path expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end context "with box architecture configured" do before do allow(Vagrant::Util::Platform).to receive(:architecture).and_return("test-arch") end context "set to :auto" do before do env[:box_architecture] = :auto end it "adds the latest version of a box with only one provider and matching architecture" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write( { name: "foo/bar", versions: [ { version: "0.5", }, { version: "0.7", providers: [ { name: "virtualbox", url: "/dev/null/invalid.path", architecture: "amd64", default_architecture: true, }, { name: "virtualbox", url: "#{box_path}", architecture: "test-arch", default_architecture: false, } ] } ] }.to_json ) f.close end env[:box_url] = tf.path expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") expect(opts[:architecture]).to eq("test-arch") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end it "adds the latest version of a box with multiple providers and only one provider matching architecture" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| # NOTE: The order of the providers here matters. The correct match needs # to be last in order to trigger the error this test was written for to # catch any regression. f.write( { name: "foo/bar", versions: [ { version: "0.5", }, { version: "0.7", providers: [ { name: "virtualbox", url: "/dev/null/invalid.path", architecture: "amd64", default_architecture: true, }, { name: "hyperv", url: "#{box_path}", architecture: "test-arch", default_architecture: true, } ] } ] }.to_json ) f.close end env[:box_url] = tf.path expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") expect(opts[:architecture]).to eq("test-arch") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end it "adds the latest version of a box with only one provider and unknown architecture set as default" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write( { name: "foo/bar", versions: [ { version: "0.5", }, { version: "0.7", providers: [ { name: "virtualbox", url: "#{box_path}", architecture: "unknown", default_architecture: true, }, { name: "virtualbox", url: "/dev/null/invalid.path", architecture: "amd64", default_architecture: false, } ] } ] }.to_json ) f.close end env[:box_url] = tf.path expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") expect(opts[:architecture]).to be_nil true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end it "errors adding latest version of a box with only one provider and no matching architecture" do allow(Vagrant::Util::Platform).to receive(:architecture).and_return("test-arch") box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write( { name: "foo/bar", versions: [ { version: "0.5", }, { version: "0.7", providers: [ { name: "virtualbox", url: "#{box_path}", architecture: "amd64", default_architecture: true, }, { name: "virtualbox", url: "/dev/null/invalid.path", architecture: "arm64", default_architecture: false, } ] } ] }.to_json ) f.close end env[:box_url] = tf.path expect { subject.call(env) }.to raise_error(Vagrant::Errors::BoxAddNoMatchingVersion) end end context "set to nil" do before do env[:box_architecture] = nil end it "adds the latest version of a box with only one provider and default architecture" do allow(Vagrant::Util::Platform).to receive(:architecture).and_return("default-arch") box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write( { name: "foo/bar", versions: [ { version: "0.5", }, { version: "0.7", providers: [ { name: "virtualbox", url: "#{box_path}", architecture: "default-arch", default_architecture: true, }, { name: "virtualbox", url: "/dev/null/invalid.path", architecture: "test-arch", default_architecture: false, } ] } ] }.to_json ) f.close end env[:box_url] = tf.path expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") expect(opts[:architecture]).to eq("default-arch") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end end context "set to explicit value" do before do env[:box_architecture] = "arm64" end it "adds the latest version of a box with only one provider and matching architecture" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write( { name: "foo/bar", versions: [ { version: "0.5", }, { version: "0.7", providers: [ { name: "virtualbox", url: "#{box_path}", architecture: "arm64", default_architecture: false, }, { name: "virtualbox", url: "/dev/null/invalid.path", architecture: "test-arch", default_architecture: true, } ] } ] }.to_json ) f.close end env[:box_url] = tf.path expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") expect(opts[:architecture]).to eq("arm64") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end it "errors adding the latest version of a box with only one provider and no matching architecture" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write( { name: "foo/bar", versions: [ { version: "0.5", }, { version: "0.7", providers: [ { name: "virtualbox", url: "#{box_path}", architecture: "amd64", default_architecture: true, }, { name: "virtualbox", url: "/dev/null/invalid.path", architecture: "386", default_architecture: false, } ] } ] }.to_json ) f.close end env[:box_url] = tf.path expect { subject.call(env) }.to raise_error(Vagrant::Errors::BoxAddNoMatchingVersion) end end end it "adds the latest version of a box with the specified provider" do box_path = iso_env.box2_file(:vmware) tf = Tempfile.new(["vagrant-box-specific-provider", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{iso_env.box2_file(:virtualbox)}" }, { "name": "vmware", "url": "#{box_path}" } ] } ] } RAW f.close end env[:box_url] = tf.path env[:box_provider] = "vmware" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) expect(env[:box_added]).to equal(box) end it "adds the latest version of a box with the specified provider, even if not latest" do box_path = iso_env.box2_file(:vmware) tf = Tempfile.new(["vagrant-box-specified-provider", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{iso_env.box2_file(:virtualbox)}" }, { "name": "vmware", "url": "#{box_path}" } ] }, { "version": "1.5" } ] } RAW f.close end env[:box_url] = tf.path env[:box_provider] = "vmware" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) expect(env[:box_added]).to equal(box) end it "adds the constrained version of a box with the only provider" do box_path = iso_env.box2_file(:vmware) tf = Tempfile.new(["vagrant-box-constrained", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5", "providers": [ { "name": "vmware", "url": "#{box_path}" } ] }, { "version": "1.1" } ] } RAW f.close end env[:box_url] = tf.path env[:box_version] = "~> 0.1" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.5") expect(opts[:metadata_url]).to eq("file://#{tf.path}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) expect(env[:box_added]).to equal(box) end it "adds the constrained version of a box with the specified provider" do box_path = iso_env.box2_file(:vmware) tf = Tempfile.new(["vagrant-box-constrained-provider", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5", "providers": [ { "name": "vmware", "url": "#{box_path}" }, { "name": "virtualbox", "url": "#{iso_env.box2_file(:virtualbox)}" } ] }, { "version": "1.1" } ] } RAW f.close end env[:box_url] = tf.path env[:box_provider] = "vmware" env[:box_version] = "~> 0.1" expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.5") expect(opts[:metadata_url]).to eq("file://#{tf.path}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) expect(env[:box_added]).to equal(box) end it "adds the latest version of a box with any specified provider" do box_path = iso_env.box2_file(:vmware) tf = Tempfile.new(["vagrant-box-latest-version", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5", "providers": [ { "name": "virtualbox", "url": "#{iso_env.box2_file(:virtualbox)}" } ] }, { "version": "0.7", "providers": [ { "name": "vmware", "url": "#{box_path}" } ] } ] } RAW f.close end env[:box_url] = tf.path env[:box_provider] = ["virtualbox", "vmware"] expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) expect(env[:box_added]).to equal(box) end it "asks the user what provider if multiple options" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-provider-asks", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" }, { "name": "vmware", "url": "#{iso_env.box2_file(:vmware)}" } ] } ] } RAW f.close end env[:box_url] = tf.path expect(env[:ui]).to receive(:ask).and_return("1") expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:metadata_url]).to eq("file://#{tf.path}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) end it "raises an exception if the name doesn't match a requested name" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-name-mismatch", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW f.close end env[:box_name] = "foo" env[:box_url] = tf.path expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxAddNameMismatch) end it "raises an exception if no matching version" do box_path = iso_env.box2_file(:vmware) tf = Tempfile.new(["vagrant-box-no-matching-version", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5", "providers": [ { "name": "vmware", "url": "#{box_path}" } ] }, { "version": "1.1" } ] } RAW f.close end env[:box_url] = tf.path env[:box_version] = "~> 2.0" expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxAddNoMatchingVersion) end it "raises an error if there is no matching provider" do tf = Tempfile.new(["vagrant-box-no-matching-provider", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{iso_env.box2_file(:virtualbox)}" } ] } ] } RAW f.close end env[:box_url] = tf.path env[:box_provider] = "vmware" expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxAddNoMatchingProvider) end it "raises an error if a box already exists" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-already-exists", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW f.close end env[:box_url] = tf.path expect(box_collection).to receive(:find). with("foo/bar", "virtualbox", "0.7", nil).and_return(box) expect(box_collection).to receive(:add).never expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxAlreadyExists) end it "force adds a box if specified" do box_path = iso_env.box2_file(:virtualbox) tf = Tempfile.new(["vagrant-box-force-add", ".json"]).tap do |f| f.write(<<-RAW) { "name": "foo/bar", "versions": [ { "version": "0.5" }, { "version": "0.7", "providers": [ { "name": "virtualbox", "url": "#{box_path}" } ] } ] } RAW f.close end env[:box_force] = true env[:box_url] = tf.path allow(box_collection).to receive(:find).and_return(box) expect(box_collection).to receive(:add).with(any_args) { |path, name, version, opts| expect(checksum(path)).to eq(checksum(box_path)) expect(name).to eq("foo/bar") expect(version).to eq("0.7") expect(opts[:force]).to be(true) expect(opts[:metadata_url]).to eq("file://#{tf.path}") true }.and_return(box) expect(app).to receive(:call).with(env) subject.call(env) expect(env[:box_added]).to equal(box) end end describe "#downloader" do let(:options) { {} } after { subject.send(:downloader, url, env, **options) } context "when box is local" do let(:url) { "/dev/null/vagrant.box" } it "should prepend file protocol to path" do expect(Vagrant::Util::Downloader).to receive(:new) do |durl, *_| expect(durl).to eq("file://#{url}") end end end context "when box is remote" do let(:url) { "http://localhost/vagrant.box" } it "should not modify the url" do expect(Vagrant::Util::Downloader).to receive(:new) do |durl, *_| expect(durl).to eq(url) end end end context "when options are set in the environment" do let(:url) { "http://localhost/vagrant.box" } context "when ca cert value is set" do let(:ca_cert) { "/dev/null/ca.cert" } before { env[:box_download_ca_cert] = ca_cert } it "should include ca cert value" do expect(Vagrant::Util::Downloader).to receive(:new) do |_, _, opts| expect(opts[:ca_cert]).to eq(ca_cert) end end end context "when ca path value is set" do let(:ca_path) { "/dev/null/ca.path" } before { env[:box_download_ca_path] = ca_path } it "should include ca path value" do expect(Vagrant::Util::Downloader).to receive(:new) do |_, _, opts| expect(opts[:ca_path]).to eq(ca_path) end end end context "when insecure value is set" do before { env[:box_download_insecure] = true } it "should include insecure value" do expect(Vagrant::Util::Downloader).to receive(:new) do |_, _, opts| expect(opts[:insecure]).to be_truthy end end end context "when client cert value is set" do let(:client_cert_path) { "/dev/null/client.cert" } before { env[:box_download_client_cert] = client_cert_path } it "should include client cert value" do expect(Vagrant::Util::Downloader).to receive(:new) do |_, _, opts| expect(opts[:client_cert]).to eq(client_cert_path) end end end context "when location trusted is set" do before { env[:box_download_location_trusted] = true } it "should include client cert value" do expect(Vagrant::Util::Downloader).to receive(:new) do |_, _, opts| expect(opts[:location_trusted]).to be_truthy end end end context "when disable ssl revoke best effort is set" do before { env[:box_download_disable_ssl_revoke_best_effort] = true } it "should include disable revoke best effort value value" do expect(Vagrant::Util::Downloader).to receive(:new) do |_, _, opts| expect(opts[:disable_ssl_revoke_best_effort]).to be_truthy end end end end end end ================================================ FILE: test/unit/vagrant/action/builtin/box_check_outdated_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::BoxCheckOutdated do include_context "unit" let(:app) { lambda { |env| } } let(:env) { { box_collection: iso_vagrant_env.boxes, machine: machine, ui: Vagrant::UI::Silent.new, } } subject { described_class.new(app, env) } let(:iso_env) do # We have to create a Vagrantfile so there is a root path isolated_environment.tap do |env| env.vagrantfile("") end end let(:iso_vagrant_env) { iso_env.create_vagrant_env } let(:box) do box_dir = iso_env.box3("foo", "1.0", :virtualbox) Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir).tap do |b| allow(b).to receive(:has_update?).and_return(nil) end end let(:machine) do m = iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy) m.config.vm.box_check_update = true m end before do allow(machine).to receive(:box).and_return(box) end context "disabling outdated checking" do it "doesn't check" do machine.config.vm.box_check_update = false expect(app).to receive(:call).with(env).once subject.call(env) expect(env).to_not have_key(:box_outdated) end it "checks if forced" do machine.config.vm.box_check_update = false env[:box_outdated_force] = true expect(app).to receive(:call).with(env).once expect(box).to receive(:has_update?) subject.call(env) expect(env).to have_key(:box_outdated) end it "checks if not forced" do machine.config.vm.box_check_update = false env[:box_outdated_force] = false expect(app).to receive(:call).with(env).once subject.call(env) end end context "no box" do it "raises an exception if the machine doesn't have a box yet" do allow(machine).to receive(:box).and_return(nil) expect(app).to receive(:call).with(env).once subject.call(env) expect(env).to_not have_key(:box_outdated) end end context "with a non-versioned box" do it "does nothing" do allow(box).to receive(:metadata_url).and_return(nil) allow(box).to receive(:version).and_return("0") expect(app).to receive(:call).once expect(box).to receive(:has_update?).never subject.call(env) end end context "with a box" do it "sets env if no update" do expect(box).to receive(:has_update?).and_return(nil) expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_outdated]).to be(false) end it "sets env if there is 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 expect(box).to receive(:has_update?).with(machine.config.vm.box_version, {download_options: {automatic_check: true, ca_cert: nil, ca_path: nil, client_cert: nil, insecure: false, disable_ssl_revoke_best_effort: false, box_extra_download_options: []}}). and_return([md, md.version("1.1"), md.version("1.1").provider("virtualbox")]) expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_outdated]).to be(true) end it "has an update if it is local" do iso_env.box3("foo", "1.1", :virtualbox) expect(box).to receive(:has_update?).and_return(nil) expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_outdated]).to be(true) end context "both local and remote update exist" do it "should prompt user to update" do iso_env.box3("foo", "1.1", :virtualbox) md = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) { "name": "foo", "versions": [ { "version": "1.2", "providers": [ { "name": "virtualbox", "url": "bar" } ] } ] } RAW expect(box).to receive(:has_update?).with( machine.config.vm.box_version, { download_options: { automatic_check: true, ca_cert: nil, ca_path: nil, client_cert: nil, disable_ssl_revoke_best_effort: false, insecure: false, box_extra_download_options: [] }}).and_return( [md, md.version("1.2"), md.version("1.2").provider("virtualbox")] ) allow(I18n).to receive(:t) { :ok } expect(I18n).to receive(:t).with(/box_outdated_single/, hash_including(latest: "1.2")).once expect(app).to receive(:call).with(env).once subject.call(env) end end it "does not have a local update if not within constraints" do iso_env.box3("foo", "1.1", :virtualbox) machine.config.vm.box_version = "> 1.0, < 1.1" expect(box).to receive(:has_update?).and_return(nil) expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_outdated]).to be(false) end it "does nothing if metadata download fails" do expect(box).to receive(:has_update?).and_raise( Vagrant::Errors::BoxMetadataDownloadError.new(message: "foo")) expect(app).to receive(:call).once subject.call(env) expect(env[:box_outdated]).to be(false) end it "does nothing if metadata cannot be parsed" do expect(box).to receive(:has_update?).and_raise( Vagrant::Errors::BoxMetadataMalformed.new(error: "Whoopsie")) expect(app).to receive(:call).once subject.call(env) expect(env[:box_outdated]).to be(false) end it "raises error if has_update? errors" do expect(box).to receive(:has_update?).and_raise(Vagrant::Errors::VagrantError) expect(app).to receive(:call).never expect { subject.call(env) }.to raise_error(Vagrant::Errors::VagrantError) end it "doesn't raise an error if ignore errors is on" do env[:box_outdated_ignore_errors] = true expect(box).to receive(:has_update?).and_raise(Vagrant::Errors::VagrantError) expect(app).to receive(:call).with(env).once 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 machine.config.vm.box_download_options = {"opt": "val"} machine.config.vm.box_download_disable_ssl_revoke_best_effort = true machine.config.vm.finalize! end it "uses download options from machine" do expect(box).to receive(:has_update?).with(machine.config.vm.box_version, { download_options: { automatic_check: true, ca_cert: "foo", ca_path: "bar", client_cert: "baz", insecure: true, disable_ssl_revoke_best_effort: true, box_extra_download_options: ["--opt", "val"]}}) 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: {automatic_check: true, ca_cert: "oof", ca_path: "rab", client_cert: "zab", insecure: false, disable_ssl_revoke_best_effort: true, box_extra_download_options: ["--tpo"], }}) env[:ca_cert] = "oof" env[:ca_path] = "rab" env[:client_cert] = "zab" env[:insecure] = false env[:box_extra_download_options] = ["--tpo"] expect(app).to receive(:call).with(env).once subject.call(env) end end end describe ".check_outdated_local" do let(:updated_box) do box_dir = iso_env.box3("foo", "1.1", :virtualbox) Vagrant::Box.new("foo", :virtualbox, "1.1", box_dir).tap do |b| allow(b).to receive(:has_update?).and_return(nil) end end it "should return the updated box if it is already installed" do expect(env[:box_collection]).to receive(:find).with("foo", :virtualbox, "> 1.0").and_return(updated_box) local_update = subject.check_outdated_local(env) expect(local_update).to eq(updated_box) end end end ================================================ FILE: test/unit/vagrant/action/builtin/box_remove_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::BoxRemove do include_context "unit" let(:app) { lambda { |env| } } let(:env) { { box_collection: box_collection, home_path: home_path, machine_index: machine_index, ui: Vagrant::UI::Silent.new, } } subject { described_class.new(app, env) } let(:box_collection) { double("box_collection") } let(:home_path) { double("home-path") } let(:machine_index) { [] } let(:iso_env) { isolated_environment } let(:box) do box_dir = iso_env.box3("foo", "1.0", :virtualbox) Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir) end it "deletes the box if it is the only option" do allow(box_collection).to receive(:all).and_return([["foo", "1.0", :virtualbox, nil]]) env[:box_name] = "foo" expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(true) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_removed]).to equal(box) end it "deletes the box with the specified provider if given" do allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox], ["foo", "1.0", :vmware], ]) env[:box_name] = "foo" env[:box_provider] = "virtualbox" expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(false) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_removed]).to equal(box) end it "deletes the box with the specified version if given" do allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox], ["foo", "1.1", :virtualbox], ]) env[:box_name] = "foo" env[:box_version] = "1.0" expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(false) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_removed]).to equal(box) end context "with architecture" do let(:architecture) { "test-arch" } let(:box) do box_dir = iso_env.box3("foo", "1.0", :virtualbox, architecture: architecture) Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir, architecture: architecture) end it "deletes the box if it is the only option" do allow(box_collection).to receive(:all).and_return([["foo", "1.0", :virtualbox, architecture]]) env[:box_name] = "foo" expect(box_collection).to receive(:find). with("foo", :virtualbox, "1.0", architecture). and_return(box) expect(box_collection).to receive(:clean). with(box.name). and_return(true) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_removed]).to equal(box) end it "deletes the box with the specified provider if given" do allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox, architecture], ["foo", "1.0", :vmware, architecture], ]) env[:box_name] = "foo" env[:box_provider] = "virtualbox" expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", architecture).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(false) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_removed]).to equal(box) end it "deletes the box with the specified version if given" do allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox, architecture], ["foo", "1.1", :virtualbox, architecture], ]) env[:box_name] = "foo" env[:box_version] = "1.0" expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", architecture).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(false) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_removed]).to equal(box) end it "deletes the box with the specificed version and architecture" do allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox, architecture], ["foo", "1.0", :virtualbox, "other-arch"], ]) env[:box_name] = "foo" env[:box_version] = "1.0" env[:box_architecture] = architecture expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", architecture).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(false) expect(box).to receive(:destroy!).once expect(app).to receive(:call).with(env).once subject.call(env) expect(env[:box_removed]).to equal(box) end it "errors when box with specified version does not included specified architecture" do allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox, architecture], ["foo", "1.0", :virtualbox, "other-arch"], ]) env[:box_name] = "foo" env[:box_version] = "1.0" env[:box_architecture] = "unknown-arch" expect { subject.call(env) }.to raise_error(Vagrant::Errors::BoxRemoveArchitectureNotFound) end it "errors when box with specified version has multiple architectures" do allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox, architecture], ["foo", "1.0", :virtualbox, "other-arch"], ]) env[:box_name] = "foo" env[:box_version] = "1.0" expect { subject.call(env) }.to raise_error(Vagrant::Errors::BoxRemoveMultiArchitecture) end 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| entry.extra_data["box"] = { "name" => "foo", "provider" => "virtualbox", "version" => "1.0", } allow(entry).to receive(:valid?).and_return(valid) end end let(:action_runner) { double("action_runner") } before do env[:action_runner] = action_runner allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox], ["foo", "1.1", :virtualbox], ]) env[:box_name] = "foo" env[:box_version] = "1.0" end it "does delete if the box is not in use" do expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", nil).and_return(box) expect(box).to receive(:destroy!).once expect(box_collection).to receive(:clean).with(box.name) .and_return(true) subject.call(env) end it "does delete if the box is in use and user confirms" do machine_index << new_entry("foo", "virtualbox", "1.0") result = { result: true } expect(action_runner).to receive(:run). with(anything, env).and_return(result) expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(true) expect(box).to receive(:destroy!).once subject.call(env) end it "doesn't delete if the box is in use" do machine_index << new_entry("foo", "virtualbox", "1.0") result = { result: false } expect(action_runner).to receive(:run). with(anything, env).and_return(result) expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", nil).and_return(box) expect(box).to receive(:destroy!).never subject.call(env) end it "doesn't delete if the box is in use and keep_used_boxes is set" do env[:keep_used_boxes] = true machine_index << new_entry("foo", "virtualbox", "1.0") result = { result: true } expect(action_runner).to receive(:run). with(anything, env).and_return(result) expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", nil).and_return(box) expect(box).to receive(:destroy!).never subject.call(env) end it "deletes if the box is in use and force is used" do machine_index << new_entry("foo", "virtualbox", "1.0") result = { result: true } expect(action_runner).to receive(:run). with(anything, env).and_return(result) expect(box_collection).to receive(:find).with( "foo", :virtualbox, "1.0", nil).and_return(box) expect(box_collection).to receive(:clean).with(box.name) .and_return(true) expect(box).to receive(:destroy!) subject.call(env) end end it "errors if the box doesn't exist" do allow(box_collection).to receive(:all).and_return([]) expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxRemoveNotFound) end it "errors if the specified provider doesn't exist" do env[:box_name] = "foo" env[:box_provider] = "bar" allow(box_collection).to receive(:all).and_return([["foo", "1.0", :virtualbox]]) expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxRemoveProviderNotFound) end it "errors if there are multiple providers" do env[:box_name] = "foo" allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox], ["foo", "1.0", :vmware], ]) expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxRemoveMultiProvider) end it "errors if the specified provider has multiple versions" do env[:box_name] = "foo" env[:box_provider] = "virtualbox" allow(box_collection).to receive(:all) .and_return([ ["foo", "1.0", :virtualbox], ["foo", "1.1", :virtualbox], ]) expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxRemoveMultiVersion) end it "errors if the specified version doesn't exist" do env[:box_name] = "foo" env[:box_version] = "1.1" allow(box_collection).to receive(:all).and_return([["foo", "1.0", :virtualbox]]) expect(app).to receive(:call).never expect { subject.call(env) }. to raise_error(Vagrant::Errors::BoxRemoveVersionNotFound) end end ================================================ FILE: test/unit/vagrant/action/builtin/call_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::Call do let(:app) { lambda { |env| } } let(:env) { {} } def wrapper_proc(data) Class.new do def initialize(app, env) @app = app end def self.name "TestAction" end define_method(:call) do |env| env[:data] << "#{data}_in" @app.call(env) env[:data] << "#{data}_out" end end end it "should yield the env to the block" do received = nil callable = lambda do |env| env[:result] = "value" end described_class.new(app, env, callable) do |env, builder| received = env[:result] end.call({}) expect(received).to eq("value") end it "should update the original env with any changes" do callable = lambda { |env| } next_step = lambda { |env| env[:inner] = true } described_class.new(app, env, callable) do |_env, builder| builder.use next_step end.call(env) expect(env[:inner]).to eq(true) end it "should call the callable with the original environment" do received = nil callable = lambda { |env| received = env[:foo] } described_class.new(app, env, callable) do |_env, _builder| # Nothing. end.call({ foo: :bar }) expect(received).to eq(:bar) end it "should call the next builder" do received = nil callable = lambda { |env| } next_step = lambda { |env| received = "value" } described_class.new(app, env, callable) do |_env, builder| builder.use next_step end.call({}) expect(received).to eq("value") end it "should call the next builder with the original environment" do received = nil callable = lambda { |env| } next_step = lambda { |env| received = env[:foo] } described_class.new(app, env, callable) do |_env, builder| builder.use next_step end.call({ foo: :bar }) expect(received).to eq(:bar) end it "should call the next builder inserted in our own stack" do callable = lambda { |env| } builder = Vagrant::Action::Builder.new.tap do |b| b.use wrapper_proc(1) b.use described_class, callable do |_env, b2| b2.use wrapper_proc(2) end b.use wrapper_proc(3) end env = { data: [] } builder.call(env) expect(env[:data]).to eq([ "1_in", "2_in", "3_in", "3_out", "2_out", "1_out"]) end it "should instantiate the callable with the extra args" do env = {} callable = Class.new do def initialize(app, env, arg) env[:arg] = arg end def self.name "TestAction" end def call(env); end end result = nil instance = described_class.new(app, env, callable, :foo) do |inner_env, _builder| result = inner_env[:arg] end instance.call(env) expect(result).to eq(:foo) end it "should call the recover method for the sequence in an error" do # Basic variables callable = lambda { |env| } # Build the steps for the test basic_step = Class.new do def initialize(app, env) @app = app @env = env end def self.name "TestAction" end def call(env) @app.call(env) end end step_a = Class.new(basic_step) do def call(env) env[:steps] << :call_A super end def self.name "TestAction" end def recover(env) env[:steps] << :recover_A end end step_b = Class.new(basic_step) do def call(env) env[:steps] << :call_B super end def self.name "TestAction" end def recover(env) env[:steps] << :recover_B end end instance = described_class.new(app, env, callable) do |_env, builder| builder.use step_a builder.use step_b end env[:steps] = [] instance.call(env) instance.recover(env) expect(env[:steps]).to eq([:call_A, :call_B, :recover_B, :recover_A]) end it "should recover even if it failed in the callable" do callable = lambda { |env| raise "error" } instance = described_class.new(app, env, callable) { |_env, _builder| } instance.call(env) rescue nil expect { instance.recover(env) }. to_not raise_error end end ================================================ FILE: test/unit/vagrant/action/builtin/cleanup_disks_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::CleanupDisks do let(:app) { lambda { |env| } } let(:vm) { double("vm") } let(:config) { double("config", vm: vm) } let(:provider) { double("provider") } let(:machine) { double("machine", config: config, provider: provider, name: "machine", provider_name: "provider", data_dir: Pathname.new("/fake/dir")) } let(:env) { { ui: ui, machine: machine} } let(:disks) { [double("disk")] } let(:ui) { Vagrant::UI::Silent.new } let(:disk_meta_file) { {disk: [{uuid: "123456789", name: "storage"}], floppy: [], dvd: []} } describe "#call" do it "calls configure_disks if disk config present" do allow(vm).to receive(:disks).and_return(disks) allow(machine).to receive(:disks).and_return(disks) allow(machine.provider).to receive(:capability?).with(:cleanup_disks).and_return(true) subject = described_class.new(app, env) expect(app).to receive(:call).with(env).ordered expect(subject).to receive(:read_disk_metadata).with(machine).and_return(disk_meta_file) expect(machine.provider).to receive(:capability). with(:cleanup_disks, disks, disk_meta_file) subject.call(env) end it "continues on if no disk config 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(:cleanup_disks, disks) subject.call(env) end it "prints a warning if disk config capability is unsupported" do allow(vm).to receive(:disks).and_return(disks) allow(machine.provider).to receive(:capability?).with(:cleanup_disks).and_return(false) subject = described_class.new(app, env) expect(subject).to receive(:read_disk_metadata).with(machine).and_return(disk_meta_file) expect(app).to receive(:call).with(env).ordered expect(machine.provider).not_to receive(:capability).with(:cleanup_disks, disks) expect(ui).to receive(:warn) subject.call(env) end end end ================================================ FILE: test/unit/vagrant/action/builtin/cloud_init_setup_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require "vagrant/util/mime" describe Vagrant::Action::Builtin::CloudInitSetup do let(:app) { lambda { |env| } } 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) } let(:provider) { double("provider") } let(:machine) { double("machine", config: config, provider: provider, name: "machine", provider_name: "provider", data_dir: Pathname.new("/fake/dir"), ui: ui, env: machine_env, id: "123-456-789") } let(:host) { double("host") } let(:machine_env) { double("machine_env", root_path: "root", host: host) } let(:env) { { ui: ui, machine: machine, env: machine_env} } let(:ui) { Vagrant::UI::Silent.new } let(:cfg) { double("cfg", type: :user_data, content_type: "text/cloud-config", content_disposition_filename: nil, path: "my/path", inline: nil) } let(:cfg_inline) { double("cfg", type: :user_data, content_type: "text/cloud-config", content_disposition_filename: nil, inline: "data: true", path: nil) } let(:cfg_with_content_disposition_filename_inline) { double("cfg", type: :user_data, content_type: "text/x-shellscript", content_disposition_filename: "test.ps1", inline: "#ps1_sysnative\n", path: nil) } let(:cloud_init_configs) { [cfg, cfg_inline] } let(:text_cfgs) { [Vagrant::Util::Mime::Entity.new("data: true", "text/cloud-config"), Vagrant::Util::Mime::Entity.new("data: false", "text/cloud-config") ] } let(:meta_data) { { "instance-id" => "i-123456789" } } let(:subject) { described_class.new(app, env) } describe "#call" do it "calls setup_user_data if cloud_init config present" do allow(vm).to receive(:cloud_init_configs).and_return(cloud_init_configs) expect(app).to receive(:call).with(env).ordered expect(subject).to receive(:setup_user_data).and_return(true) expect(subject).to receive(:write_cfg_iso).and_return(true) subject.call(env) end it "continues on if no cloud_init config present" do allow(vm).to receive(:cloud_init_configs).and_return([]) expect(app).to receive(:call).with(env).ordered expect(subject).not_to receive(:setup_user_data) expect(subject).not_to receive(:write_cfg_iso) 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 it "builds a MIME message and prepares a disc to be attached" do expect(subject).to receive(:read_text_cfg).twice expect(subject).to receive(:generate_cfg_msg) subject.setup_user_data(machine, env, cloud_init_configs) end end describe "#read_text_cfg" do let(:cfg_text) { "config: true" } it "takes a text cfg path and saves it as a MIME text message" do mime_text_part = double("mime_text_part") expect(mime_text_part).not_to receive(:disposition=) allow(File).to receive(:read).and_return(cfg_text) expect(Vagrant::Util::Mime::Entity).to receive(:new).with(cfg_text, "text/cloud-config").and_return(mime_text_part) subject.read_text_cfg(machine, cfg) end it "takes a text cfg inline string and saves it as a MIME text message" do mime_text_part = double("mime_text_part") expect(mime_text_part).not_to receive(:disposition=) expect(Vagrant::Util::Mime::Entity).to receive(:new).with("data: true", "text/cloud-config").and_return(mime_text_part) subject.read_text_cfg(machine, cfg_inline) end it "takes a text cfg inline string with content_disposition_filename and saves it as a MIME text message" do mime_text_part = double("mime_text_part") expect(mime_text_part).to receive(:disposition=).with("attachment; filename=\"test.ps1\"") expect(Vagrant::Util::Mime::Entity).to receive(:new).with("#ps1_sysnative\n", "text/x-shellscript").and_return(mime_text_part) subject.read_text_cfg(machine, cfg_with_content_disposition_filename_inline) end end describe "#generate_cfg_msg" do it "creates a miltipart mixed message of combined configs" do message = subject.generate_cfg_msg(machine, text_cfgs) expect(message).to be_a(Vagrant::Util::Mime::Multipart) end it "sets a MIME-Version header" do message = subject.generate_cfg_msg(machine, text_cfgs) expect(message.headers["MIME-Version"]).to eq("1.0") end end describe "#write_cfg_iso" 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 message = subject.generate_cfg_msg(machine, text_cfgs) allow(host).to receive(:capability?).with(:create_iso).and_return(false) expect{subject.write_cfg_iso(machine, env, message, {})}.to raise_error(Vagrant::Errors::CreateIsoHostCapNotFound) end it "creates a temp dir with the cloud_init config and generates an iso" do message = subject.generate_cfg_msg(machine, text_cfgs) allow(host).to receive(:capability?).with(:create_iso).and_return(true) allow(Dir).to receive(:mktmpdir).and_return(source_dir) expect(File).to receive(:open).with("#{source_dir}/user-data", 'w').and_return(true) expect(File).to receive(:open).with("#{source_dir}/meta-data", 'w').and_yield(meta_data_file) expect(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) 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 let(:iso_path) { Pathname.new("fake/iso/path") } it "creates a new disk config based on the iso_path" do expect(vm.disks).to receive(:each) subject.attach_disk_config(machine, env, iso_path) end end end ================================================ FILE: test/unit/vagrant/action/builtin/cloud_init_wait_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::CloudInitWait do let(:app) { lambda { |env| } } let(:config) { double("config", :vm => vm) } let(:comm) { double("comm") } 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"]) } 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) end it "raises an error when cloud init not installed" do allow(comm).to receive(:test).with("command -v cloud-init").and_return(false) expect { subject.call(env) }. to raise_error(Vagrant::Errors::CloudInitNotFound) end it "raises an error when cloud init command fails" do expect(comm).to receive(:sudo).with("cloud-init status --wait", any_args).and_return(1) 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 allow(comm).to receive(:test).with("command -v cloud-init").and_return(true) end it "does not wait for cloud init if there are no cloud init configs" do expect(comm).to_not receive(:sudo).with("cloud-init status --wait", any_args) subject.call(env) end end end end ================================================ FILE: test/unit/vagrant/action/builtin/confirm_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::Confirm do let(:app) { lambda { |env| } } let(:env) { { ui: Vagrant::UI::Silent.new } } let(:message) { "foo" } ["y", "Y"].each do |valid| it "should set the result to true if '#{valid}' is given" do expect(env[:ui]).to receive(:ask).with(message).and_return(valid) described_class.new(app, env, message).call(env) expect(env[:result]).to be end end it "should set the result to true if force matches" do force_key = :tubes env[force_key] = true described_class.new(app, env, message, force_key).call(env) expect(env[:result]).to be end it "should ask if force is not true" do force_key = :tubes env[force_key] = false expect(env[:ui]).to receive(:ask).with(message).and_return("nope") described_class.new(app, env, message).call(env) expect(env[:result]).not_to be end it "should set result to false if anything else is given" do expect(env[:ui]).to receive(:ask).with(message).and_return("nope") described_class.new(app, env, message).call(env) expect(env[:result]).not_to be end it "should ask multiple times if an allowed set is given and response isn't in that set" do times = 0 allow(env[:ui]).to receive(:ask) do |arg| expect(arg).to eql(message) times += 1 if times <= 3 "nope" else "y" end end described_class.new(app, env, message, allowed: ["y", "N"]).call(env) expect(env[:result]).to be(true) expect(times).to eq(4) end end ================================================ FILE: test/unit/vagrant/action/builtin/delayed_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::Delayed do let(:app) { lambda {|*_|} } let(:env) { {} } it "should raise error when callable does not provide #call" do expect { described_class.new(app, env, true) }. to raise_error(TypeError) end it "should delay executing action to end of stack" do result = [] one = proc{ |*_| result << :one } two = proc{ |*_| result << :two } builder = Vagrant::Action::Builder.build(described_class, two) builder.use(one) builder.call(env) expect(result.first).to eq(:one) expect(result.last).to eq(:two) end end ================================================ FILE: test/unit/vagrant/action/builtin/disk_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::Disk do let(:app) { lambda { |env| } } let(:vm) { double("vm") } let(:config) { double("config", vm: vm) } let(:provider) { double("provider") } let(:machine) { double("machine", config: config, provider: provider, provider_name: "provider", data_dir: Pathname.new("/fake/dir")) } let(:env) { { ui: ui, machine: machine} } let(:disks) { [double("disk")] } let(:ui) { Vagrant::UI::Silent.new } let(:disk_data) { {disk: [{uuid: "123456789", name: "storage"}], floppy: [], dvd: []} } describe "#call" do it "calls configure_disks if disk config present" do allow(vm).to receive(:disks).and_return(disks) allow(machine).to receive(:disks).and_return(disks) allow(machine.provider).to receive(:capability?).with(:configure_disks).and_return(true) subject = described_class.new(app, env) expect(app).to receive(:call).with(env).ordered expect(machine.provider).to receive(:capability). with(:configure_disks, disks).and_return(disk_data) expect(subject).to receive(:write_disk_metadata).and_return(true) subject.call(env) end 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).to receive(:write_disk_metadata) subject.call(env) end it "prints a warning if disk config capability is unsupported" 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) expect(ui).to receive(:warn) subject.call(env) end it "writes down a disk_meta file if disks are configured" do subject = described_class.new(app, env) expect(File).to receive(:open).with("/fake/dir/disk_meta", "w+").and_return(true) subject.write_disk_metadata(machine, disk_data) end end end ================================================ FILE: test/unit/vagrant/action/builtin/env_set_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::EnvSet do let(:app) { lambda { |env| } } let(:env) { {} } it "should set the new environment" do described_class.new(app, env, foo: :bar).call(env) expect(env[:foo]).to eq(:bar) end it "should call the next middleware" do callable = lambda { |env| env[:called] = env[:foo] } expect(env[:called]).to be_nil described_class.new(callable, env, foo: :yep).call(env) expect(env[:called]).to eq(:yep) end end ================================================ FILE: test/unit/vagrant/action/builtin/graceful_halt_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::GracefulHalt do let(:app) { lambda { |env| } } let(:env) { { machine: machine, ui: Vagrant::UI::Silent.new } } let(:machine) do result = double("machine") allow(result).to receive(:config).and_return(machine_config) allow(result).to receive(:guest).and_return(machine_guest) allow(result).to receive(:state).and_return(machine_state) result end let(:machine_config) do double("machine_config").tap do |top_config| vm_config = double("machine_vm_config") allow(vm_config).to receive(:graceful_halt_timeout).and_return(10) allow(top_config).to receive(:vm).and_return(vm_config) end end let(:machine_guest) { double("machine_guest") } let(:machine_state) do double("machine_state").tap do |result| allow(result).to receive(:id).and_return(:unknown) end end let(:target_state) { :target } let(:ui) do double("ui").tap do |result| allow(result).to receive(:output) end end it "should do nothing if force is specified" do env[:force_halt] = true expect(machine_guest).not_to receive(:capability) described_class.new(app, env, target_state).call(env) expect(env[:result]).to eq(false) end it "should do nothing if there is an invalid source state" do allow(machine_state).to receive(:id).and_return(:invalid_source) expect(machine_guest).not_to receive(:capability) described_class.new(app, env, target_state, :target_source).call(env) expect(env[:result]).to eq(false) end it "should gracefully halt and wait for the target state" do expect(machine_guest).to receive(:capability).with(:halt).once allow(machine_state).to receive(:id).and_return(target_state) described_class.new(app, env, target_state).call(env) expect(env[:result]).to eq(true) end end ================================================ FILE: test/unit/vagrant/action/builtin/handle_box_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::HandleBox do include_context "unit" let(:app) { lambda { |env| } } let(:env) { { action_runner: action_runner, machine: machine, ui: Vagrant::UI::Silent.new, } } subject { described_class.new(app, env) } let(:iso_env) do # We have to create a Vagrantfile so there is a root path isolated_environment.tap do |env| env.vagrantfile("") end end let(:iso_vagrant_env) { iso_env.create_vagrant_env } let(:action_runner) { double("action_runner") } let(:box) do box_dir = iso_env.box3("foo", "1.0", :virtualbox) Vagrant::Box.new("foo", :virtualbox, "1.0", box_dir) end let(:machine) { iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy) } it "works if there is no box set" do machine.config.vm.box = nil machine.config.vm.box_url = nil expect(app).to receive(:call).with(env) subject.call(env) end it "works if box is empty string" do machine.config.vm.box = "" machine.config.vm.box_url = nil expect(app).to receive(:call).with(env) subject.call(env) end it "doesn't do anything if a box exists" do allow(machine).to receive(:box).and_return(box) expect(action_runner).to receive(:run).never expect(app).to receive(:call).with(env) subject.call(env) end context "with a box set and no box_url" do before do allow(machine).to receive(:box).and_return(nil) machine.config.vm.box = "foo" end it "adds a box that doesn't exist" do expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_name]).to eq(machine.config.vm.box) expect(opts[:box_url]).to eq(machine.config.vm.box) expect(opts[:box_provider]).to eq(:dummy) expect(opts[:box_version]).to eq(machine.config.vm.box_version) true } expect(app).to receive(:call).with(env) subject.call(env) end it "adds a box using any format the provider allows" do machine.provider_options[:box_format] = [:foo, :bar] expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_name]).to eq(machine.config.vm.box) expect(opts[:box_url]).to eq(machine.config.vm.box) expect(opts[:box_provider]).to eq([:foo, :bar]) expect(opts[:box_version]).to eq(machine.config.vm.box_version) true } expect(app).to receive(:call).with(env) subject.call(env) end end context "with a box and box_url set" do before do allow(machine).to receive(:box).and_return(nil) machine.config.vm.box = "foo" machine.config.vm.box_url = "bar" end it "adds a box that doesn't exist" do expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_name]).to eq(machine.config.vm.box) expect(opts[:box_url]).to eq(machine.config.vm.box_url) expect(opts[:box_provider]).to eq(:dummy) expect(opts[:box_version]).to eq(machine.config.vm.box_version) true } expect(app).to receive(:call).with(env) subject.call(env) end end context "with a box with a checksum set" do before do allow(machine).to receive(:box).and_return(nil) machine.config.vm.box = "foo" machine.config.vm.box_url = "bar" machine.config.vm.box_download_checksum_type = "sha256" machine.config.vm.box_download_checksum = "1f42ac2decf0169c4af02b2d8c77143ce35f7ba87d5d844e19bf7cbb34fbe74e" end it "adds a box that doesn't exist and maps checksum options correctly" do expect(action_runner).to receive(:run).with(any_args) { |action, opts| expect(opts[:box_name]).to eq(machine.config.vm.box) expect(opts[:box_url]).to eq(machine.config.vm.box_url) expect(opts[:box_provider]).to eq(:dummy) expect(opts[:box_version]).to eq(machine.config.vm.box_version) expect(opts[:box_checksum_type]).to eq(machine.config.vm.box_download_checksum_type) expect(opts[:box_checksum]).to eq(machine.config.vm.box_download_checksum) true } expect(app).to receive(:call).with(env) subject.call(env) end end end ================================================ FILE: test/unit/vagrant/action/builtin/handle_forwarded_port_collisions_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/vm") describe Vagrant::Action::Builtin::HandleForwardedPortCollisions do include_context "unit" let(:app) { lambda { |env| } } let(:env) { { machine: machine, ui: ui, port_collision_extra_in_use: extra_in_use, port_collision_remap: collision_remap, port_collision_repair: collision_repair, port_collision_port_check: collision_port_check } } let(:provider_name) { "default" } let(:extra_in_use){ nil } let(:collision_remap){ nil } let(:collision_repair){ nil } let(:collision_port_check){ nil } let(:port_check_method){ nil } let(:machine) do double("machine").tap do |machine| allow(machine).to receive(:provider_name).and_return(provider_name) allow(machine).to receive(:config).and_return(machine_config) allow(machine).to receive(:env).and_return(machine_env) end end let(:machine_config) do double("machine_config").tap do |config| allow(config).to receive(:vm).and_return(vm_config) end end let(:data_dir){ temporary_dir } let(:machine_env) do isolated_environment.tap do |i_env| allow(i_env).to receive(:data_dir).and_return(data_dir) allow(i_env).to receive(:lock).and_yield end end let(:vm_config) do double("machine_vm_config").tap do |config| allow(config).to receive(:usable_port_range).and_return(1000..2000) allow(config).to receive(:networks).and_return([]) end end let(:ui){ Vagrant::UI::Silent.new } let(:instance){ described_class.new(app, env) } describe "#call" do it "should create a lock while action runs" do expect(machine_env).to receive(:lock).with("fpcollision").and_yield instance.call(env) end context "with extra ports in use provided as Array type" do let(:extra_in_use){ [80] } it "should not generate an error" do expect{ instance.call(env) }.not_to raise_error end end context "with forwarded port defined" do let(:port_options){ {guest: 80, host: 8080} } before do expect(vm_config).to receive(:networks).and_return([[:forwarded_port, port_options]]).twice allow(instance).to receive(:ipv4_addresses).and_return(["127.0.0.1"]) end it "should check if host port is in use" do expect(instance).to receive(:is_forwarded_already).and_return(false) expect(Vagrant::Util::IsPortOpen).to receive(:is_port_open?).and_return(false) instance.call(env) end context "with forwarded port already in use" do let(:extra_in_use){ [8080] } it "should raise a port collision error" do expect{ instance.call(env) }.to raise_error(Vagrant::Errors::ForwardPortCollision) end context "with auto_correct enabled" do before{ port_options[:auto_correct] = true } it "should raise a port collision error" do expect{ instance.call(env) }.to raise_error(Vagrant::Errors::ForwardPortCollision) end context "with collision repair enabled" do let(:collision_repair){ true } it "should automatically correct collision" do expect{ instance.call(env) }.not_to raise_error end end end end context "with custom port_check method" do let(:check_result){ [] } let(:port_options){ {guest: 80, host: 8080, host_ip: "127.0.1.1"} } context "that accepts two parameters" do let(:collision_port_check) do lambda do |host_ip, host_port| check_result.push(host_ip) check_result.push(host_port) false end end it "should receive both host_ip and host_port" do instance.call(env) expect(check_result).to include(port_options[:host]) expect(check_result).to include(port_options[:host_ip]) end end context "that accepts one parameter" do let(:collision_port_check) do lambda do |host_port| check_result.push(host_port) false end end it "should receive the host_port only" do instance.call(env) expect(check_result).to eq([port_options[:host]]) end end end end end describe "#recover" do end describe "#port_check" do let(:host_ip){ "127.0.0.1" } let(:host_port){ 8080 } let(:interfaces) { [ ["lo0", "127.0.0.1"], ["eth0", "192.168.1.7"] ] } before do instance.instance_variable_set(:@machine, machine) allow(Vagrant::Util::IPv4Interfaces).to receive(:ipv4_interfaces).and_return(interfaces) end it "should check if the port is open" do expect(Vagrant::Util::IsPortOpen).to receive(:is_port_open?).with(host_ip, host_port).and_return(true) instance.send(:port_check, host_ip, host_port) end context "when host ip is 0.0.0.0" do let(:host_ip) { "0.0.0.0" } context "on windows" do before do expect(Vagrant::Util::Platform).to receive(:windows?).and_return(true) end it "should check the port on every IPv4 interface" do expect(Vagrant::Util::IsPortOpen).to receive(:is_port_open?).with(interfaces[0][1], host_port) expect(Vagrant::Util::IsPortOpen).to receive(:is_port_open?).with(interfaces[1][1], host_port) instance.send(:port_check, host_ip, host_port) end it "should return false if the port is closed on any IPv4 interfaces" do expect(Vagrant::Util::IsPortOpen).to receive(:is_port_open?).with(interfaces[0][1], host_port). and_return(true) expect(Vagrant::Util::IsPortOpen).to receive(:is_port_open?).with(interfaces[1][1], host_port). and_return(false) expect(instance.send(:port_check, host_ip, host_port)).to be(false) end it "should return true if the port is open on all IPv4 interfaces" do expect(Vagrant::Util::IsPortOpen).to receive(:is_port_open?).with(interfaces[0][1], host_port). and_return(true) expect(Vagrant::Util::IsPortOpen).to receive(:is_port_open?).with(interfaces[1][1], host_port). and_return(true) expect(instance.send(:port_check, host_ip, host_port)).to be(true) end end end context "when host ip does not exist" do let(:host_ip) { "192.168.99.100" } let(:name) { "default" } it "should not raise an error" do allow(machine).to receive(:name).and_return(name) expect{ instance.send(:port_check, host_ip, host_port) }. not_to raise_error end end context "with loopback address" do let (:host_ip) { "127.1.2.40" } it "should check if the port is open" do expect(Vagrant::Util::IsPortOpen).to receive(:is_port_open?).with(host_ip, host_port).and_return(true) instance.send(:port_check, host_ip, host_port) end end end end ================================================ FILE: test/unit/vagrant/action/builtin/has_provisioner_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::HasProvisioner do include_context "unit" let(:provisioner_one) { double("provisioner_one") } let(:provisioner_two) { double("provisioner_two") } let(:provisioners) { [provisioner_one, provisioner_two] } let(:machine) { double("machine") } let(:ui) { Vagrant::UI::Silent.new } let(:env) {{ machine: machine, ui: ui, root_path: Pathname.new(".") }} let(:app) { lambda { |*args| }} subject { described_class.new(app, env) } describe "#call" do before do allow(provisioner_one).to receive(:communicator_required).and_return(true) allow(provisioner_one).to receive(:name) allow(provisioner_one).to receive(:type) allow(provisioner_two).to receive(:communicator_required).and_return(false) allow(provisioner_two).to receive(:name) allow(provisioner_two).to receive(:type) allow(machine).to receive_message_chain(:config, :vm, :provisioners).and_return(provisioners) end context "provider has capability :has_communicator" do before do allow(machine).to receive_message_chain(:provider, :capability?).with(:has_communicator).and_return(true) end it "does not skip any provisioners if provider has ssh" do allow(machine).to receive_message_chain(:provider, :capability).with(:has_communicator).and_return(true) expect(provisioner_one).to_not receive(:communicator_required) expect(provisioner_two).to_not receive(:communicator_required) subject.call(env) expect(env[:skip]).to eq([]) end it "skips provisioners that require a communicator if provider does not have ssh" do allow(machine).to receive_message_chain(:provider, :capability).with(:has_communicator).and_return(false) expect(provisioner_one).to receive(:communicator_required) expect(provisioner_two).to receive(:communicator_required) expect(provisioner_one).to receive(:run=).with(:never) subject.call(env) expect(env[:skip]).to eq([provisioner_one]) end end context "provider does not have capability :has_communicator" do before do allow(machine).to receive_message_chain(:provider, :capability?).with(:has_communicator).and_return(false) end it "does not skip any provisioners" do expect(provisioner_one).to_not receive(:communicator_required) expect(provisioner_two).to_not receive(:communicator_required) subject.call(env) expect(env[:skip]).to eq([]) end end end end ================================================ FILE: test/unit/vagrant/action/builtin/is_env_set_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 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 ================================================ FILE: test/unit/vagrant/action/builtin/is_state_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::IsState do let(:app) { lambda { |env| } } let(:env) { { machine: machine } } let(:machine) do double("machine").tap do |machine| allow(machine).to receive(:state).and_return(state) end end let(:state) { double("state") } describe "#call" do it "sets result to false if is proper state" do allow(state).to receive(:id).and_return(:foo) subject = described_class.new(app, env, :bar) expect(app).to receive(:call).with(env) subject.call(env) expect(env[:result]).to be(false) end it "sets result to true if is proper state" do allow(state).to receive(:id).and_return(:foo) subject = described_class.new(app, env, :foo) expect(app).to receive(:call).with(env) subject.call(env) expect(env[:result]).to be(true) end it "inverts the result if specified" do allow(state).to receive(:id).and_return(:foo) subject = described_class.new(app, env, :foo, invert: true) expect(app).to receive(:call).with(env) subject.call(env) expect(env[:result]).to be(false) end end end ================================================ FILE: test/unit/vagrant/action/builtin/lock_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::Lock do let(:app) { lambda { |env| } } let(:env) { {} } let(:lock_path) do Dir::Tmpname.create("vagrant-test-lock") {} end let(:options) do { exception: Class.new(StandardError), path: lock_path } end after do File.unlink(lock_path) if File.file?(lock_path) end it "should require a path" do expect { described_class.new(app, env) }. to raise_error(ArgumentError) expect { described_class.new(app, env, path: "foo") }. to raise_error(ArgumentError) expect { described_class.new(app, env, exception: "foo") }. to raise_error(ArgumentError) expect { described_class.new(app, env, path: "bar", exception: "foo") }. to_not raise_error end it "should allow the path to be a proc" do inner_acquire = true app = lambda do |env| File.open(lock_path, "w+") do |f| inner_acquire = f.flock(File::LOCK_EX | File::LOCK_NB) end end options[:path] = lambda { |env| lock_path } instance = described_class.new(app, env, options) instance.call(env) expect(inner_acquire).to eq(false) end it "should allow the exception to be a proc" do exception = options[:exception] options[:exception] = lambda { |env| exception } File.open(lock_path, "w+") do |f| # Acquire lock expect(f.flock(File::LOCK_EX | File::LOCK_NB)).to eq(0) # Test! instance = described_class.new(app, env, options) expect { instance.call(env) }. to raise_error(exception) end end it "should call the middleware with the lock held" do inner_acquire = true app = lambda do |env| File.open(lock_path, "w+") do |f| inner_acquire = f.flock(File::LOCK_EX | File::LOCK_NB) end end instance = described_class.new(app, env, options) instance.call(env) expect(inner_acquire).to eq(false) end it "should raise an exception if the lock is already held" do File.open(lock_path, "w+") do |f| # Acquire lock expect(f.flock(File::LOCK_EX | File::LOCK_NB)).to eq(0) # Test! instance = described_class.new(app, env, options) expect { instance.call(env) }. to raise_error(options[:exception]) end end it "should allow nesting locks within the same middleware sequence" do called = false app = lambda { |env| called = true } inner = described_class.new(app, env, options) outer = described_class.new(inner, env, options) outer.call(env) expect(called).to eq(true) end end ================================================ FILE: test/unit/vagrant/action/builtin/message_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::Message do let(:app) { lambda { |env| } } let(:env) { { ui: ui } } let(:ui) { Vagrant::UI::Silent.new } describe "#call" do it "outputs the given message" do subject = described_class.new(app, env, "foo") expect(ui).to receive(:output).with("foo").ordered expect(app).to receive(:call).with(env).ordered subject.call(env) end it "outputs the given message after the call" do subject = described_class.new(app, env, "foo", post: true) expect(app).to receive(:call).with(env).ordered expect(ui).to receive(:output).with("foo").ordered subject.call(env) end end end ================================================ FILE: test/unit/vagrant/action/builtin/mixin_provisioners_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/vm") require "vagrant/action/builtin/mixin_provisioners" describe Vagrant::Action::Builtin::MixinProvisioners do include_context "unit" let(:sandbox) { isolated_environment } let(:iso_env) do # We have to create a Vagrantfile so there is a root path sandbox.vagrantfile("") sandbox.create_vagrant_env end let(:provisioner_config){ double("provisioner_config", name: nil) } let(:provisioner_one) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("spec-test", :shell) prov.config = provisioner_config prov end let(:provisioner_two) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("spec-test", :shell) prov.config = provisioner_config prov end let(:provisioner_three) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new(nil, :shell) provisioner_config = double("provisioner_config", name: "my_shell") prov.config = provisioner_config prov end let(:provisioner_instances) { [provisioner_one,provisioner_two,provisioner_three] } let(:ui) { Vagrant::UI::Silent.new } let(:vm) { double("vm", provisioners: provisioner_instances) } let(:config) { double("config", vm: vm) } let(:machine) { double("machine", ui: ui, config: config) } let(:env) {{ machine: machine, ui: machine.ui, root_path: Pathname.new(".") }} subject do Class.new do extend Vagrant::Action::Builtin::MixinProvisioners end end after do sandbox.close described_class.reset! end describe "#provisioner_instances" do it "returns all the instances of configured provisioners" do result = subject.provisioner_instances(env) expect(result.size).to eq(provisioner_instances.size) shell_one = result.first expect(shell_one.first).to be_a(VagrantPlugins::Shell::Provisioner) shell_two = result[1] expect(shell_two.first).to be_a(VagrantPlugins::Shell::Provisioner) end it "returns all the instances of configured provisioners" do result = subject.provisioner_instances(env) expect(result.size).to eq(provisioner_instances.size) shell_one = result.first expect(shell_one[1][:name]).to eq(:"spec-test") shell_two = result[1] expect(shell_two[1][:name]).to eq(:"spec-test") shell_three = result[2] expect(shell_three[1][:name]).to eq(:"my_shell") end end context "#sort_provisioner_instances" do describe "with no dependency provisioners" do it "returns the original array" do result = subject.provisioner_instances(env) expect(result.size).to eq(provisioner_instances.size) shell_one = result.first expect(shell_one.first).to be_a(VagrantPlugins::Shell::Provisioner) shell_two = result[1] expect(shell_two.first).to be_a(VagrantPlugins::Shell::Provisioner) end end describe "with before and after dependency provisioners" do let(:provisioner_config){ {} } let(:provisioner_root) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell) prov.config = provisioner_config prov end let(:provisioner_before) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("before-test", :shell) prov.config = provisioner_config prov.before = "root-test" prov end let(:provisioner_after) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("after-test", :shell) prov.config = provisioner_config prov.after = "root-test" prov end let(:provisioner_instances) { [provisioner_root,provisioner_before,provisioner_after] } it "returns the array in the correct order" do result = subject.provisioner_instances(env) expect(result[0].last[:name]).to eq(:"before-test") expect(result[1].last[:name]).to eq(:"root-test") expect(result[2].last[:name]).to eq(:"after-test") end end describe "with before :each dependency provisioners" do let(:provisioner_config){ {} } let(:provisioner_root) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell) prov.config = provisioner_config prov end let(:provisioner_root2) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root2-test", :shell) prov.config = provisioner_config prov end let(:provisioner_before) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("before-test", :shell) prov.config = provisioner_config prov.before = :each prov end let(:provisioner_instances) { [provisioner_root,provisioner_root2,provisioner_before] } it "puts the each shortcut provisioners in place" do result = subject.provisioner_instances(env) expect(result[0].last[:name]).to eq(:"before-test") expect(result[1].last[:name]).to eq(:"root-test") expect(result[2].last[:name]).to eq(:"before-test") expect(result[3].last[:name]).to eq(:"root2-test") end end describe "with after :each dependency provisioners" do let(:provisioner_config){ {} } let(:provisioner_root) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell) prov.config = provisioner_config prov end let(:provisioner_root2) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root2-test", :shell) prov.config = provisioner_config prov end let(:provisioner_after) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("after-test", :shell) prov.config = provisioner_config prov.after = :each prov end let(:provisioner_instances) { [provisioner_root,provisioner_root2,provisioner_after] } it "puts the each shortcut provisioners in place" do result = subject.provisioner_instances(env) expect(result[0].last[:name]).to eq(:"root-test") expect(result[1].last[:name]).to eq(:"after-test") expect(result[2].last[:name]).to eq(:"root2-test") expect(result[3].last[:name]).to eq(:"after-test") end end describe "with before and after :each dependency provisioners" do let(:provisioner_config){ {} } let(:provisioner_root) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell) prov.config = provisioner_config prov end let(:provisioner_root2) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root2-test", :shell) prov.config = provisioner_config prov end let(:provisioner_after) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("after-test", :shell) prov.config = provisioner_config prov.after = :each prov end let(:provisioner_before) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("before-test", :shell) prov.config = provisioner_config prov.before = :each prov end let(:provisioner_instances) { [provisioner_root,provisioner_root2,provisioner_before,provisioner_after] } it "puts the each shortcut provisioners in place" do result = subject.provisioner_instances(env) expect(result[0].last[:name]).to eq(:"before-test") expect(result[1].last[:name]).to eq(:"root-test") expect(result[2].last[:name]).to eq(:"after-test") expect(result[3].last[:name]).to eq(:"before-test") expect(result[4].last[:name]).to eq(:"root2-test") expect(result[5].last[:name]).to eq(:"after-test") end end describe "with before and after :all dependency provisioners" do let(:provisioner_config){ {} } let(:provisioner_root) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root-test", :shell) prov.config = provisioner_config prov end let(:provisioner_root2) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("root2-test", :shell) prov.config = provisioner_config prov end let(:provisioner_after) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("after-test", :shell) prov.config = provisioner_config prov.after = :all prov end let(:provisioner_before) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("before-test", :shell) prov.config = provisioner_config prov.before = :all prov end let(:provisioner_instances) { [provisioner_root,provisioner_root2,provisioner_before,provisioner_after] } it "puts the each shortcut provisioners in place" do result = subject.provisioner_instances(env) expect(result[0].last[:name]).to eq(:"before-test") expect(result[1].last[:name]).to eq(:"root-test") expect(result[2].last[:name]).to eq(:"root2-test") expect(result[3].last[:name]).to eq(:"after-test") end end end end ================================================ FILE: test/unit/vagrant/action/builtin/mixin_synced_folders_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tmpdir" require File.expand_path("../../../../base", __FILE__) require "vagrant/action/builtin/mixin_synced_folders" describe Vagrant::Action::Builtin::MixinSyncedFolders do include_context "synced folder actions" subject do Class.new do extend Vagrant::Action::Builtin::MixinSyncedFolders end end let(:data_dir) { Pathname.new(Dir.mktmpdir("vagrant-test-mixin-synced-folders")) } let(:folders_class) { Vagrant::Plugin::V2::SyncedFolder::Collection } let(:machine) do double("machine").tap do |machine| allow(machine).to receive(:config).and_return(machine_config) allow(machine).to receive(:data_dir).and_return(data_dir) end end let(:machine_config) do double("machine_config").tap do |top_config| allow(top_config).to receive(:vm).and_return(vm_config) end end let(:vm_config) { double("machine_vm_config", :allowed_synced_folder_types => nil) } after do FileUtils.rm_rf(data_dir) end describe "default_synced_folder_type" do it "returns the usable implementation" do plugins = { "bad" => [impl(false, "bad"), 0], "good" => [impl(true, "good"), 1], "best" => [impl(true, "best"), 5], } result = subject.default_synced_folder_type(machine, plugins) expect(result).to eq("best") end it "filters based on allowed_synced_folder_types" do expect(vm_config).to receive(:allowed_synced_folder_types).and_return(["bad", "good"]) plugins = { "bad" => [impl(false, "bad"), 0], "good" => [impl(true, "good"), 1], "best" => [impl(true, "best"), 5], } result = subject.default_synced_folder_type(machine, plugins) expect(result).to eq("good") end it "reprioritizes based on allowed_synced_folder_types" do plugins = { "bad" => [impl(false, "bad"), 0], "good" => [impl(true, "good"), 1], "same" => [impl(true, "same"), 1], } expect(vm_config).to receive(:allowed_synced_folder_types).and_return(["good", "same"]) result = subject.default_synced_folder_type(machine, plugins) expect(result).to eq("good") expect(vm_config).to receive(:allowed_synced_folder_types).and_return(["same", "good"]) result = subject.default_synced_folder_type(machine, plugins) expect(result).to eq("same") end end describe "impl_opts" do it "should return only relevant keys" do env = { foo_bar: "baz", bar_bar: "nope", foo_baz: "bar", } result = subject.impl_opts("foo", env) expect(result.length).to eq(2) expect(result[:foo_bar]).to eq("baz") expect(result[:foo_baz]).to eq("bar") end end describe "synced_folders" do let(:folders) { folders_class.new } let(:plugins) { {} } before do plugins[:default] = [impl(true, "default"), 10] plugins[:nfs] = [impl(true, "nfs"), 5] allow(subject).to receive(:plugins).and_return(plugins) allow(vm_config).to receive(:synced_folders).and_return(folders) end it "should raise exception if bad type is given" do folders["root"] = { type: "bad" } expect { subject.synced_folders(machine) }. to raise_error(StandardError) end it "should return the proper set of folders" do folders["root"] = {} folders["another"] = { type: "" } folders["foo"] = { type: "default" } folders["nfs"] = { type: "nfs" } result = subject.synced_folders(machine) expect(result.length).to eq(2) expect(result[:default]).to eq({ "another" => folders["another"].merge(__vagrantfile: true, plugin: true), "foo" => folders["foo"].merge(__vagrantfile: true, plugin: true), "root" => folders["root"].merge(__vagrantfile: true, plugin: true), }) expect(result[:nfs]).to eq({ "nfs" => folders["nfs"].merge(__vagrantfile: true, plugin: true), }) expect(result.types).to eq([:default, :nfs]) end it "should return the proper set of folders of a custom config" do folders["root"] = {} folders["another"] = {} other_folders = { "bar" => {} } other = double("config") allow(other).to receive(:synced_folders).and_return(other_folders) result = subject.synced_folders(machine, config: other) expect(result.length).to eq(1) expect(result[:default]).to eq({ "bar" => other_folders["bar"].merge(plugin: true), }) expect(result.types).to eq([:default]) end it "should error if an explicit type is unusable" do plugins[:unusable] = [impl(false, "bad"), 15] folders["root"] = { type: "unusable" } expect { subject.synced_folders(machine) }. to raise_error(RuntimeError) end it "should ignore disabled folders" do folders["root"] = {} folders["foo"] = { disabled: true } result = subject.synced_folders(machine) expect(result.length).to eq(1) expect(result[:default].length).to eq(1) expect(result.types).to eq([:default]) end it "should scope hash override the settings" do folders["root"] = { hostpath: "foo", type: "nfs", nfs__foo: "bar", } result = subject.synced_folders(machine) expect(result[:nfs]["root"][:foo]).to eql("bar") expect(result.types).to eq([:nfs]) end it "returns {} if cached read with no cache" do result = subject.synced_folders(machine, cached: true) expect(result).to eql({}) expect(result.types).to eq([]) end it "should be able to save and retrieve cached versions" do folders["root"] = {} folders["another"] = { type: "" } folders["foo"] = { type: "default" } folders["nfs"] = { type: "nfs" } result = subject.synced_folders(machine) subject.save_synced_folders(machine, result) # Clear the folders so we know its reading from cache old_folders = folders.dup folders.clear result = subject.synced_folders(machine, cached: true) expect(result.length).to eq(2) expect(result[:default]).to eq({ "another" => old_folders["another"].merge(__vagrantfile: true, plugin: true), "foo" => old_folders["foo"].merge(__vagrantfile: true, plugin: true), "root" => old_folders["root"].merge(__vagrantfile: true, plugin: true), }) expect(result[:nfs]).to eq({ "nfs" => old_folders["nfs"].merge(__vagrantfile: true, plugin: true) }) expect(result.types).to eq([:default, :nfs]) end it "should be able to save and retrieve cached versions" do other_folders = {} other = double("config") allow(other).to receive(:synced_folders).and_return(other_folders) other_folders["foo"] = { type: "default" } result = subject.synced_folders(machine, config: other) subject.save_synced_folders(machine, result) # Clear the folders and set some more folders.clear folders["bar"] = { type: "default" } folders["baz"] = { type: "nfs" } result = subject.synced_folders(machine) subject.save_synced_folders(machine, result, merge: true) # Clear one last time folders.clear # Read them all back result = subject.synced_folders(machine, cached: true) expect(result.length).to eq(2) expect(result[:default]).to eq({ "foo" => { type: "default", plugin: true }, "bar" => { type: "default", __vagrantfile: true, plugin: true }, }) expect(result[:nfs]).to eq({ "baz" => { type: "nfs", __vagrantfile: true, plugin: true } }) expect(result.types).to eq([:default, :nfs]) end it "should remove items from the vagrantfile that were removed" do folders["foo"] = { type: "default" } result = subject.synced_folders(machine) subject.save_synced_folders(machine, result) # Clear the folders and set some more folders.clear folders["bar"] = { type: "default" } folders["baz"] = { type: "nfs" } result = subject.synced_folders(machine) subject.save_synced_folders(machine, result, merge: true, vagrantfile: true) # Clear one last time folders.clear # Read them all back result = subject.synced_folders(machine, cached: true) expect(result.length).to eq(2) expect(result[:default]).to eq({ "bar" => { type: "default", __vagrantfile: true, plugin: true}, }) expect(result[:nfs]).to eq({ "baz" => { type: "nfs", __vagrantfile: true, plugin: true } }) expect(result.types).to eq([:default, :nfs]) end end describe "#save_synced_folders" do let(:folders) { folders_class.new } let(:options) { {} } let(:output_file) { double("output_file") } before do allow(machine.data_dir).to receive(:join).with("synced_folders"). and_return(output_file) allow(output_file).to receive(:open).and_yield(output_file) allow(output_file).to receive(:write) end it "should write empty hash to file" do expect(output_file).to receive(:write).with("{}") subject.save_synced_folders(machine, folders, options) end context "when folder data is defined" do let(:folders) { {"root" => { hostpath: "foo", type: "nfs", nfs__foo: "bar"}} } it "should write folder information to file" do expect(output_file).to receive(:write).with(JSON.dump(folders)) subject.save_synced_folders(machine, folders, options) end end end describe "#synced_folders_diff" do it "sees two equal " do one = { default: { "foo" => {} }, } two = { default: { "foo" => {} }, } expect(subject.synced_folders_diff(one, two)).to be_empty end it "sees modifications" do one = { default: { "foo" => {} }, } two = { default: { "foo" => { hostpath: "foo" } }, } result = subject.synced_folders_diff(one, two) expect(result[:modified]).to_not be_empty end it "sees adding" do one = { default: { "foo" => {} }, } two = { default: { "foo" => {}, "bar" => {}, }, } result = subject.synced_folders_diff(one, two) expect(result[:added]).to_not be_empty expect(result[:removed]).to be_empty expect(result[:modified]).to be_empty end it "sees removing" do one = { default: { "foo" => {} }, } two = { default: {}, } result = subject.synced_folders_diff(one, two) expect(result[:added]).to be_empty expect(result[:removed]).to_not be_empty expect(result[:modified]).to be_empty end end end ================================================ FILE: test/unit/vagrant/action/builtin/provision_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/vm") describe Vagrant::Action::Builtin::Provision do include_context "unit" let(:app) { lambda { |env| } } let(:env) { { machine: machine, ui: ui, hook: hook, provision_ignore_sentinel: false } } let(:hook){ double("hook") } let(:machine) do double("machine").tap do |machine| allow(machine).to receive(:id).and_return('machine-id') allow(machine).to receive(:data_dir).and_return(data_dir) allow(machine).to receive(:config).and_return(machine_config) allow(machine).to receive(:env).and_return(machine_env) end end let(:machine_config) do double("machine_config").tap do |config| allow(config).to receive(:vm).and_return(vm_config) end end let(:data_dir){ temporary_dir } let(:machine_env) do isolated_environment.tap do |i_env| allow(i_env).to receive(:data_dir).and_return(data_dir) allow(i_env).to receive(:lock).and_yield end end let(:vm_config) do double("machine_vm_config").tap do |config| allow(config).to receive(:provisioners).and_return([]) end end let(:ui) { Vagrant::UI::Silent.new } let(:instance){ described_class.new(app, env) } describe "#call" do context "with no provisioners defined" do it "should process empty set of provisioners" do expect(instance.call(env)).to eq([]) end context "with provisioning disabled" do before{ env[:provision_enabled] = false } after{ env.delete(:provision_enabled) } it "should not process any provisioners" do expect(instance.call(env)).to be_nil end end end context "with single provisioner defined" do let(:provisioner) do prov = VagrantPlugins::Kernel_V2::VagrantConfigProvisioner.new("spec-test", :shell) prov.config = provisioner_config prov end let(:provisioner_config){ double("provisioner_config", name: "spec-test") } before{ expect(vm_config).to receive(:provisioners).and_return([provisioner]) } it "should call the defined provisioner" do expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end context "with provisioning disabled" do before{ env[:provision_enabled] = false } after{ env.delete(:provision_enabled) } it "should not process any provisioners" do expect(hook).not_to receive(:call).with(:provisioner_run, anything) expect(instance.call(env)).to be_nil end end context "with provisioner configured to run once" do before{ provisioner.run = :once } it "should run if machine is not provisioned" do expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should not run if machine is provisioned" do File.open(File.join(data_dir.to_s, "action_provision"), "w") do |file| file.write("1.5:machine-id") end expect(hook).not_to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should not run if provision types are set and provisioner is not included" do env[:provision_types] = [:"other-provisioner", :"other-test"] expect(hook).not_to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should run if provision types are set and include provisioner name" do env[:provision_types] = [:"spec-test"] expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should run if provision types are set and include provisioner type" do env[:provision_types] = [:shell] expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end end context "with provisioner configured to run always" do before{ provisioner.run = :always } it "should run if machine is not provisioned" do expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should run if machine is provisioned" do File.open(File.join(data_dir.to_s, "action_provision"), "w") do |file| file.write("1.5:machine-id") end expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should not run if provision types are set and provisioner is not included" do env[:provision_types] = [:"other-provisioner", :"other-test"] expect(hook).not_to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should run if provision types are set and include provisioner name" do env[:provision_types] = [:"spec-test"] expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should run if provision types are set and include provisioner type" do env[:provision_types] = [:shell] expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end end context "with provisioner configured to never run" do before{ provisioner.run = :never } it "should not run if machine is not provisioned" do expect(hook).not_to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should not run if machine is provisioned" do File.open(File.join(data_dir.to_s, "action_provision"), "w") do |file| file.write("1.5:machine-id") end expect(hook).not_to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should not run if provision types are set and provisioner is not included" do env[:provision_types] = [:"other-provisioner", :"other-test"] expect(hook).not_to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should run if provision types are set and include provisioner name" do env[:provision_types] = [:"spec-test"] expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should run if provision types are set and include provisioner name and machine is provisioned" do File.open(File.join(data_dir.to_s, "action_provision"), "w") do |file| file.write("1.5:machine-id") end env[:provision_types] = [:"spec-test"] expect(hook).to receive(:call).with(:provisioner_run, anything) instance.call(env) end it "should not run if provision types are set and include provisioner type" do env[:provision_types] = [:shell] expect(hook).not_to receive(:call).with(:provisioner_run, anything) instance.call(env) end end end end end ================================================ FILE: test/unit/vagrant/action/builtin/provisioner_cleanup_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::ProvisionerCleanup do let(:app) { lambda { |env| } } let(:env) { { machine: machine, ui: ui } } let(:machine) do double("machine").tap do |machine| allow(machine).to receive(:config).and_return(machine_config) end end let(:machine_config) do double("machine_config").tap do |config| allow(config).to receive(:vm).and_return(vm_config) end end let(:vm_config) { double("machine_vm_config") } let(:ui) { Vagrant::UI::Silent.new } let(:provisioner) do Class.new(Vagrant.plugin("2", :provisioner)) end before do allow_any_instance_of(described_class).to receive(:provisioner_type_map) .and_return(provisioner => :test_provisioner) allow_any_instance_of(described_class).to receive(:provisioner_instances) .and_return([provisioner]) end describe "initialize with :before" do it "runs cleanup before" do instance = described_class.new(app, env, :before) expect(provisioner).to receive(:cleanup).ordered expect(app).to receive(:call).ordered instance.call(env) end end describe "initialize with :after" do it "runs cleanup after" do instance = described_class.new(app, env, :after) expect(app).to receive(:call).ordered expect(provisioner).to receive(:cleanup).ordered instance.call(env) end end it "only runs cleanup tasks if the subclass defines it" do parent = Class.new do class_variable_set(:@@cleanup, false) def self.called? class_variable_get(:@@cleanup) end def cleanup self.class.class_variable_set(:@@cleanup) end end child = Class.new(parent) allow_any_instance_of(described_class).to receive(:provisioner_type_map) .and_return(child => :test_provisioner) allow_any_instance_of(described_class).to receive(:provisioner_instances) .and_return([child]) expect(parent.called?).to be(false) instance = described_class.new(app, env) instance.call(env) expect(parent.called?).to be(false) end end ================================================ FILE: test/unit/vagrant/action/builtin/set_hostname_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::SetHostname do let(:env) { { machine: machine, ui: ui } } let(:app) { lambda { |env| } } let(:machine) { double("machine") } let(:ui) { Vagrant::UI::Silent.new } subject { described_class.new(app, env) } before do allow(machine).to receive_message_chain(:config, :vm, :hostname).and_return("whatever") allow(machine).to receive_message_chain(:guest, :capability) end it "should change hostname if hosts modification enabled" do allow(machine).to receive_message_chain(:config, :vm, :allow_hosts_modification).and_return(true) expect(machine).to receive(:guest) subject.call(env) end it "should not change hostname if hosts modification disabled" do allow(machine).to receive_message_chain(:config, :vm, :allow_hosts_modification).and_return(false) expect(machine).not_to receive(:guest) subject.call(env) end end ================================================ FILE: test/unit/vagrant/action/builtin/ssh_exec_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::SSHExec do let(:app) { lambda { |env| } } let(:env) { { machine: machine } } let(:machine) do result = double("machine") allow(result).to receive(:ssh_info).and_return(machine_ssh_info) result end let(:machine_ssh_info) { {} } let(:ssh_klass) { Vagrant::Util::SSH } before(:each) do # Stub the methods so that even if we test incorrectly, no side # effects actually happen. allow(ssh_klass).to receive(:exec) end it "should raise an exception if SSH is not ready" do not_ready_machine = double("machine") allow(not_ready_machine).to receive(:ssh_info).and_return(nil) env[:machine] = not_ready_machine expect { described_class.new(app, env).call(env) }. to raise_error(Vagrant::Errors::SSHNotReady) end it "should exec with the SSH info in the env if given" do ssh_info = { foo: :bar } expect(ssh_klass).to receive(:exec). with(ssh_info, nil) env[:ssh_info] = ssh_info described_class.new(app, env).call(env) end it "should exec with the options given in `ssh_opts`" do ssh_opts = { foo: :bar } expect(ssh_klass).to receive(:exec). with(machine_ssh_info, ssh_opts) env[:ssh_opts] = ssh_opts described_class.new(app, env).call(env) end end ================================================ FILE: test/unit/vagrant/action/builtin/ssh_run_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::SSHRun do let(:app) { lambda { |env| } } let(:env) { { machine: machine, tty: true } } # SSH configuration information mock let(:ssh) do double("ssh", timeout: 1, host: nil, port: 5986, guest_port: 5986, pty: false, keep_alive: false, insert_key: false, shell: 'bash -l' ) end let(:vm) do double("vm", communicator: nil ) end # Configuration mock let(:config) { double("config", ssh: ssh, vm: vm) } let(:machine) do double("machine", config: config,) end let(:machine_ssh_info) { {} } let(:ssh_klass) { Vagrant::Util::SSH } before(:each) do # Stub the methods so that even if we test incorrectly, no side # effects actually happen. allow(ssh_klass).to receive(:exec) allow(machine).to receive(:ssh_info).and_return(machine_ssh_info) end it "should raise an exception if SSH is not ready" do not_ready_machine = double("machine") allow(not_ready_machine).to receive(:ssh_info).and_return(nil) env[:machine] = not_ready_machine expect { described_class.new(app, env).call(env) }. to raise_error(Vagrant::Errors::SSHNotReady) end it "should exec with the SSH info in the env if given" do ssh_info = { foo: :bar } opts = {:extra_args=>["-t", "bash -l -c 'echo test'"], :subprocess=>true} expect(ssh_klass).to receive(:exec). with(ssh_info, opts) env[:ssh_info] = ssh_info env[:ssh_run_command] = "echo test" described_class.new(app, env).call(env) end it "should exec with the SSH info in the env if given and disable tty" do ssh_info = { foo: :bar } opts = {:extra_args=>["bash -l -c 'echo test'"], :subprocess=>true} env[:tty] = false expect(ssh_klass).to receive(:exec). with(ssh_info, opts) env[:ssh_info] = ssh_info env[:ssh_run_command] = "echo test" described_class.new(app, env).call(env) end it "should exec with the options given in `ssh_opts`" do ssh_opts = { foo: :bar } expect(ssh_klass).to receive(:exec). with(machine_ssh_info, ssh_opts) env[:ssh_opts] = ssh_opts env[:ssh_run_command] = "echo test" described_class.new(app, env).call(env) end context "when using the WinSSH communicator" do let(:winssh) { double("winssh", shell: "foo") } before do expect(vm).to receive(:communicator).and_return(:winssh) expect(config).to receive(:winssh).and_return(winssh) env[:tty] = nil end it "should use the WinSSH shell for running ssh commands" do ssh_info = { foo: :bar } opts = {:extra_args=>["foo -c 'dir'"], :subprocess=>true} expect(ssh_klass).to receive(:exec). with(ssh_info, opts) env[:ssh_info] = ssh_info env[:ssh_run_command] = "dir" described_class.new(app, env).call(env) end context "when shell is cmd" do before do expect(winssh).to receive(:shell).and_return('cmd') end it "should use appropriate options for cmd" do ssh_info = { foo: :bar } opts = {:extra_args=>["cmd /C dir "], :subprocess=>true} expect(ssh_klass).to receive(:exec). with(ssh_info, opts) env[:ssh_info] = ssh_info env[:ssh_run_command] = "dir" described_class.new(app, env).call(env) end end context "when shell is powershell" do before do expect(winssh).to receive(:shell).and_return('powershell') end it "should base64 encode the command" do ssh_info = { foo: :bar } encoded_command = "JABQAHIAbwBnAHIAZQBzAHMAUAByAGUAZgBlAHIAZQBuAGMAZQAgAD0AIAAiAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAIgA7ACAAZABpAHIA" opts = {:extra_args=>["powershell -encodedCommand #{encoded_command}"], :subprocess=>true} expect(ssh_klass).to receive(:exec). with(ssh_info, opts) env[:ssh_info] = ssh_info env[:ssh_run_command] = "dir" described_class.new(app, env).call(env) end end end end ================================================ FILE: test/unit/vagrant/action/builtin/synced_folder_cleanup_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::SyncedFolderCleanup do include_context "synced folder actions" let(:app) { lambda { |env| } } let(:env) { { machine: machine, ui: ui } } let(:machine) do double("machine").tap do |machine| allow(machine).to receive(:config).and_return(machine_config) end end let(:machine_config) do double("machine_config").tap do |top_config| allow(top_config).to receive(:vm).and_return(vm_config) end end let(:vm_config) { double("machine_vm_config") } let(:ui) { Vagrant::UI::Silent.new } subject { described_class.new(app, env) } def create_cleanup_tracker Class.new(impl(true, "good")) do class_variable_set(:@@clean, false) def self.clean class_variable_get(:@@clean) end def cleanup(machine, opts) self.class.class_variable_set(:@@clean, true) end end end describe "call" do let(:synced_folders) { {} } let(:plugins) { {} } before do plugins[:default] = [impl(true, "default"), 10] plugins[:nfs] = [impl(true, "nfs"), 5] env[:machine] = Object.new env[:root_path] = Pathname.new(Dir.mktmpdir("vagrant-test-synced-folder-cleanup-call")) allow(subject).to receive(:plugins).and_return(plugins) allow(subject).to receive(:synced_folders).and_return(synced_folders) end after do FileUtils.rm_rf(env[:root_path]) end it "should invoke cleanup" do tracker = create_cleanup_tracker plugins[:tracker] = [tracker, 15] synced_folders["tracker"] = { "root" => { hostpath: "foo", }, "other" => { hostpath: "bar", create: true, } } expect_any_instance_of(tracker).to receive(:cleanup). with(env[:machine], { tracker_foo: :bar }) # Test that the impl-specific opts are passed through env[:tracker_foo] = :bar subject.call(env) end it "should invoke cleanup once per implementation" do trackers = [] (0..2).each do |tracker| trackers << create_cleanup_tracker end plugins[:tracker_0] = [trackers[0], 15] plugins[:tracker_1] = [trackers[1], 15] plugins[:tracker_2] = [trackers[2], 15] synced_folders["tracker_0"] = { "root" => { hostpath: "foo" }, "other" => { hostpath: "bar", create: true } } synced_folders["tracker_1"] = { "root" => { hostpath: "foo" } } synced_folders["tracker_2"] = { "root" => { hostpath: "foo" }, "other" => { hostpath: "bar", create: true }, "another" => { hostpath: "baz" } } subject.call(env) expect(trackers[0].clean).to be(true) expect(trackers[1].clean).to be(true) expect(trackers[2].clean).to be(true) end end end ================================================ FILE: test/unit/vagrant/action/builtin/synced_folders_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/vm") describe Vagrant::Action::Builtin::SyncedFolders do include_context "unit" include_context "synced folder actions" let(:app) { lambda { |env| } } let(:env) { { machine: machine, ui: ui } } let(:machine) do double("machine").tap do |machine| allow(machine).to receive(:config).and_return(machine_config) end end let(:machine_config) do double("machine_config").tap do |top_config| allow(top_config).to receive(:vm).and_return(vm_config) end end let(:vm_config) { double("machine_vm_config") } let(:ui) { Vagrant::UI::Silent.new } subject { described_class.new(app, env) } describe "call" do let(:synced_folders) { {} } let(:plugins) { {} } before do plugins[:default] = [impl(true, "default"), 10] plugins[:nfs] = [impl(true, "nfs"), 5] env[:root_path] = Pathname.new(Dir.mktmpdir("vagrant-test-synced-folders-call")) allow(subject).to receive(:plugins).and_return(plugins) allow(subject).to receive(:synced_folders).and_return(synced_folders) allow(subject).to receive(:save_synced_folders) allow(machine).to receive_message_chain(:guest, :capability?).with(:persist_mount_shared_folder).and_return(false) end after do FileUtils.rm_rf(env[:root_path]) end it "should create on the host if specified" do synced_folders["default"] = { "root" => { hostpath: "foo", }, "other" => { hostpath: "bar", create: true, } } subject.call(env) expect(env[:root_path].join("foo")).not_to be_directory expect(env[:root_path].join("bar")).to be_directory end it "doesn't expand the host path if told not to" do called_folders = nil tracker = Class.new(impl(true, "good")) do define_method(:prepare) do |machine, folders, opts| called_folders = folders end end plugins[:tracker] = [tracker, 15] synced_folders["tracker"] = { "root" => { hostpath: "foo", hostpath_exact: true, }, "other" => { hostpath: "/bar", } } subject.call(env) expect(called_folders).to_not be_nil expect(called_folders["root"][:hostpath]).to eq("foo") end it "expands the host path relative to the root path" do called_folders = nil tracker = Class.new(impl(true, "good")) do define_method(:prepare) do |machine, folders, opts| called_folders = folders end end plugins[:tracker] = [tracker, 15] synced_folders["tracker"] = { "root" => { hostpath: "foo", }, "other" => { hostpath: "/bar", } } subject.call(env) expect(called_folders).to_not be_nil expect(called_folders["root"][:hostpath]).to eq( Pathname.new(File.expand_path( called_folders["root"][:hostpath], env[:root_path])).to_s) end it "should invoke prepare then enable" do ids = [] order = [] tracker = Class.new(impl(true, "good")) do define_method(:prepare) do |machine, folders, opts| ids << self.object_id order << :prepare end define_method(:enable) do |machine, folders, opts| ids << self.object_id order << :enable end end plugins[:tracker] = [tracker, 15] synced_folders["tracker"] = { "root" => { hostpath: "foo", }, "other" => { hostpath: "bar", create: true, } } subject.call(env) expect(order).to eq([:prepare, :enable]) expect(ids.length).to eq(2) expect(ids[0]).to eq(ids[1]) end it "syncs custom folders" do ids = [] order = [] tracker = Class.new(impl(true, "good")) do define_method(:prepare) do |machine, folders, opts| ids << self.object_id order << :prepare end define_method(:enable) do |machine, folders, opts| ids << self.object_id order << :enable end end plugins[:tracker] = [tracker, 15] synced_folders["tracker"] = { "root" => { hostpath: "foo", }, "other" => { hostpath: "bar", create: true, } } new_config = double("config") env[:synced_folders_config] = new_config expect(subject).to receive(:synced_folders). with(machine, config: new_config, cached: false). and_return(synced_folders) subject.call(env) expect(order).to eq([:prepare, :enable]) expect(ids.length).to eq(2) expect(ids[0]).to eq(ids[1]) end context "with folders from the machine" do it "removes outdated folders not present in config" do expect(subject).to receive(:save_synced_folders).with( machine, anything, merge: true, vagrantfile: true) subject.call(env) end end context "with custom folders" do before do new_config = double("config") env[:synced_folders_config] = new_config allow(subject).to receive(:synced_folders). with(machine, config: new_config, cached: false). and_return({}) end it "doesn't remove outdated folders not present in config" do expect(subject).to receive(:save_synced_folders).with( machine, anything, merge: true) subject.call(env) end end context "with guest capability to persist synced folders" do it "persists folders" do synced_folders["default"] = { "root" => { hostpath: "foo", }, "other" => { hostpath: "bar", create: true, } } allow(machine).to receive_message_chain(:guest, :capability?).with(:persist_mount_shared_folder).and_return(true) expect(vm_config).to receive(:allow_fstab_modification).and_return(true) expect(machine).to receive_message_chain(:guest, :capability).with(:persist_mount_shared_folder, synced_folders) subject.call(env) end it "does not persists folders if configured to not do so" do allow(machine).to receive_message_chain(:guest, :capability?).with(:persist_mount_shared_folder).and_return(true) expect(vm_config).to receive(:allow_fstab_modification).and_return(false) expect(machine).to receive_message_chain(:guest, :capability).with(:persist_mount_shared_folder, nil) subject.call(env) end end context "when guest is not available" do it "does not persist folders if guest is not available" do allow(machine).to receive_message_chain(:guest, :capability?).and_raise(Vagrant::Errors::MachineGuestNotReady) expect(vm_config).to_not receive(:allow_fstab_modification) subject.call(env) end end end end ================================================ FILE: test/unit/vagrant/action/builtin/trigger_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::Trigger do let(:app) { lambda { |env| } } let(:env) { {machine: machine} } let(:machine) { nil } let(:triggers) { double("triggers") } let(:name) { "trigger-name" } let(:timing) { :before } let(:type) { :action } let(:subject) { described_class. new(app, env, name, triggers, timing, type) } before do allow(triggers).to receive(:fire) allow(app).to receive(:call) end it "should properly create a new instance" do expect(subject).to be_a(described_class) end it "should fire triggers" do expect(triggers).to receive(:fire) subject.call(env) end it "should fire triggers without machine name" do expect(triggers).to receive(:fire).with(name, timing, nil, type, anything) subject.call(env) end context "when machine is provided" do let(:machine) { double("machine", name: "machine-name") } it "should include machine name when firing triggers" do expect(triggers).to receive(:fire).with(name, timing, "machine-name", type, anything) subject.call(env) end end context "when timing is :before" do it "should not error" do expect { subject }.not_to raise_error end end context "when timing is :after" do it "should not error" do expect { subject }.not_to raise_error end end context "when timing is not :before or :after" do let(:timing) { :unknown } it "should raise error" do expect { subject }.to raise_error(ArgumentError) end end end ================================================ FILE: test/unit/vagrant/action/builtin/wait_for_communicator_test.rb ================================================ require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::Builtin::WaitForCommunicator do let(:app) { lambda { |env| } } let(:ui) { lambda { |env| } } let(:env) { { machine: machine, ui: ui } } let(:vm) do double("vm", communicator: nil ) end # Configuration mock let(:config) { double("config", vm: vm) } # Communicate mock let(:communicate) { double("communicate") } let(:state) { double("state") } let(:ui) { Vagrant::UI::Silent.new } let(:machine) do double("machine", config: config, communicate: communicate, state: state,) end before do allow(vm).to receive(:boot_timeout).and_return(1) allow(communicate).to receive(:wait_for_ready).with(1).and_return(true) end it "raise an error if a bad state is encountered" do allow(state).to receive(:id).and_return(:stopped) expect { described_class.new(app, env, [:running]).call(env) }. to raise_error(Vagrant::Errors::VMBootBadState) end it "raise an error if the vm doesn't boot" do allow(communicate).to receive(:wait_for_ready).and_return(false) allow(state).to receive(:id).and_return(:running) expect { described_class.new(app, env, [:running]).call(env) }. to raise_error(Vagrant::Errors::VMBootTimeout) end it "succeed when a valid state is encountered" do allow(communicate).to receive(:wait_for_ready).and_return(true) allow(state).to receive(:id).and_return(:running) expect { described_class.new(app, env, [:running]).call(env) }. to_not raise_error end end ================================================ FILE: test/unit/vagrant/action/general/package_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Action::General::Package do let(:app) { double("app", call: nil) } let(:env) { {env: environment, machine: machine, ui: ui} } let(:environment) { double("environment") } let(:machine) { double("machine") } let(:ui) { Vagrant::UI::Silent.new } let(:subject) { described_class.new(app, env) } before do allow_any_instance_of(Vagrant::Errors::VagrantError). to receive(:translate_error) end describe ".validate!" do let(:output) { double("output", to_s: "output-path") } let(:directory) { double("directory") } before do allow(described_class).to receive(:fullpath).and_return(output) allow(File).to receive(:directory?).with(output).and_return(false) allow(File).to receive(:directory?).with(directory).and_return(true) allow(File).to receive(:exist?).and_return(false) allow(Vagrant::Util::Presence).to receive(:present?).with(directory).and_return(true) end it "should not raise an error when options are valid" do expect { described_class.validate!(output, directory) }.not_to raise_error end it "should raise error when output directory exists" do expect(File).to receive(:directory?).with(output).and_return(true) expect { described_class.validate!(output, directory) }.to raise_error(Vagrant::Errors::PackageOutputDirectory) end it "should raise error if output path exists" do expect(File).to receive(:exist?).with(output).and_return(true) expect { described_class.validate!(output, directory) }.to raise_error(Vagrant::Errors::PackageOutputExists) end it "should raise error if directory value not provided" do expect(Vagrant::Util::Presence).to receive(:present?).and_call_original expect { described_class.validate!(output, nil) }.to raise_error(Vagrant::Errors::PackageRequiresDirectory) end it "should raise error if directory path is not a directory" do expect(File).to receive(:directory?).with(directory).and_return(false) expect { described_class.validate!(output, directory) }.to raise_error(Vagrant::Errors::PackageRequiresDirectory) end end describe "#package_with_folder_path" do let(:expanded_path) { double("expanded_path") } before do allow(File).to receive(:expand_path).and_return(expanded_path) end it "should create box folder if it does not exist" do expect(File).to receive(:directory?).with(expanded_path).and_return(false) expect(subject).to receive(:create_box_folder).with(expanded_path) subject.package_with_folder_path end it "should not create box folder if it already exists" do expect(File).to receive(:directory?).with(expanded_path).and_return(true) expect(subject).not_to receive(:create_box_folder) subject.package_with_folder_path end end describe "#create_box_folder" do let(:path) { double("path") } before do allow(FileUtils).to receive(:mkdir_p) subject.instance_variable_set(:@env, env) end it "should notify user of new directory creation" do expect(I18n).to receive(:t).with(an_instance_of(String), hash_including(folder_path: path)) subject.create_box_folder(path) end it "should create the directory" do expect(FileUtils).to receive(:mkdir_p).with(path) subject.create_box_folder(path) end end describe "#recover" do let(:env) { {"vagrant.error" => error} } let(:error) { nil } let(:fullpath) { double("fullpath") } before { allow(described_class).to receive(:fullpath).and_return(fullpath) } it "should delete packaged files if they exist" do expect(File).to receive(:exist?).with(fullpath).and_return(true) expect(File).to receive(:delete).with(fullpath) subject.recover(env) end it "should not delete anything if package files do not exist" do expect(File).to receive(:exist?).with(fullpath).and_return(false) expect(File).not_to receive(:delete).with(fullpath) subject.recover(env) end context "when vagrant error is PackageOutputDirectory" do let(:error) { Vagrant::Errors::PackageOutputDirectory.new } it "should not do anything" do expect(File).not_to receive(:exist?) expect(File).not_to receive(:delete) subject.recover(env) end end context "when vagrant error is PackageOutputExists" do let(:error) { Vagrant::Errors::PackageOutputExists.new } it "should not do anything" do expect(File).not_to receive(:exist?) expect(File).not_to receive(:delete) subject.recover(env) end end end describe "#copy_include_files" do let(:package_directory) { @package_directory } let(:package_files) { Dir.glob(File.join(@package_files_directory, "*")).map {|i| [i, File.basename(i)] } } before do @package_directory = Dir.mktmpdir @package_files_directory = Dir.mktmpdir 3.times { |i| FileUtils.touch(File.join(@package_files_directory, "file.#{i}")) } env["package.files"] = package_files env["package.directory"] = package_directory subject.instance_variable_set(:@env, env) end after do FileUtils.rm_rf(@package_directory) FileUtils.rm_rf(@package_files_directory) end it "should copy all files to package directory" do subject.copy_include_files expected_files = package_files.map(&:last).map { |f| File.join(package_directory, "include", f) }.sort expect( Dir.glob(File.join(package_directory, "**", "*.*")).sort ).to eq(expected_files) end it "should notify user of copy" do expect(ui).to receive(:info).at_least(1).and_call_original subject.copy_include_files end end describe "#copy_info" do let(:package_directory) { @package_directory } let(:package_info) { File.join(@package_info_directory, "info.json") } before do @package_directory = Dir.mktmpdir @package_info_directory = Dir.mktmpdir FileUtils.touch(File.join(@package_info_directory, "info.json")) env["package.info"] = package_info env["package.directory"] = package_directory subject.instance_variable_set(:@env, env) allow(ui).to receive(:info) end after do FileUtils.rm_rf(@package_directory) FileUtils.rm_rf(@package_info_directory) end it "should copy the specified info.json to package directory" do subject.copy_info expected_info = File.join(package_directory, "info.json") expect(File.file? expected_info).to be_truthy end end describe "#compress" do let(:package_directory) { @package_directory } let(:fullpath) { "PATH" } before do @package_directory = Dir.mktmpdir FileUtils.touch(File.join(@package_directory, "test-file1")) env["package.directory"] = package_directory subject.instance_variable_set(:@env, env) allow(subject).to receive(:fullpath).and_return(fullpath) end after do FileUtils.rm_rf(package_directory) end it "should change directory into package directory" do expect(Vagrant::Util::SafeChdir).to receive(:safe_chdir).with(package_directory) subject.compress end it "should compress files using bsdtar" do expect(Vagrant::Util::SafeChdir).to receive(:safe_chdir).with(package_directory).and_call_original expect(Vagrant::Util::Subprocess).to receive(:execute).with("bsdtar", any_args, "./test-file1") subject.compress end end describe "#write_metadata_json" do let(:metadata_path) { File.join(package_directory, "metadata.json") } let(:package_directory) { @package_directory } let(:machine_provider) { "machine-provider" } let(:default_provider) { "default-provider" } before do @package_directory = Dir.mktmpdir env["package.directory"] = @package_directory subject.instance_variable_set(:@env, env) allow(machine).to receive(:provider_name).and_return(machine_provider) allow(environment).to receive(:default_provider).and_return(default_provider) end after { FileUtils.rm_rf(@package_directory) } it "should not create a metadata.json file if it already exists" do expect(File).to receive(:exist?).with(metadata_path).and_return(true) expect(File).not_to receive(:write) subject.write_metadata_json end it "should write a metadata file" do expect(File).to receive(:write).with(metadata_path, any_args) subject.write_metadata_json end it "should write machine provider to metadata file" do subject.write_metadata_json content = JSON.load(File.read(metadata_path)) expect(content["provider"]).to eq(machine_provider) end context "when machine provider is unset" do let(:machine_provider) { nil } it "should write default provider to metadata file" do subject.write_metadata_json content = JSON.load(File.read(metadata_path)) expect(content["provider"]).to eq(default_provider) end end context "when machine provider and default provider are unset" do let(:machine_provider) { nil } let(:default_provider) { nil } it "should not write metadata file" do subject.write_metadata_json expect(File.exist?(metadata_path)).to be_falsey end end end describe "#setup_private_key" do let(:package_directory) { @package_directory } let(:private_key_path) { File.join(package_directory, "datadir", "private_key") } let(:new_key_path) { File.join(package_directory, "vagrant_private_key") } let(:vagrantfile_path) { File.join(package_directory, "Vagrantfile") } let(:data_dir) { Pathname.new(File.join(package_directory, "datadir")) } let(:config) { double("config", ssh: double("ssh", private_key_path: [private_key_path])) } before do @package_directory = Dir.mktmpdir env["package.directory"] = @package_directory FileUtils.mkdir(File.join(@package_directory, "datadir")) File.write(private_key_path, "SSH KEY") subject.instance_variable_set(:@env, env) allow(machine).to receive(:data_dir).and_return(data_dir) allow(machine).to receive(:config).and_return(config) end after { FileUtils.rm_rf(@package_directory) } it "should create a new Vagrantfile" do subject.setup_private_key expect(File.exist?(vagrantfile_path)).to be_truthy end it "should create the new private ssh key" do subject.setup_private_key expect(File.exist?(new_key_path)).to be_truthy end it "should copy the contents of the ssh key" do subject.setup_private_key expect(File.read(new_key_path)).to eq(File.read(private_key_path)) end context "with no machine provided" do before { env.delete(:machine) } it "should not create a private ssh key file" do subject.setup_private_key expect(File.exist?(new_key_path)).to be_falsey end end context "when vagrant_private_key exists" do let(:private_key_path) { File.join(package_directory, "datadir", "vagrant_private_key") } it "should create the new private ssh key" do subject.setup_private_key expect(File.exist?(new_key_path)).to be_truthy end it "should copy the contents of the ssh key" do subject.setup_private_key expect(File.read(new_key_path)).to eq(File.read(private_key_path)) end end end describe "#call" do let(:fullpath) { "FULLPATH" } before do allow(described_class).to receive(:validate!) allow(subject).to receive(:fullpath).and_return(fullpath) allow(subject).to receive(:package_with_folder_path) allow(subject).to receive(:copy_include_files) allow(subject).to receive(:setup_private_key) allow(subject).to receive(:write_metadata_json) allow(subject).to receive(:compress) end it "should validate required arguments" do expect(described_class).to receive(:validate!) subject.call(env) end it "should raise error if output path is a directory" do expect(File).to receive(:directory?).with(fullpath).and_return(true) expect { subject.call(env) }.to raise_error(Vagrant::Errors::PackageOutputDirectory) end it "should call the next middleware" do expect(app).to receive(:call) subject.call(env) end it "should notify of package compressing" do expect(ui).to receive(:info).and_call_original subject.call(env) end it "should copy include files" do expect(subject).to receive(:copy_include_files) subject.call(env) end it "should setup private ssh key" do expect(subject).to receive(:setup_private_key) subject.call(env) end it "should write metadata json file" do expect(subject).to receive(:write_metadata_json) subject.call(env) end it "should compress the box" do expect(subject).to receive(:compress) subject.call(env) end end end ================================================ FILE: test/unit/vagrant/action/hook_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/action/builder" require "vagrant/action/hook" describe Vagrant::Action::Hook do describe "defaults" do describe '#after_hooks' do subject { super().after_hooks } it { should be_empty } end describe '#before_hooks' do subject { super().before_hooks } it { should be_empty } end describe '#append_hooks' do subject { super().append_hooks } it { should be_empty } end describe '#prepend_hooks' do subject { super().prepend_hooks } it { should be_empty } end end describe "before hooks" do let(:existing) { "foo" } it "should append them" do block = Proc.new {} subject.before(existing, 1) subject.before(existing, 2) subject.before(existing, 3, :arg, &block) hooks = subject.before_hooks[existing] expect(hooks.size).to eq(3) expect(hooks[0].middleware).to eq(1) expect(hooks[0].arguments.parameters).to eq([]) expect(hooks[0].arguments.keywords).to eq({}) expect(hooks[0].arguments.block).to be_nil expect(hooks[1].middleware).to eq(2) expect(hooks[1].arguments.parameters).to eq([]) expect(hooks[1].arguments.keywords).to eq({}) expect(hooks[1].arguments.block).to be_nil expect(hooks[2].middleware).to eq(3) expect(hooks[2].arguments.parameters).to eq([:arg]) expect(hooks[2].arguments.keywords).to eq({}) expect(hooks[2].arguments.block).to eq(block) end end describe "after hooks" do let(:existing) { "foo" } it "should append them" do block = Proc.new {} subject.after(existing, 1) subject.after(existing, 2) subject.after(existing, 3, :arg, &block) hooks = subject.after_hooks[existing] expect(hooks.size).to eq(3) expect(hooks[0].middleware).to eq(1) expect(hooks[0].arguments.parameters).to eq([]) expect(hooks[0].arguments.keywords).to eq({}) expect(hooks[0].arguments.block).to be_nil expect(hooks[1].middleware).to eq(2) expect(hooks[1].arguments.parameters).to eq([]) expect(hooks[1].arguments.keywords).to eq({}) expect(hooks[1].arguments.block).to be_nil expect(hooks[2].middleware).to eq(3) expect(hooks[2].arguments.parameters).to eq([:arg]) expect(hooks[2].arguments.keywords).to eq({}) expect(hooks[2].arguments.block).to eq(block) end end describe "append" do it "should make a list" do block = Proc.new {} subject.append(1) subject.append(2) subject.append(3, :arg, &block) hooks = subject.append_hooks expect(hooks.size).to eq(3) expect(hooks[0].middleware).to eq(1) expect(hooks[0].arguments.parameters).to eq([]) expect(hooks[0].arguments.keywords).to eq({}) expect(hooks[0].arguments.block).to be_nil expect(hooks[1].middleware).to eq(2) expect(hooks[1].arguments.parameters).to eq([]) expect(hooks[1].arguments.keywords).to eq({}) expect(hooks[1].arguments.block).to be_nil expect(hooks[2].middleware).to eq(3) expect(hooks[2].arguments.parameters).to eq([:arg]) expect(hooks[2].arguments.keywords).to eq({}) expect(hooks[2].arguments.block).to eq(block) end end describe "prepend" do it "should make a list" do block = Proc.new {} subject.prepend(1) subject.prepend(2) subject.prepend(3, :arg, &block) hooks = subject.prepend_hooks expect(hooks.size).to eq(3) expect(hooks[0].middleware).to eq(1) expect(hooks[0].arguments.parameters).to eq([]) expect(hooks[0].arguments.keywords).to eq({}) expect(hooks[0].arguments.block).to be_nil expect(hooks[1].middleware).to eq(2) expect(hooks[1].arguments.parameters).to eq([]) expect(hooks[1].arguments.keywords).to eq({}) expect(hooks[1].arguments.block).to be_nil expect(hooks[2].middleware).to eq(3) expect(hooks[2].arguments.parameters).to eq([:arg]) expect(hooks[2].arguments.keywords).to eq({}) expect(hooks[2].arguments.block).to eq(block) end end describe "applying" do let(:builder) { Vagrant::Action::Builder.new } it "should build the proper stack" do subject.prepend("1", 2) subject.append("9") subject.after("1", "2") subject.before("9", "8") subject.apply(builder) stack = builder.stack expect(stack[0].middleware).to eq("1") expect(stack[0].arguments.parameters).to eq([2]) expect(stack[1].middleware).to eq("2") expect(stack[1].arguments.parameters).to eq([]) expect(stack[2].middleware).to eq("8") expect(stack[2].arguments.parameters).to eq([]) expect(stack[3].middleware).to eq("9") expect(stack[3].arguments.parameters).to eq([]) end it "should not prepend or append if disabled" do builder.use("3") builder.use("8") subject.prepend("1", 2) subject.append("9") subject.after("3", "4") subject.before("8", "7") subject.apply(builder, no_prepend_or_append: true) stack = builder.stack expect(stack[0].middleware).to eq("3") expect(stack[1].middleware).to eq("4") expect(stack[2].middleware).to eq("7") expect(stack[3].middleware).to eq("8") end end end ================================================ FILE: test/unit/vagrant/action/runner_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) describe Vagrant::Action::Runner do let(:instance) { described_class.new(action_name: "test") } it "should raise an error if an invalid callable is given" do expect { instance.run(7) }.to raise_error(ArgumentError, /must be a callable/) end it "should be able to use a Proc as a callable" do callable = Proc.new { raise Exception, "BOOM" } expect { instance.run(callable) }.to raise_error(Exception, "BOOM") end it "should be able to use a Method instance as a callable" do klass = Class.new do def action(env) raise Exception, "BANG" end end callable = klass.new.method(:action) expect { instance.run(callable) }.to raise_error(Exception, "BANG") end it "should be able to use a Class as a callable" do callable = Class.new do def initialize(app, env) end def self.name "TestAction" end def call(env) raise Exception, "BOOM" end end expect { instance.run(callable) }.to raise_error(Exception, "BOOM") end it "should be able to use a Class as a callable with no name attribute" do callable = Class.new do def initialize(app, env) end def call(env) raise Exception, "BOOM" end end expect { instance.run(callable) }.to raise_error(Exception, "BOOM") end it "should return the resulting environment" do callable = lambda do |env| env[:data] = "value" # Return nil so we can make sure it isn't using this return value nil end result = instance.run(callable) expect(result[:data]).to eq("value") end it "should pass options into hash given to callable" do result = nil callable = lambda do |env| result = env["data"] end instance.run(callable, "data" => "foo") expect(result).to eq("foo") end it "should pass global options into the hash" do result = nil callable = lambda do |env| result = env["data"] end instance = described_class.new("data" => "bar", action_name: "test") instance.run(callable) expect(result).to eq("bar") end it "should yield the block passed to the init method to get lazy loaded globals" do result = nil callable = lambda do |env| result = env["data"] end instance = described_class.new { { "data" => "bar", action_name: "test" } } instance.run(callable) expect(result).to eq("bar") end describe "triggers" do let(:environment) { double("environment", ui: nil) } let(:machine) { double("machine", triggers: machine_triggers, name: "") } let(:env_triggers) { double("env_triggers", find: []) } let(:machine_triggers) { double("machine_triggers", find: []) } before do allow(environment).to receive_message_chain(:vagrantfile, :config, :trigger) allow(Vagrant::Plugin::V2::Trigger).to receive(:new). and_return(env_triggers) end context "when only environment is provided" do let(:instance) { described_class.new(action_name: "test", env: environment) } it "should use environment to build new trigger" do expect(environment).to receive_message_chain(:vagrantfile, :config, :trigger) instance.run(lambda{|_|}) end it "should pass environment based triggers to callable" do callable = lambda { |env| expect(env[:triggers]).to eq(env_triggers) } instance.run(callable) end end context "when only machine is provided" do let(:instance) { described_class.new(action_name: "test", machine: machine) } it "should pass machine based triggers to callable" do callable = lambda { |env| expect(env[:triggers]).to eq(machine_triggers) } instance.run(callable) end end context "when both environment and machine is provided" do let(:instance) { described_class.new(action_name: "test", machine: machine, env: environment) } it "should pass machine based triggers to callable" do callable = lambda { |env| expect(env[:triggers]).to eq(machine_triggers) } instance.run(callable) end end end end ================================================ FILE: test/unit/vagrant/action/warden_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) describe Vagrant::Action::Warden do class ActionOne def initialize(app, env) @app = app end def call(env) env[:data] << 1 if env[:data] @app.call(env) end def recover(env) env[:recover] << 1 end end class ActionTwo def initialize(app, env) @app = app end def call(env) env[:data] << 2 if env[:data] @app.call(env) end def recover(env) env[:recover] << 2 end end class ExitAction def initialize(app, env) @app = app end def call(env) @app.call(env) end def recover(env) env[:recover] = true end end let(:data) { { data: [] } } let(:instance) { described_class.new } # This returns a proc that can be used with the builder # that simply appends data to an array in the env. def appender_proc(data) Proc.new { |env| env[:data] << data } end it "calls the actions like normal" do instance = described_class.new([appender_proc(1), appender_proc(2)], data) instance.call(data) expect(data[:data]).to eq([1, 2]) end it "starts a recovery sequence when an exception is raised" do error_proc = Proc.new { raise "ERROR!" } data = { recover: [] } instance = described_class.new([ActionOne, ActionTwo, error_proc], data) # The error should be raised back up expect { instance.call(data) }. to raise_error(RuntimeError) # Verify the recovery process goes in reverse order expect(data[:recover]).to eq([2, 1]) # Verify that the error is available in the data expect(data["vagrant.error"]).to be_kind_of(RuntimeError) end it "does not do a recovery sequence if SystemExit is raised" do # Make a proc that just calls "abort" which raises a # SystemExit exception. error_proc = Proc.new { abort } instance = described_class.new([ExitAction, error_proc], data) # The SystemExit should come through expect { instance.call(data) }.to raise_error(SystemExit) # The recover should not have been called expect(data.key?(:recover)).not_to be end it "does not do a recovery sequence if NoMemoryError is raised" do error_proc = Proc.new { raise NoMemoryError } instance = described_class.new([ExitAction, error_proc], data) # The SystemExit should come through expect { instance.call(data) }.to raise_error(NoMemoryError) # The recover should not have been called expect(data.key?(:recover)).not_to be end end ================================================ FILE: test/unit/vagrant/alias_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require "vagrant/alias" describe Vagrant::Alias do include_context "unit" include_context "command plugin helpers" let(:iso_env) { isolated_environment } let(:env) { iso_env.create_vagrant_env } describe "#interpret" do let(:interpreter) { described_class.new(env) } it "returns nil for comments" do comments = [ "# this is a comment", "# so is this ", " # and this", " # this too " ] comments.each do |comment| expect(interpreter.interpret(comment)).to be_nil end end it "raises an error on invalid keywords" do keywords = [ "keyword with a space = command", "keyword\twith a tab = command", "keyword\nwith a newline = command", ] keywords.each do |keyword| expect { interpreter.interpret(keyword) }.to raise_error(Vagrant::Errors::AliasInvalidError) end end it "properly interprets a simple alias" do keyword, command = interpreter.interpret("keyword=command") expect(keyword).to eq("keyword") expect(command).to eq("command") end it "properly interprets an alias with excess whitespace" do keyword, command = interpreter.interpret(" keyword = command ") expect(keyword).to eq("keyword") expect(command).to eq("command") end it "properly interprets an alias with an equals sign in the command" do keyword, command = interpreter.interpret(" keyword = command = command ") expect(keyword).to eq("keyword") expect(command).to eq("command = command") end it "allows keywords with non-alpha-numeric characters" do keyword, command = interpreter.interpret("keyword! = command") expect(keyword).to eq("keyword!") expect(command).to eq("command") end end end ================================================ FILE: test/unit/vagrant/batch_action_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'thread' require 'timeout' require File.expand_path("../../base", __FILE__) describe Vagrant::BatchAction do let(:called_actions) { [] } let!(:lock) { Mutex.new } let(:provider_name) { "test" } let(:provider_options) { {} } def new_machine(options) double("machine").tap do |m| allow(m).to receive(:provider_name).and_return(provider_name) allow(m).to receive(:provider_options).and_return(options) allow(m).to receive(:action) do |action, opts| lock.synchronize do called_actions << [m, action, opts] end end end end describe "#run" do let(:machine) { new_machine(provider_options) } let(:machine2) { new_machine(provider_options) } it "should run the actions on the machines in order" do subject.action(machine, "up") subject.action(machine2, "destroy") subject.run expect(called_actions.include?([machine, "up", nil])).to be expect(called_actions.include?([machine2, "destroy", nil])).to be end it "should run the arbitrary methods in order" do called = [] subject.custom(machine) { |m| called << m } subject.custom(machine2) { |m| called << m } subject.run expect(called[0]).to equal(machine) expect(called[1]).to equal(machine2) end it "should handle forks gracefully", :skip_windows do # Doesn't need to be tested on Windows since Windows doesn't # support fork(1) allow(machine).to receive(:action) do |action, opts| pid = fork if !pid # Child process exit end # Parent process, wait for the child to exit Timeout.timeout(1) do Process.waitpid(pid) end end subject.action(machine, "up") subject.run end context "with provider supporting parallel actions" do let(:provider_options) { {parallel: true} } it "should flag threads as being parallel actions" do parallel = nil subject.custom(machine) { |m| parallel = Thread.current[:batch_parallel_action] } subject.custom(machine) { |*_| } subject.run expect(parallel).to eq(true) end it "should exit the process if exit_code has been set" do subject.custom(machine) { |m| Thread.current[:exit_code] = 1} subject.custom(machine) { |*_| } expect(Process).to receive(:exit!).with(1) subject.run end end end end ================================================ FILE: test/unit/vagrant/box_collection_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) require "pathname" require 'tempfile' describe Vagrant::BoxCollection, :skip_windows, :bsdtar do include_context "unit" let(:box_class) { Vagrant::Box } let(:environment) { isolated_environment } subject { described_class.new(environment.boxes_dir) } it "should tell us the directory it is using" do expect(subject.directory).to eq(environment.boxes_dir) end describe "#all" do let(:ui) { Vagrant::UI::Silent.new } it "should return an empty array when no boxes are there" do expect(subject.all).to eq([]) end it "should return the boxes and their providers" do # Create some boxes environment.box3("foo", "1.0", :virtualbox) environment.box3("foo", "1.0", :vmware) environment.box3("bar", "0", :ec2) environment.box3("foo-VAGRANTSLASH-bar", "1.0", :virtualbox) environment.box3("foo-VAGRANTCOLON-colon", "1.0", :virtualbox) # Verify some output results = subject.all expect(results.length).to eq(5) expect(results).to include(["foo", "1.0", :virtualbox, nil]) expect(results).to include(["foo", "1.0", :vmware, nil]) expect(results).to include(["bar", "0", :ec2, nil]) expect(results).to include(["foo/bar", "1.0", :virtualbox, nil]) expect(results).to include(["foo:colon", "1.0", :virtualbox, nil]) end it "should return the boxes and their providers even if box has wrong version" do allow(Vagrant::UI::Prefixed).to receive(:new).and_return(ui) # Create some boxes environment.box3("foo", "fake-invalid-version", :virtualbox) environment.box3("foo", "1.0", :vmware) environment.box3("bar", "0", :ec2) environment.box3("foo-VAGRANTSLASH-bar", "1.0", :virtualbox) environment.box3("foo-VAGRANTCOLON-colon", "1.0", :virtualbox) expect(ui).to receive(:warn).once.and_call_original # Verify some output results = subject.all expect(results.length).to eq(4) expect(results).not_to include(["foo", "1.0", :virtualbox, nil]) expect(results).to include(["foo", "1.0", :vmware, nil]) expect(results).to include(["bar", "0", :ec2, nil]) expect(results).to include(["foo/bar", "1.0", :virtualbox, nil]) expect(results).to include(["foo:colon", "1.0", :virtualbox, nil]) end it 'does not raise an exception when a file appears in the boxes dir' do Tempfile.open('vagrant-a_file', environment.boxes_dir) do expect { subject.all }.to_not raise_error end end context "with multiple versions of the same box" do before do environment.box3("foo", "1.0", :virtualbox) environment.box3("foo", "2.0.3", :virtualbox) environment.box3("foo", "2.0.4", :virtualbox) environment.box3("foo", "10.3", :virtualbox) environment.box3("foo", "1.0", :vmware) environment.box3("foo", "0.4.3", :vmware) environment.box3("foo", "2.0.1", :vmware) environment.box3("foo", "2.0.2.dev", :vmware) environment.box3("foo", "2.0.2", :vmware) environment.box3("bar", "20161203.2", :ec2) environment.box3("bar", "20161203.2.3", :ec2) environment.box3("bar", "20151102.0.0", :ec2) environment.box3("foo-VAGRANTSLASH-bar", "1.0", :virtualbox) environment.box3("foo-VAGRANTCOLON-colon", "1.0", :virtualbox) end it "should sort boxes by name" do result = subject.all.map(&:first).uniq expect(result).to eq(["bar", "foo", "foo/bar", "foo:colon"]) end it "should group boxes by provider" do expect do current = "" seen_pairs = {} subject.all.each do |box_info| box_key = "#{box_info[0]}_#{box_info[2]}" if current != box_key if seen_pairs[box_key] raise KeyError.new("Box/provider pair already seen. Invalid sort!") else current = box_key seen_pairs[box_key] = true end end end end.not_to raise_error end context "with architectures defined" do before do environment.box3("foo-VAGRANTSLASH-bar", "1.0", :virtualbox, architecture: :arm64) end it "should sort boxes by name" do result = subject.all.map(&:first).uniq expect(result).to eq(["bar", "foo", "foo/bar", "foo:colon"]) end end it "should sort boxes by version" do box_list = subject.all.find_all do |box_info| box_info[0] == "foo" && box_info[2].to_s == "virtualbox" end result = box_list.map{|box_info| box_info[1]} expect(result).to eq([ "1.0", "2.0.3", "2.0.4", "10.3" ]) end it "should sort boxes with pre-release versions" do box_list = subject.all.find_all do |box_info| box_info[0] == "foo" && box_info[2].to_s == "vmware" end result = box_list.map{|box_info| box_info[1]} expect(result).to eq([ "0.4.3", "1.0", "2.0.1", "2.0.2.dev", "2.0.2" ]) end end end describe "#clean" do it "removes the directory if no other versions of the box exists" do # Create a few boxes, immediately destroy them environment.box3("foo", "1.0", :virtualbox) environment.box3("foo", "1.0", :vmware) # Delete them all subject.all.each do |parts| subject.find(parts[0], parts[2], ">= 0").destroy! end # Cleanup subject.clean("foo") # Make sure the whole directory is empty expect(environment.boxes_dir.children).to be_empty end it "doesn't remove the directory if a provider exists" do # Create a few boxes, immediately destroy them environment.box3("foo", "1.0", :virtualbox) environment.box3("foo", "1.0", :vmware) # Delete them all subject.find("foo", :virtualbox, ">= 0").destroy! # Cleanup subject.clean("foo") # Make sure the whole directory is not empty expect(environment.boxes_dir.children).to_not be_empty # Make sure the results still exist results = subject.all expect(results.length).to eq(1) expect(results).to include(["foo", "1.0", :vmware, nil]) end it "doesn't remove the directory if a version exists" do # Create a few boxes, immediately destroy them environment.box3("foo", "1.0", :virtualbox) environment.box3("foo", "1.2", :virtualbox) # Delete them all subject.find("foo", :virtualbox, ">= 1.1").destroy! # Cleanup subject.clean("foo") # Make sure the whole directory is not empty expect(environment.boxes_dir.children).to_not be_empty # Make sure the results still exist results = subject.all expect(results.length).to eq(1) expect(results).to include(["foo", "1.0", :virtualbox, nil]) end end describe "#find" do it "fails with custom error on invalid version" do expect { subject.find("foo", :i_dont_exist, "v1.2.2") }. to raise_error(Vagrant::Errors::BoxVersionInvalid) end it "returns nil if the box does not exist" do expect(subject.find("foo", :i_dont_exist, ">= 0")).to be_nil end it "returns a box if the box does exist" do # Create the "box" environment.box3("foo", "0", :virtualbox) # Actual test result = subject.find("foo", :virtualbox, ">= 0") expect(result).to_not be_nil expect(result).to be_kind_of(box_class) expect(result.name).to eq("foo") expect(result.metadata_url).to be_nil end it "returns a box if the box does exist, with no constraints" do # Create the "box" environment.box3("foo", "0", :virtualbox) # Actual test result = subject.find("foo", :virtualbox, nil) expect(result).to_not be_nil expect(result).to be_kind_of(box_class) expect(result.name).to eq("foo") expect(result.metadata_url).to be_nil end it "sets a metadata URL if it has one" do # Create the "box" environment.box3("foo", "0", :virtualbox, metadata_url: "foourl") # Actual test result = subject.find("foo", :virtualbox, ">= 0") expect(result).to_not be_nil expect(result).to be_kind_of(box_class) expect(result.name).to eq("foo") expect(result.metadata_url).to eq("foourl") end it "sets the metadata URL to an authenticated URL if it has one" do hook = double("hook") subject = described_class.new(environment.boxes_dir, hook: hook) # Create the "box" environment.box3("foo", "0", :virtualbox, metadata_url: "foourl") expect(hook).to receive(:call).with(any_args) { |name, env| expect(name).to eq(:authenticate_box_url) expect(env[:box_urls]).to eq(["foourl"]) true }.and_return(box_urls: ["bar"]) # Actual test result = subject.find("foo", :virtualbox, ">= 0") expect(result).to_not be_nil expect(result).to be_kind_of(box_class) expect(result.name).to eq("foo") expect(result.metadata_url).to eq("bar") end it "returns latest version matching constraint" do # Create the "box" environment.box3("foo", "1.0", :virtualbox) environment.box3("foo", "1.5", :virtualbox) # Actual test result = subject.find("foo", :virtualbox, ">= 0") expect(result).to_not be_nil expect(result).to be_kind_of(box_class) expect(result.name).to eq("foo") expect(result.version).to eq("1.5") end it "can satisfy complex constraints" do # Create the "box" environment.box3("foo", "0.1", :virtualbox) environment.box3("foo", "1.0", :virtualbox) environment.box3("foo", "2.1", :virtualbox) # Actual test result = subject.find("foo", :virtualbox, ">= 0.9, < 1.5") expect(result).to_not be_nil expect(result).to be_kind_of(box_class) expect(result.name).to eq("foo") expect(result.version).to eq("1.0") end it "handles prerelease versions" do # Create the "box" environment.box3("foo", "0.1.0-alpha.1", :virtualbox) environment.box3("foo", "0.1.0-alpha.2", :virtualbox) # Actual test result = subject.find("foo", :virtualbox, ">= 0") expect(result).to_not be_nil expect(result).to be_kind_of(box_class) expect(result.name).to eq("foo") expect(result.version).to eq("0.1.0-alpha.2") end it "returns nil if a box's constraints can't be satisfied" do # Create the "box" environment.box3("foo", "0.1", :virtualbox) environment.box3("foo", "1.0", :virtualbox) environment.box3("foo", "2.1", :virtualbox) # Actual test result = subject.find("foo", :virtualbox, "> 1.0, < 1.5") expect(result).to be_nil end context "with multiple versions of the same box" do before do environment.box3("foo", "1.0", :virtualbox) environment.box3("foo", "2.0.3", :virtualbox) environment.box3("foo", "2.0.4", :virtualbox) environment.box3("foo", "10.3", :virtualbox) environment.box3("foo", "1.0", :vmware) environment.box3("foo", "0.4.3", :vmware) environment.box3("foo", "2.0.1", :vmware) environment.box3("foo", "2.0.2.dev", :vmware) environment.box3("foo", "2.0.2", :vmware) environment.box3("bar", "20161203.2", :ec2) environment.box3("bar", "20161203.2.3", :ec2) environment.box3("bar", "20151102.0.0", :ec2) environment.box3("foo-VAGRANTSLASH-bar", "1.0", :virtualbox) environment.box3("foo-VAGRANTCOLON-colon", "1.0", :virtualbox) end it "should return expected latest version" do result = subject.find("foo", :virtualbox, "> 2, < 3") expect(result.version).to eq("2.0.4") end it "should sort boxes with pre-release versions" do result = subject.find("foo", :vmware, "> 2, < 3") expect(result.version).to eq("2.0.2") end end end describe "#add" do it "should add a valid box to the system" do box_path = environment.box2_file(:virtualbox) # Add the box box = subject.add(box_path, "foo", "1.0", providers: :virtualbox) expect(box).to be_kind_of(box_class) expect(box.name).to eq("foo") expect(box.provider).to eq(:virtualbox) # Verify we can find it as well expect(subject.find("foo", :virtualbox, "1.0")).to_not be_nil end it "should add a box with a name with '/' in it" do box_path = environment.box2_file(:virtualbox) # Add the box box = subject.add(box_path, "foo/bar", "1.0") expect(box).to be_kind_of(box_class) expect(box.name).to eq("foo/bar") expect(box.provider).to eq(:virtualbox) # Verify we can find it as well expect(subject.find("foo/bar", :virtualbox, "1.0")).to_not be_nil end it "should add a box without specifying a provider" do box_path = environment.box2_file(:vmware) # Add the box box = subject.add(box_path, "foo", "1.0") expect(box).to be_kind_of(box_class) expect(box.name).to eq("foo") expect(box.provider).to eq(:vmware) end it "should store a metadata URL" do box_path = environment.box2_file(:virtualbox) subject.add( box_path, "foo", "1.0", metadata_url: "bar") box = subject.find("foo", :virtualbox, "1.0") expect(box.metadata_url).to eq("bar") end it "should add a V1 box" do # Create a V1 box. box_path = environment.box1_file # Add the box box = subject.add(box_path, "foo", "1.0") expect(box).to be_kind_of(box_class) expect(box.name).to eq("foo") expect(box.provider).to eq(:virtualbox) end it "should raise an exception if the box already exists" do prev_box_name = "foo" prev_box_provider = :virtualbox prev_box_version = "1.0" # Create the box we're adding environment.box3(prev_box_name, "1.0", prev_box_provider) # Attempt to add the box with the same name box_path = environment.box2_file(prev_box_provider) expect { subject.add(box_path, prev_box_name, prev_box_version, providers: prev_box_provider) }.to raise_error(Vagrant::Errors::BoxAlreadyExists) end it "should replace the box if force is specified" do prev_box_name = "foo" prev_box_provider = :vmware prev_box_version = "1.0" # Setup the environment with the box pre-added environment.box3(prev_box_name, prev_box_version, prev_box_provider) # Attempt to add the box with the same name box_path = environment.box2_file(prev_box_provider, metadata: { "replaced" => "yes" }) box = subject.add(box_path, prev_box_name, prev_box_version, force: true) expect(box.metadata["replaced"]).to eq("yes") end it "should raise an exception if the box already exists and no provider is given" do # Create some box file box_name = "foo" box_path = environment.box2_file(:vmware) # Add it once, successfully expect { subject.add(box_path, box_name, "1.0") }.to_not raise_error # Add it again, and fail! expect { subject.add(box_path, box_name, "1.0") }. to raise_error(Vagrant::Errors::BoxAlreadyExists) end it "should raise an exception and not add the box if the provider doesn't match" do box_name = "foo" good_provider = :virtualbox bad_provider = :vmware # Create a VirtualBox box file box_path = environment.box2_file(good_provider) # Add the box but with an invalid provider, verify we get the proper # error. expect { subject.add(box_path, box_name, "1.0", providers: bad_provider) }. to raise_error(Vagrant::Errors::BoxProviderDoesntMatch) # Verify the box doesn't exist expect(subject.find(box_name, bad_provider, "1.0")).to be_nil end it "should raise an exception if you add an invalid box file" do # Tar Header information CHECKSUM_OFFSET = 148 CHECKSUM_LENGTH = 8 Tempfile.open(['vagrant-testing', '.tar']) do |f| f.binmode # Corrupt the tar by writing over the checksum field f.seek(CHECKSUM_OFFSET) f.write("\0"*CHECKSUM_LENGTH) f.close expect { subject.add(f.path, "foo", "1.0") }. to raise_error(Vagrant::Errors::BoxUnpackageFailure) end end end describe "#upgrade_v1_1_v1_5" do let(:boxes_dir) { environment.boxes_dir } before do # Create all the various box directories @foo_path = environment.box2("foo", "virtualbox") @vbox_path = environment.box2("precise64", "virtualbox") @vmware_path = environment.box2("precise64", "vmware") @v1_path = environment.box("v1box") end it "upgrades the boxes" do subject.upgrade_v1_1_v1_5 # The old paths should not exist anymore expect(@foo_path).to_not exist expect(@vbox_path).to_not exist expect(@vmware_path).to_not exist expect(@v1_path.join("box.ovf")).to_not exist # New paths should exist foo_path = boxes_dir.join("foo", "0", "virtualbox") vbox_path = boxes_dir.join("precise64", "0", "virtualbox") vmware_path = boxes_dir.join("precise64", "0", "vmware") v1_path = boxes_dir.join("v1box", "0", "virtualbox") expect(foo_path).to exist expect(vbox_path).to exist expect(vmware_path).to exist expect(v1_path).to exist end end end ================================================ FILE: test/unit/vagrant/box_metadata_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) require "vagrant/box_metadata" describe Vagrant::BoxMetadata do include_context "unit" let(:raw) do { name: "foo", description: "bar", versions: [ { version: "1.0.0", providers: [ { name: "virtualbox" }, { name: "vmware" } ], }, { version: "1.1.5", providers: [ { name: "virtualbox" } ] }, { version: "1.1.0", providers: [ { name: "virtualbox" }, { name: "vmware", architecture: "test-arch" } ] } ] }.to_json end subject { described_class.new(raw) } describe '#name' do subject { super().name } it { should eq("foo") } end describe '#description' do subject { super().description } it { should eq("bar") } end context "with poorly formatted JSON" do let(:raw) { {name: "foo"}.to_json + "," } it "raises an exception" do expect { subject }. to raise_error(Vagrant::Errors::BoxMetadataMalformed) end end context "with poorly formatted version" do let(:raw) { { name: "foo", versions: [ { version: "I AM NOT VALID" } ] }.to_json } it "raises an exception" do expect { subject }. to raise_error(Vagrant::Errors::BoxMetadataMalformedVersion) end end describe "#version" do it "matches an exact version" do result = subject.version("1.0.0") expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.0.0") end it "matches a constraint with latest matching version" do result = subject.version(">= 1.0") expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.1.5") end it "matches complex constraints" do result = subject.version(">= 0.9, ~> 1.0.0") expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.0.0") end it "matches the constraint that has the given provider" do result = subject.version(">= 0", provider: :vmware) expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.1.0") end end describe "#versions" do it "returns the versions it contained" do expect(subject.versions).to eq( ["1.0.0", "1.1.0", "1.1.5"]) end it "filters versions by matching provider" do expect(subject.versions(provider: :vmware)).to eq( ["1.0.0", "1.1.0"]) end it "filters versions by architecture" do expect(subject.versions(architecture: "test-arch")).to eq(["1.1.0"]) end it "filters versions by provider and architecture" do expect(subject.versions(architecture: "test-arch", provider: "virtualbox")).to eq([]) expect(subject.versions(architecture: "test-arch", provider: "vmware")).to eq(["1.1.0"]) end it "filters versions by multiple providers" do expect(subject.versions(provider: ["vmware", "my-virt"])).to eq(["1.0.0", "1.1.0"]) end end describe "#compatible_version_update?" do let(:raw) do { name: "foo", description: "bar", versions: [ { version: "1.0.0", providers: [ { name: "virtualbox" }, { name: "vmware" } ], }, { version: "1.1.5", providers: [ { name: "virtualbox" } ] }, { version: "1.1.0", providers: [ { name: "virtualbox" }, { name: "vmware" } ] } ] }.to_json end it "is compatible if current version is older than new version" do expect(subject.compatible_version_update?("1.0.0", "1.1.0", provider: "virtualbox")).to be true expect(subject.compatible_version_update?("1.1.5", "1.1.0", provider: "virtualbox")).to be false end it "is compatible if architecture is set and isn't defined in metadata" do expect(subject.compatible_version_update?("1.0.0", "1.1.0", provider: "virtualbox", architecture: :auto)).to be true end end context "with architecture" do let(:raw) do { name: "foo", description: "bar", versions: [ { version: "1.0.0", providers: [ { name: "virtualbox", default_architecture: true, architecture: "amd64" }, { name: "virtualbox", default_architecture: false, architecture: "arm64" }, { name: "vmware", default_architecture: true, architecture: "arm64" }, { name: "vmware", default_architecture: false, architecture: "amd64" } ], }, { version: "1.1.5", providers: [ { name: "virtualbox", architecture: "amd64", default_architecture: true, } ] }, { version: "1.1.6", providers: [ { name: "virtualbox", architecture: "arm64", default_architecture: true, }, ] }, { version: "1.1.0", providers: [ { name: "virtualbox", architecture: "amd64", default_architecture: true, }, { name: "vmware", architecture: "amd64", default_architecture: true, } ] }, { version: "2.0.0", providers: [ { name: "vmware", architecture: "arm64", default_architecture: true, } ] } ] }.to_json end subject { described_class.new(raw) } before { allow(Vagrant::Util::Platform).to receive(:architecture).and_return("amd64") } describe "#version" do it "matches an exact version" do result = subject.version("1.0.0") expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.0.0") end it "matches a constraint with latest matching version" do result = subject.version(">= 1.0") expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.1.5") end it "matches complex constraints" do result = subject.version(">= 0.9, ~> 1.0.0") expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.0.0") end context "with provider filter" do it "matches the constraint that has the given provider" do result = subject.version(">= 0", provider: :vmware) expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.1.0") end it "matches the exact version that has the given provider" do result = subject.version("1.0.0", provider: :virtualbox) expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.0.0") end it "does not match exact version that has given provider but not host architecture" do result = subject.version("1.1.6", provider: :virtualbox) expect(result).to be_nil end context "with architecture filter" do it "matches the exact version that has provider with host architecture when using :auto" do result = subject.version("1.0.0", provider: :virtualbox, architecture: :auto) expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.0.0") end it "matches the exact version that has provider with defined host architecture" do result = subject.version("1.0.0", provider: :virtualbox, architecture: "arm64") expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.0.0") end it "does not match the exact version that has provider with defined host architecture" do result = subject.version("1.0.0", provider: :virtualbox, architecture: "ppc64") expect(result).to be_nil end end end context "with architecture filter" do it "matches a constraint that has the detected host architecture" do result = subject.version("> 0", architecture: :auto) expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.1.5") end it "matches a constraint that has the provided architecture" do result = subject.version("> 0", architecture: "arm64") expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("2.0.0") end it "matches exact version that has the provided architecture" do result = subject.version("1.0.0", architecture: "arm64") expect(result).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result.version).to eq("1.0.0") end it "does not match exact version that does not have provided architecture" do result = subject.version("2.0.0", architecture: "amd64") expect(result).to be_nil end end end describe "#versions" do it "returns the versions it contained" do expect(subject.versions). to eq(["1.0.0", "1.1.0", "1.1.5", "1.1.6", "2.0.0"]) end context "with provider filter" do it "filters versions" do expect(subject.versions(provider: :vmware)). to eq(["1.0.0", "1.1.0", "2.0.0"]) end end context "with architecture filter" do it "filters versions" do expect(subject.versions(architecture: "arm64")). to eq(["1.0.0", "1.1.6", "2.0.0"]) end it "returns none when no matching architecture available" do expect(subject.versions(architecture: "other")). to be_empty end it "filters based on host architecture when :auto used" do expect(subject.versions(architecture: :auto)). to eq(subject.versions(architecture: "amd64")) end end end describe "#compatible_version_update?" do let(:raw) do { name: "foo", description: "bar", versions: [ { version: "1.0.0", providers: [ { name: "vmware", default_architecture: true, architecture: "arm64" }, { name: "docker", default_architecture: true, architecture: "unknown" }, { name: "virtualbox", default_architecture: true, architecture: "unknown" }, { name: "other", default_architecture: true, architecture: "amd64" } ] }, { version: "2.0.0", providers: [ { name: "vmware", architecture: "arm64", default_architecture: true, }, { name: "docker", default_architecture: true, architecture: "unknown" }, { name: "virtualbox", default_architecture: true, architecture: "amd64" }, { name: "other", default_architecture: true, architecture: "unknown" }, { name: "missing", default_architecture: true, architecture: "unknown" } ] } ] }.to_json end it "is compatible if architectures match" do expect(subject.compatible_version_update?("1.0.0", "2.0.0", provider: "vmware", architecture: "arm64")).to be true end it "is compatible if current arch is unknown, but newer arch matches system" do expect(subject.compatible_version_update?("1.0.0", "2.0.0", provider: "virtualbox", architecture: :auto)).to be true end it "is compatible if current architecture is unknown" do expect(subject.compatible_version_update?("1.0.0", "2.0.0", provider: "docker", architecture: :auto)).to be true end it "is compatible if current_version is not available from metadata" do expect(subject.compatible_version_update?("1.0.0", "2.0.0", provider: "missing", architecture: :auto)).to be true end it "is not compatible if current architecture is defined, but newer architecture is unknown" do expect(subject.compatible_version_update?("1.0.0", "2.0.0", provider: "other", architecture: :auto)).to be false end it "is compatible if current architecture is defined, but newer architecture is unknown, and architecture is set to nil" do expect(subject.compatible_version_update?("1.0.0", "2.0.0", provider: "other", architecture: nil)).to be true end end end end describe Vagrant::BoxMetadata::Version do let(:raw) { {} } subject { described_class.new(raw) } before do raw["providers"] = [ { "name" => "virtualbox", }, { "name" => "vmware", } ] end describe "#version" do it "is the version in the raw data" do v = "1.0" raw["version"] = v expect(subject.version).to eq(v) end end describe "#provider" do it "returns nil if a provider isn't supported" do expect(subject.provider("foo")).to be_nil end it "returns the provider specified" do result = subject.provider("virtualbox") expect(result).to_not be_nil expect(result).to be_kind_of(Vagrant::BoxMetadata::Provider) end end describe "#providers" do it "returns the providers available" do expect(subject.providers.sort).to eq( [:virtualbox, :vmware]) end end end describe Vagrant::BoxMetadata::Provider do let(:raw) { {} } subject { described_class.new(raw) } describe "#name" do it "is the name specified" do raw["name"] = "foo" expect(subject.name).to eq("foo") end end describe "#url" do it "is the URL specified" do raw["url"] = "bar" expect(subject.url).to eq("bar") end end describe "#checksum and #checksum_type" do it "is set properly" do raw["checksum"] = "foo" raw["checksum_type"] = "bar" expect(subject.checksum).to eq("foo") expect(subject.checksum_type).to eq("bar") end it "is nil if not set" do expect(subject.checksum).to be_nil expect(subject.checksum_type).to be_nil end end describe "architecture" do it "is set properly" do raw["architecture"] = "test-arch" expect(subject.architecture).to eq("test-arch") end it "is nil if not set" do expect(subject.architecture).to be_nil end end describe "#architecture_support?" do it "is false if architecture is not supported" do expect(subject.architecture_support?).to be(false) end it "is true if architecture is supported" do raw["default_architecture"] = false expect(subject.architecture_support?).to be(true) end end end ================================================ FILE: test/unit/vagrant/box_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) require "pathname" require "stringio" require "tempfile" require "vagrant/box_metadata" describe Vagrant::Box, :skip_windows do include_context "unit" let(:environment) { isolated_environment } let(:box_collection) { Vagrant::BoxCollection.new(environment.boxes_dir) } let(:name) { "foo" } let(:provider) { :virtualbox } let(:version) { "1.0" } let(:architecture) { "test-architecture" } let(:directory) { environment.box3("foo", "1.0", :virtualbox) } subject { described_class.new(name, provider, version, directory) } describe '#metadata_url' do subject { super().metadata_url } it { should be_nil } end it "provides the name" do expect(subject.name).to eq(name) end it "provides the provider" do expect(subject.provider).to eq(provider) end it "provides the directory" do expect(subject.directory).to eq(directory) end it "provides the metadata associated with a box" do data = { "foo" => "bar" } # Write the metadata directory.join("metadata.json").open("w") do |f| f.write(JSON.generate(data)) end # Verify the metadata expect { subject.metadata }. to raise_error(Vagrant::Errors::BoxMetadataMissingRequiredFields) end it "provides the metadata associated with a box" do data = { "provider" => "bar" } # Write the metadata directory.join("metadata.json").open("w") do |f| f.write(JSON.generate(data)) end # Verify the metadata expect(subject.metadata).to eq(data) end context "with a metadata URL" do subject do described_class.new( name, provider, version, directory, metadata_url: "foo") end describe '#metadata_url' do subject { super().metadata_url } it { should eq("foo") } end end context "with a corrupt metadata file" do before do directory.join("metadata.json").open("w") do |f| f.write("") end end it "should raise an exception" do expect { subject }. to raise_error(Vagrant::Errors::BoxMetadataCorrupted) end end context "without a metadata file" do before :each do directory.join("metadata.json").delete end it "should raise an exception" do expect { subject }. to raise_error(Vagrant::Errors::BoxMetadataFileNotFound) end end context "#has_update?" do subject do described_class.new( name, provider, version, directory, metadata_url: "foo") end it "raises an exception if no metadata_url is set" do subject = described_class.new( name, provider, version, directory) expect { subject.has_update?("> 0") }. to raise_error(Vagrant::Errors::BoxUpdateNoMetadata) end it "returns nil if there is no update" do metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) { "name": "foo", "versions": [ { "version": "1.0" } ] } RAW allow(subject).to receive(:load_metadata).and_return(metadata) expect(subject.has_update?).to be_nil end it "returns the updated box info if there is an update available" do metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) { "name": "foo", "versions": [ { "version": "1.0" }, { "version": "1.1", "providers": [ { "name": "virtualbox", "url": "bar" } ] } ] } RAW allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update? expect(result).to_not be_nil expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) expect(result[0].name).to eq("foo") expect(result[1].version).to eq("1.1") expect(result[2].url).to eq("bar") end it "returns the updated box info within constraints" do metadata = Vagrant::BoxMetadata.new(StringIO.new(<<-RAW)) { "name": "foo", "versions": [ { "version": "1.0" }, { "version": "1.1", "providers": [ { "name": "virtualbox", "url": "bar" } ] }, { "version": "1.4", "providers": [ { "name": "virtualbox", "url": "bar" } ] } ] } RAW allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update?(">= 1.1, < 1.4") expect(result).to_not be_nil expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) expect(result[0].name).to eq("foo") expect(result[1].version).to eq("1.1") expect(result[2].url).to eq("bar") end context "with architecture" do subject do described_class.new( name, provider, version, directory, architecture: architecture, metadata_url: "foo" ) end it "raises an exception if no metadata_url is set" do subject = described_class.new( name, provider, version, directory, architecture: architecture, ) expect { subject.has_update?("> 0") }. to raise_error(Vagrant::Errors::BoxUpdateNoMetadata) end it "returns nil if there is no update" do metadata = Vagrant::BoxMetadata.new( { name: "foo", versions: [ { version: "1.0" } ] }.to_json ) allow(subject).to receive(:load_metadata).and_return(metadata) expect(subject.has_update?).to be_nil end it "returns the updated box info if there is an update available" do metadata = Vagrant::BoxMetadata.new( { name: "foo", versions: [ {version: "1.0"}, { version: "1.1", providers: [ { name: "virtualbox", url: "bar", architecture: architecture, } ] } ] }.to_json ) allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update? expect(result).to_not be_nil expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) expect(result[0].name).to eq("foo") expect(result[1].version).to eq("1.1") expect(result[2].url).to eq("bar") end it "returns nil if update does not support architecture" do metadata = Vagrant::BoxMetadata.new( { name: "foo", versions: [ {version: "1.0"}, { version: "1.1", providers: [ { name: "virtualbox", url: "bar", architecture: "other-architecture", } ] } ] }.to_json ) allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update? expect(result).to be_nil end it "returns the updated box info within constraints" do metadata = Vagrant::BoxMetadata.new( { name: "foo", versions: [ { version: "1.0", }, { version: "1.1", providers: [ { name: "virtualbox", url: "bar", architecture: architecture }, ] }, { version: "1.2", providers: [ { name: "virtualbox", url: "bar", architecture: "other-architecture", }, ] }, { version: "1.4", providers: [ { name: "virtualbox", url: "bar", architecture: architecture } ] } ] }.to_json ) allow(subject).to receive(:load_metadata).and_return(metadata) result = subject.has_update?(">= 1.1, < 1.4") expect(result).to_not be_nil expect(result[0]).to be_kind_of(Vagrant::BoxMetadata) expect(result[1]).to be_kind_of(Vagrant::BoxMetadata::Version) expect(result[2]).to be_kind_of(Vagrant::BoxMetadata::Provider) expect(result[0].name).to eq("foo") expect(result[1].version).to eq("1.1") expect(result[2].url).to eq("bar") end end end context "#automatic_update_check_allowed?" do it "should return true on intial check" do expect(subject.automatic_update_check_allowed?).to be_truthy end it "should return false on second check" do expect(subject.automatic_update_check_allowed?).to be_truthy expect(subject.automatic_update_check_allowed?).to be_falsey end it "should use a file to mark last check time" do expect(FileUtils).to receive(:touch) subject.automatic_update_check_allowed? end it "should return true when time since last check is greater than check interval" do subject.automatic_update_check_allowed? stub_const("Vagrant::Box::BOX_UPDATE_CHECK_INTERVAL", -1) expect(subject.automatic_update_check_allowed?).to be_truthy end end context "#in_use?" do let(:index) { [] } def new_entry(name, provider, version, architecture=nil) Vagrant::MachineIndex::Entry.new.tap do |entry| entry.extra_data["box"] = { "name" => name, "provider" => provider, "version" => version, "architecture" => architecture, } end end it "returns nil if the index has no matching entries" do index << new_entry("foo", "bar", "1.0") index << new_entry("foo", "baz", "1.2") expect(subject).to_not be_in_use(index) end it "returns matching entries if they exist" do matching = new_entry(name, provider.to_s, version) index << new_entry("foo", "bar", "1.0") index << matching index << new_entry("foo", "baz", "1.2") expect(subject.in_use?(index)).to eq([matching]) end context "with architecture information" do subject { described_class.new(name, provider, version, directory, architecture: architecture) } it "returns nil if the index has no matching entries" do index << new_entry("foo", "bar", "1.0", "amd64") index << new_entry("foo", "baz", "1.2", "arm64") index << new_entry(name, provider.to_s, version, "random-arch") expect(subject).to_not be_in_use(index) end it "returns matching entries if they exist" do matching = new_entry(name, provider.to_s, version, architecture) index << new_entry("foo", "bar", "1.0", "amd64") index << matching index << new_entry("foo", "baz", "1.2") expect(subject.in_use?(index)).to eq([matching]) end end end context "#load_metadata" do let(:metadata_url) do Tempfile.new("vagrant-test-box-test").tap do |f| f.write(<<-RAW) { "name": "foo", "description": "bar" } RAW f.close end end subject do described_class.new( name, provider, version, directory, metadata_url: metadata_url.path) end after do metadata_url.unlink end it "loads the url and returns the data" do result = subject.load_metadata expect(result.name).to eq("foo") expect(result.description).to eq("bar") end it "raises an error if the download failed" do dl = double("downloader") allow(Vagrant::Util::Downloader).to receive(:new).and_return(dl) expect(dl).to receive(:download!).and_raise( Vagrant::Errors::DownloaderError.new(message: "foo")) expect { subject.load_metadata }. to raise_error(Vagrant::Errors::BoxMetadataDownloadError) end context "box has a hook for adding authentication" do let(:hook){ double("hook") } subject do described_class.new( name, provider, version, directory, metadata_url: metadata_url.path, hook: hook) end it "add authentication headers to the url" do expect(hook).to receive(:call).with(:authenticate_box_downloader, any_args) result = subject.load_metadata expect(result.name).to eq("foo") expect(result.description).to eq("bar") end end end describe "destroying" do it "should destroy an existing box" do # Verify that our "box" exists expect(directory.exist?).to be # Destroy it expect(subject.destroy!).to be # Verify that it is "destroyed" expect(directory.exist?).not_to be end it "should not error destroying a non-existent box" do # Get the subject so that it is instantiated box = subject # Delete the directory directory.rmtree # Destroy it expect(box.destroy!).to be end end describe "repackaging" do let(:scratch) { Dir.mktmpdir("vagrant-test-box-repackaging") } let(:box_output_path) { File.join(scratch, "package.box") } after do FileUtils.rm_rf(scratch) end it "should repackage the box", :bsdtar do test_file_contents = "hello, world!" # Put a file in the box directory to verify it is packaged properly # later. directory.join("test_file").open("w") do |f| f.write(test_file_contents) end # Repackage our box to some temporary directory expect(subject.repackage(box_output_path)).to be(true) # Let's now add this box again under a different name, and then # verify that we get the proper result back. new_box = box_collection.add(box_output_path, "foo2", "1.0") expect(new_box.directory.join("test_file").read).to eq(test_file_contents) end end describe "comparison and ordering" do it "should be equal if the name, provider, version match" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("a", :foo, "1.0", directory) expect(a).to eq(b) end it "should not be equal if name doesn't match" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("b", :foo, "1.0", directory) expect(a).to_not eq(b) end it "should not be equal if provider doesn't match" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("a", :bar, "1.0", directory) expect(a).to_not eq(b) end it "should not be equal if version doesn't match" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("a", :foo, "1.1", directory) expect(a).to_not eq(b) end it "should sort them in order of name, version, provider" do a = described_class.new("a", :foo, "1.0", directory) b = described_class.new("a", :foo2, "1.0", directory) c = described_class.new("a", :foo2, "1.1", directory) d = described_class.new("b", :foo2, "1.0", directory) expect([d, c, a, b].sort).to eq([a, b, c, d]) end end end ================================================ FILE: test/unit/vagrant/bundler_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "tmpdir" require_relative "../base" require "vagrant/bundler" describe Vagrant::Bundler::SolutionFile do let(:plugin_path) { Pathname.new(tmpdir) + "plugin_file" } let(:solution_path) { Pathname.new(tmpdir) + "solution_file" } let(:tmpdir) { @tmpdir ||= Dir.mktmpdir("vagrant-bundler-test") } let(:subject) { described_class.new( plugin_file: plugin_path, solution_file: solution_path ) } after do if @tmpdir FileUtils.rm_rf(@tmpdir) @tmpdir = nil end end describe "#initialize" do context "file paths" do context "with solution_file not provided" do let(:subject) { described_class.new(plugin_file: plugin_path) } it "should set the plugin_file" do expect(subject.plugin_file.to_s).to eq(plugin_path.to_s) end it "should set solution path to same directory" do expect(subject.solution_file.to_s).to eq(plugin_path.to_s + ".sol") end end context "with custom solution_file provided" do let(:subject) { described_class. new(plugin_file: plugin_path, solution_file: solution_path) } it "should set the plugin file path" do expect(subject.plugin_file.to_s).to eq(plugin_path.to_s) end it "should set the solution file path to given value" do expect(subject.solution_file.to_s).to eq(solution_path.to_s) end end end context "initialization behavior" do context "on creation" do before { expect_any_instance_of(described_class).to receive(:load) } it "should load solution file during initialization" do subject end end it "should be invalid by default" do expect(subject.valid?).to be_falsey end end end describe "#dependency_list=" do it "should accept a list of Gem::Dependency instances" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } subject.dependency_list = list expect(subject.dependency_list.map(&:dependency)).to eq(list) end it "should error if list includes instance not Gem::Dependency" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } << :invalid expect{ subject.dependency_list = list }.to raise_error(TypeError) end it "should convert list into resolver dependency request" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } subject.dependency_list = list subject.dependency_list.each do |dep| expect(dep).to be_a(Gem::Resolver::DependencyRequest) end end it "should freeze the new dependency list" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } subject.dependency_list = list expect(subject.dependency_list).to be_frozen end end describe "#delete!" do context "when file does not exist" do before { subject.solution_file.delete if subject.solution_file.exist? } it "should return false" do expect(subject.delete!).to be_falsey end it "should not exist" do subject.delete! expect(subject.solution_file.exist?).to be_falsey end end context "when file does exist" do before { subject.solution_file.write('x') } it "should return true" do expect(subject.delete!).to be_truthy end it "should not exist" do expect(subject.solution_file.exist?).to be_truthy subject.delete! expect(subject.solution_file.exist?).to be_falsey end end end describe "store!" do context "when plugin file does not exist" do before { subject.plugin_file.delete if subject.plugin_file.exist? } it "should return false" do expect(subject.store!).to be_falsey end it "should not create a solution file" do subject.store! expect(subject.solution_file.exist?).to be_falsey end end context "when plugin file does exist" do before { subject.plugin_file.write("x") } it "should return true" do expect(subject.store!).to be_truthy end it "should create a solution file" do expect(subject.solution_file.exist?).to be_falsey subject.store! expect(subject.solution_file.exist?).to be_truthy end context "stored file" do let(:content) { @content ||= JSON.load(subject.solution_file.read) } before { subject.store! } after { @content = nil } it "should store JSON hash" do expect(content).to be_a(Hash) end it "should include dependencies key as array value" do expect(content["dependencies"]).to be_a(Array) end it "should include checksum key as string value" do expect(content["checksum"]).to be_a(String) end it "should include vagrant_version key as string value" do expect(content["vagrant_version"]).to be_a(String) end it "should include vagrant_version key that matches current version" do expect(content["vagrant_version"]).to eq(Vagrant::VERSION) end end end end describe "behavior" do context "when storing new solution set" do let(:deps) { ["dep1", "dep2"].map{ |n| Gem::Dependency.new(n) } } context "when plugin file does not exist" do before { subject.solution_file.delete if subject.solution_file.exist? } it "should not create a solution file" do subject.dependency_list = deps subject.store! expect(subject.solution_file.exist?).to be_falsey end end context "when plugin file does exist" do before { subject.plugin_file.write("x") } it "should create a solution file" do subject.dependency_list = deps subject.store! expect(subject.solution_file.exist?).to be_truthy end it "should update solution file instance to valid" do expect(subject.valid?).to be_falsey subject.dependency_list = deps subject.store! expect(subject.valid?).to be_truthy end context "when solution file does exist" do before do subject.dependency_list = deps subject.store! end it "should be a valid solution" do subject = described_class.new( plugin_file: plugin_path, solution_file: solution_path ) expect(subject.valid?).to be_truthy end it "should have expected dependency list" do subject = described_class.new( plugin_file: plugin_path, solution_file: solution_path ) expect(subject.dependency_list).to eq(deps) end context "when plugin file has been changed" do before { subject.plugin_file.write("xy") } it "should not be a valid solution" do subject = described_class.new( plugin_file: plugin_path, solution_file: solution_path ) expect(subject.valid?).to be_falsey end it "should have empty dependency list" do subject = described_class.new( plugin_file: plugin_path, solution_file: solution_path ) expect(subject.dependency_list).to be_empty end end end end end end describe "#load" do let(:plugin_file_exists) { false } let(:solution_file_exists) { false } let(:plugin_file_path) { "PLUGIN_FILE_PATH" } let(:solution_file_path) { "SOLUTION_FILE_PATH" } let(:plugin_file) { double("plugin-file") } let(:solution_file) { double("solution-file") } subject do described_class.new(plugin_file: plugin_file_path, solution_file: solution_file_path) end before do allow(Pathname).to receive(:new).with(plugin_file_path).and_return(plugin_file) allow(Pathname).to receive(:new).with(solution_file_path).and_return(solution_file) allow(plugin_file).to receive(:exist?).and_return(plugin_file_exists) allow(solution_file).to receive(:exist?).and_return(solution_file_exists) end context "when plugin file and solution file do not exist" do it "should not attempt to read the solution" do expect_any_instance_of(described_class).not_to receive(:read_solution) subject end end context "when plugin file exists and solution file does not" do let(:plugin_file_exists) { true } it "should not attempt to read the solution" do expect_any_instance_of(described_class).not_to receive(:read_solution) subject end end context "when solution file exists and plugin file does not" do let(:solution_file_exists) { true } it "should not attempt to read the solution" do expect_any_instance_of(described_class).not_to receive(:read_solution) subject end end context "when solution file and plugin file exist" do let(:plugin_file_exists) { true } let(:solution_file_exists) { true } let(:solution_file_contents) { "" } before do allow(solution_file).to receive(:read).and_return(solution_file_contents) allow_any_instance_of(described_class).to receive(:plugin_file_checksum).and_return("VALID") end context "when solution file is empty" do it "should return false" do expect(subject.send(:load)).to be_falsey end end context "when solution file contains invalid checksum" do let(:solution_file_contents) { {checksum: "INVALID", vagrant_version: Vagrant::VERSION}.to_json } it "should return false" do expect(subject.send(:load)).to be_falsey end end context "when solution file contains different Vagrant version" do let(:solution_file_contents) { {checksum: "VALID", vagrant_version: "0.1"}.to_json } it "should return false" do expect(subject.send(:load)).to be_falsey end end context "when solution file contains valid Vagrant version and valid checksum" do let(:solution_file_contents) { {checksum: "VALID", vagrant_version: Vagrant::VERSION, dependencies: file_dependencies}.to_json } let(:file_dependencies) { dependency_list.map{|d| [d.name, d.requirements_list]} } let(:dependency_list) { [] } it "should return true" do expect(subject.send(:load)).to be_truthy end it "should be valid" do expect(subject).to be_valid end context "when solution file contains dependency list" do let(:dependency_list) { [ Gem::Dependency.new("dep1", "> 0"), Gem::Dependency.new("dep2", "< 3") ] } it "should be valid" do expect(subject).to be_valid end it "should convert list into dependency requests" do subject.dependency_list.each do |d| expect(d).to be_a(Gem::Resolver::DependencyRequest) end end it "should include defined dependencies" do expect(subject.dependency_list.first).to eq(dependency_list.first) expect(subject.dependency_list.last).to eq(dependency_list.last) end it "should freeze the dependency list" do expect(subject.dependency_list).to be_frozen end end end end end describe "#read_solution" do let(:solution_file_contents) { "" } let(:plugin_file_path) { "PLUGIN_FILE_PATH" } let(:solution_file_path) { "SOLUTION_FILE_PATH" } let(:plugin_file) { double("plugin-file") } let(:solution_file) { double("solution-file") } subject do described_class.new(plugin_file: plugin_file_path, solution_file: solution_file_path) end before do allow(Pathname).to receive(:new).with(plugin_file_path).and_return(plugin_file) allow(Pathname).to receive(:new).with(solution_file_path).and_return(solution_file) allow(plugin_file).to receive(:exist?).and_return(false) allow(solution_file).to receive(:exist?).and_return(false) allow(solution_file).to receive(:read).and_return(solution_file_contents) end it "should return nil when file contents are empty" do expect(subject.send(:read_solution)).to be_nil end context "when file contents are hash" do let(:solution_file_contents) { {checksum: "VALID"}.to_json } it "should return a hash" do expect(subject.send(:read_solution)).to be_a(Hash) end it "should return a hash with indifferent access" do expect(subject.send(:read_solution)).to be_a(Vagrant::Util::HashWithIndifferentAccess) end end context "when file contents are array" do let(:solution_file_contents) { ["test"].to_json } it "should return a hash" do expect(subject.send(:read_solution)).to be_a(Hash) end it "should return a hash with indifferent access" do expect(subject.send(:read_solution)).to be_a(Vagrant::Util::HashWithIndifferentAccess) end end context "when file contents are null" do let(:solution_file_contents) { "null" } it "should return nil" do expect(subject.send(:read_solution)).to be_nil end end context "when file contents are invalid" do let(:solution_file_contents) { "{2dfwef" } it "should return nil" do expect(subject.send(:read_solution)).to be_nil end end end end describe Vagrant::Bundler do include_context "unit" let(:iso_env) { isolated_environment } let(:env) { iso_env.create_vagrant_env } let(:tmpdir) { @v_tmpdir ||= Pathname.new(Dir.mktmpdir("vagrant-bundler-test")) } before do @tmpdir = Dir.mktmpdir("vagrant-bundler-test") @vh = ENV["VAGRANT_HOME"] ENV["VAGRANT_HOME"] = @tmpdir end after do ENV["VAGRANT_HOME"] = @vh FileUtils.rm_rf(@tmpdir) FileUtils.rm_rf(@v_tmpdir) if @v_tmpdir end it "should isolate gem path based on Ruby version" do expect(subject.plugin_gem_path.to_s).to end_with(RUBY_VERSION) end it "should not have an env_plugin_gem_path by default" do expect(subject.env_plugin_gem_path).to be_nil end describe "#initialize" do it "should automatically set the plugin gem path" do expect(subject.plugin_gem_path).not_to be_nil end it "should add current ruby version to plugin gem path suffix" do expect(subject.plugin_gem_path.to_s).to end_with(RUBY_VERSION) end it "should freeze the plugin gem path" do expect(subject.plugin_gem_path).to be_frozen end end describe "#environment_path=" do it "should error if not given Pathname" do expect { subject.environment_path = :value }. to raise_error(TypeError) end context "when set with Pathname" do let(:env_path) { Pathname.new("/dev/null") } before { subject.environment_path = env_path } it "should set the environment_data_path" do expect(subject.environment_data_path).to eq(env_path) end it "should set the env_plugin_gem_path" do expect(subject.env_plugin_gem_path).not_to be_nil end it "should suffix current ruby version to env_plugin_gem_path" do expect(subject.env_plugin_gem_path.to_s).to end_with(RUBY_VERSION) end it "should base env_plugin_gem_path on environment_path value" do expect(subject.env_plugin_gem_path.to_s).to start_with(env_path.to_s) end it "should freeze the env_plugin_gem_path" do expect(subject.env_plugin_gem_path).to be_frozen end end end describe "#load_solution_file" do let(:local_opt) { nil } let(:global_opt) { nil } let(:options) { {local: local_opt, global: global_opt} } it "should return nil when local and global options are blank" do expect(subject.load_solution_file(options)).to be_nil end context "when environment data path is set" do let(:env_path) { "/dev/null" } before { subject.environment_path = Pathname.new(env_path) } context "when local option is set" do let(:local_opt) { tmpdir + "local" } it "should return a SolutionFile instance" do expect(subject.load_solution_file(options)).to be_a(Vagrant::Bundler::SolutionFile) end it "should be located in the environment data path" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to start_with(env_path) end it "should have a local.sol solution file" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to end_with("local.sol") end it "should have plugin file set to local value" do file = subject.load_solution_file(options) expect(file.plugin_file.to_s).to eq(local_opt.to_s) end end context "when global option is set" do let(:global_opt) { tmpdir + "global" } it "should return a SolutionFile instance" do expect(subject.load_solution_file(options)).to be_a(Vagrant::Bundler::SolutionFile) end it "should be located in the environment data path" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to start_with(env_path) end it "should have a global.sol solution file" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to end_with("global.sol") end it "should have plugin file set to global value" do file = subject.load_solution_file(options) expect(file.plugin_file.to_s).to eq(global_opt.to_s) end end context "when local and global option is set" do let(:global_opt) { tmpdir + "global" } let(:local_opt) { tmpdir + "local" } it "should return nil" do expect(subject.load_solution_file(options)).to be_nil end end end context "when environment data path is unset" do context "when local option is set" do let(:local_opt) { tmpdir + "local" } it "should return nil" do expect(subject.load_solution_file(options)).to be_nil end end context "when global option is set" do let(:global_opt) { tmpdir + "global" } it "should return a SolutionFile instance" do expect(subject.load_solution_file(options)).to be_a(Vagrant::Bundler::SolutionFile) end it "should be located in the vagrant user data path" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to start_with(Vagrant.user_data_path.to_s) end it "should have a global.sol solution file" do file = subject.load_solution_file(options) expect(file.solution_file.to_s).to end_with("global.sol") end it "should have plugin file set to global value" do file = subject.load_solution_file(options) expect(file.plugin_file.to_s).to eq(global_opt.to_s) end end end end describe "#deinit" do it "should provide method for backwards compatibility" do subject.deinit end end describe "DEFAULT_GEM_SOURCES" do it "should list hashicorp gemstore first" do expect(described_class.const_get(:DEFAULT_GEM_SOURCES).first).to eq( described_class.const_get(:HASHICORP_GEMSTORE)) end end describe "#init!" do context "Gem.sources" do before { Gem.sources.clear Gem.sources << "https://rubygems.org/" } it "should add hashicorp gem store" do subject.init!([]) expect(Gem.sources).to include(described_class.const_get(:HASHICORP_GEMSTORE)) end it "should add hashicorp gem store to start of sources list" do subject.init!([]) expect(Gem.sources.sources.first.uri.to_s).to eq(described_class.const_get(:HASHICORP_GEMSTORE)) end end context "multiple specs" do let(:solution_file) { double('solution_file') } let(:vagrant_set) { double('vagrant_set') } before do allow(subject).to receive(:load_solution_file).and_return(solution_file) allow(subject).to receive(:generate_vagrant_set).and_return(vagrant_set) allow(solution_file).to receive(:valid?).and_return(true) end it "should activate spec of deps already loaded" do spec = Gem.loaded_specs.first deps = [spec[0]] specs = [spec[1].dup, spec[1].dup] specs[0].version = Gem::Version::new('0.0.1') # make sure haven't accidentally modified both expect(specs[0].version).to_not eq(specs[1].version) expect(solution_file).to receive(:dependency_list).and_return(deps) expect(vagrant_set).to receive(:find_all).and_return(specs) expect(subject).to receive(:activate_solution) do |activate_specs| expect(activate_specs.length()).to eq(1) expect(activate_specs[0].full_spec()).to eq(specs[1]) end subject.init!([]) end end end describe "#install" do let(:plugins){ {"my-plugin" => {"gem_version" => "> 0"}} } it "should pass plugin information hash to internal install" do expect(subject).to receive(:internal_install).with(plugins, any_args) subject.install(plugins) end it "should not include any update plugins" do expect(subject).to receive(:internal_install).with(anything, nil, any_args) subject.install(plugins) end it "should flag local when local is true" do expect(subject).to receive(:internal_install).with(any_args, env_local: true) subject.install(plugins, true) end it "should not flag local when local is not set" do expect(subject).to receive(:internal_install).with(any_args, env_local: false) subject.install(plugins) end end describe "#install_local" do let(:plugin_source){ double("plugin_source", spec: plugin_spec) } let(:plugin_spec){ double("plugin_spec", name: plugin_name, version: plugin_version) } let(:plugin_name){ "PLUGIN_NAME" } let(:plugin_version){ "1.0.0" } let(:plugin_path){ "PLUGIN_PATH" } let(:sources){ "SOURCES" } before do allow(Gem::Source::SpecificFile).to receive(:new).and_return(plugin_source) allow(subject).to receive(:internal_install) end it "should return plugin gem specification" do expect(subject.install_local(plugin_path)).to eq(plugin_spec) end it "should set custom sources" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(info[plugin_name]["sources"]).to eq(sources) end subject.install_local(plugin_path, sources: sources) end it "should not set the update parameter" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(update).to be_nil end subject.install_local(plugin_path) end it "should not set plugin as environment local by default" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(opts[:env_local]).to be_falsey end subject.install_local(plugin_path) end it "should set if plugin is environment local" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(opts[:env_local]).to be_truthy end subject.install_local(plugin_path, env_local: true) end end describe "#update" do let(:plugins){ :plugins } let(:specific){ [] } after{ subject.update(plugins, specific) } it "should mark update as true" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(update).to be_truthy end end context "with specific plugins named" do let(:specific){ ["PLUGIN_NAME"] } it "should set update to specific names" do expect(subject).to receive(:internal_install) do |info, update, opts| expect(update[:gems]).to eq(specific) end end end end describe "#vagrant_internal_specs" do let(:vagrant_spec) { double("vagrant_spec", name: "vagrant", version: Gem::Version.new(Vagrant::VERSION), activated?: vagrant_spec_activated, activate: nil, runtime_dependencies: vagrant_dep_specs) } let(:spec_list) { [] } let(:spec_dirs) { [] } let(:spec_default_dir) { "/dev/null" } let(:dir_spec_list) { [] } let(:vagrant_spec_activated) { true } let(:vagrant_dep_specs) { [] } before do allow(Gem::Specification).to receive(:find) { |&b| vagrant_spec if b.call(vagrant_spec) } allow(Gem::Specification).to receive(:find_all).and_return(spec_list) allow(Gem::Specification).to receive(:dirs).and_return(spec_dirs) allow(Gem::Specification).to receive(:default_specifications_dir).and_return(spec_default_dir) allow(Gem::Specification).to receive(:each_spec).and_return(dir_spec_list) end it "should return an empty list" do expect(subject.send(:vagrant_internal_specs)).to eq([]) end context "when vagrant specification is not activated" do let(:vagrant_spec_activated) { false } it "should activate the specification" do expect(vagrant_spec).to receive(:activate) subject.send(:vagrant_internal_specs) end end context "when vagrant specification is not found" do before { allow(Gem::Specification).to receive(:find).and_return(nil) } it "should raise not found error" do expect { subject.send(:vagrant_internal_specs) }.to raise_error(Vagrant::Errors::SourceSpecNotFound) end end context "when bundler is not defined" do before { expect(Vagrant).to receive(:in_bundler?).and_return(false) } context "when running inside the installer" do before { expect(Vagrant).to receive(:in_installer?).and_return(true) } it "should load gem specification directories" do expect(Gem::Specification).to receive(:dirs).and_return(spec_dirs) subject.send(:vagrant_internal_specs) end context "when checking paths" do let(:spec_dirs) { [double("spec-dir", start_with?: in_user_dir)] } let(:in_user_dir) { true } let(:user_dir) { double("user-dir") } before { allow(Gem).to receive(:user_dir).and_return(user_dir) } it "should check if path is within local user directory" do expect(spec_dirs.first).to receive(:start_with?).with(user_dir).and_return(false) subject.send(:vagrant_internal_specs) end context "when path is not within user directory" do let(:in_user_dir) { false } it "should use path when loading specs" do expect(Gem::Specification).to receive(:each_spec) { |arg| expect(arg).to include(spec_dirs.first) } subject.send(:vagrant_internal_specs) end end end end context "when running outside the installer" do before { expect(Vagrant).to receive(:in_installer?).and_return(false) } it "should not load gem specification directories" do expect(Gem::Specification).not_to receive(:dirs) subject.send(:vagrant_internal_specs) end end end end describe Vagrant::Bundler::PluginSet do let(:name) { "test-gem" } let(:version) { "1.0.0" } let(:directory) { @directory ||= Dir.mktmpdir("vagrant-bundler-test") } after do FileUtils.rm_rf(@directory) if @directory @directory = nil end describe "#add_vendor_gem" do context "when spec file does not exist" do it "should raise a not found error" do expect { subject.add_vendor_gem(name, directory) }.to raise_error(Gem::GemNotFoundException) end end context "when spec file exists" do before do spec = Gem::Specification.new(name, version) File.write(File.join(directory, "#{name}.gemspec"), spec.to_ruby) end it "should load the specification" do expect(subject.add_vendor_gem(name, directory)).to be_a(Gem::Specification) end it "should set the full path in specification" do spec = subject.add_vendor_gem(name, directory) expect(spec.full_gem_path).to eq(directory) end end end describe "#find_all" do let(:request) { Gem::Resolver::DependencyRequest.new(dependency, nil) } let(:dependency) { Gem::Dependency.new("test-gem", requirement) } let(:requirement) { Gem::Requirement.new(version) } context "when specification is not included in set" do it "should return empty array" do expect(subject.find_all(request)).to eq([]) end end context "when specification is included in set" do before do spec = Gem::Specification.new(name, version) File.write(File.join(directory, "#{name}.gemspec"), spec.to_ruby) subject.add_vendor_gem(name, directory) end it "should return a vendor specification instance" do expect(subject.find_all(request).first).to be_a(Gem::Resolver::VendorSpecification) end end end end end ================================================ FILE: test/unit/vagrant/capability_host_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) require "vagrant/capability_host" describe Vagrant::CapabilityHost do include_context "capability_helpers" subject do Class.new do extend Vagrant::CapabilityHost end end describe "#initialize_capabilities! and #capability_host_chain" do it "raises an error if an explicit host is not found" do expect { subject.initialize_capabilities!(:foo, {}, {}) }. to raise_error(Vagrant::Errors::CapabilityHostExplicitNotDetected) end it "raises an error if a host can't be detected" do hosts = { foo: [detect_class(false), nil], bar: [detect_class(false), :foo], } expect { subject.initialize_capabilities!(nil, hosts, {}) }. to raise_error(Vagrant::Errors::CapabilityHostNotDetected) end it "passes on extra args to the detect method" do klass = Class.new do def detect?(*args) raise "detect: #{args.inspect}" end end hosts = { foo: [klass, nil], } expect { subject.initialize_capabilities!(nil, hosts, {}, 1, 2) }. to raise_error(RuntimeError, "detect: [1, 2]") end it "detects a basic child" do hosts = { foo: [detect_class(false), nil], bar: [detect_class(true), nil], baz: [detect_class(false), nil], } subject.initialize_capabilities!(nil, hosts, {}) chain = subject.capability_host_chain expect(chain.length).to eql(1) expect(chain[0][0]).to eql(:bar) end it "detects the host with the most parents (deepest) first" do hosts = { foo: [detect_class(true), nil], bar: [detect_class(true), :foo], baz: [detect_class(true), :bar], foo2: [detect_class(true), nil], bar2: [detect_class(true), :foo2], } subject.initialize_capabilities!(nil, hosts, {}) chain = subject.capability_host_chain expect(chain.length).to eql(3) expect(chain.map(&:first)).to eql([:baz, :bar, :foo]) end it "detects a forced host" do hosts = { foo: [detect_class(false), nil], bar: [detect_class(false), nil], baz: [detect_class(false), nil], } subject.initialize_capabilities!(:bar, hosts, {}) chain = subject.capability_host_chain expect(chain.length).to eql(1) expect(chain[0][0]).to eql(:bar) end end describe "#capability?" do before do host = nil hosts = { foo: [detect_class(true), nil], bar: [detect_class(true), :foo], } caps = { foo: { parent: Class.new }, bar: { self: Class.new }, } subject.initialize_capabilities!(host, hosts, caps) end it "does not have a non-existent capability" do expect(subject.capability?(:foo)).to be(false) end it "has capabilities of itself" do expect(subject.capability?(:self)).to be(true) end it "has capabilities of parent" do expect(subject.capability?(:parent)).to be(true) end end describe "capability" do let(:caps) { {} } def init host = nil hosts = { foo: [detect_class(true), nil], bar: [detect_class(true), :foo], } subject.initialize_capabilities!(host, hosts, caps) end it "executes the capability" do caps[:bar] = { test: cap_instance(:test) } init expect { subject.capability(:test) }. to raise_error(RuntimeError, "cap: test []") end it "executes the capability with arguments" do caps[:bar] = { test: cap_instance(:test) } init expect { subject.capability(:test, 1) }. to raise_error(RuntimeError, "cap: test [1]") end it "raises an exception if the capability doesn't exist" do init expect { subject.capability(:what_is_this_i_dont_even) }. to raise_error(Vagrant::Errors::CapabilityNotFound) end it "raises an exception if the method doesn't exist on the module" do caps[:bar] = { test_is_corrupt: cap_instance(:test_is_corrupt, corrupt: true) } init expect { subject.capability(:test_is_corrupt) }. to raise_error(Vagrant::Errors::CapabilityInvalid) end end end ================================================ FILE: test/unit/vagrant/cli_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../base" require "vagrant/cli" require "vagrant/util" describe Vagrant::CLI do include_context "unit" include_context "command plugin helpers" let(:commands) { {} } let(:iso_env) { isolated_environment } let(:env) { iso_env.create_vagrant_env } let(:checkpoint) { double("checkpoint") } before do allow(Vagrant.plugin("2").manager).to receive(:commands).and_return(commands) allow(Vagrant::Util::CheckpointClient).to receive(:instance).and_return(checkpoint) allow(checkpoint).to receive(:setup).and_return(checkpoint) allow(checkpoint).to receive(:check) allow(checkpoint).to receive(:display) end describe "#initialize" do it "should setup checkpoint" do expect(checkpoint).to receive(:check) described_class.new(["destroy"], env) end end describe "#execute" do let(:triggers) { double("triggers") } it "invokes help and exits with 1 if invalid command" do subject = described_class.new(["i-dont-exist"], env) expect(subject).to receive(:help).once expect(subject.execute).to eql(1) end it "invokes command and returns its exit status if the command is valid" do commands[:destroy] = [command_lambda("destroy", 42), {}] subject = described_class.new(["destroy"], env) expect(subject).not_to receive(:help) expect(subject.execute).to eql(42) end it "returns exit code 1 if interrupted" do commands[:destroy] = [command_lambda("destroy", 42, exception: Interrupt), {}] subject = described_class.new(["destroy"], env) expect(subject.execute).to eql(1) end it "displays any checkpoint information" do commands[:destroy] = [command_lambda("destroy", 42), {}] expect(checkpoint).to receive(:display) described_class.new(["destroy"], env).execute end it "fires triggers, if enabled" do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). with("typed_triggers").and_return(true) allow(triggers).to receive(:fire) allow(triggers).to receive(:find).and_return([double("trigger-result")]) commands[:destroy] = [command_lambda("destroy", 42), {}] allow(Vagrant::Plugin::V2::Trigger).to receive(:new).and_return(triggers) subject = described_class.new(["destroy"], env) expect(triggers).to receive(:fire).twice expect(subject).not_to receive(:help) expect(subject.execute).to eql(42) end it "does not fire triggers if disabled" do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). with("typed_triggers").and_return(false) commands[:destroy] = [command_lambda("destroy", 42), {}] subject = described_class.new(["destroy"], env) expect(triggers).not_to receive(:fire) expect(subject).not_to receive(:help) expect(subject.execute).to eql(42) end end describe "#help" do subject { described_class.new([], env) } it "includes all primary subcommands" do commands[:foo] = [command_lambda("foo", 0), { primary: true }] commands[:bar] = [command_lambda("bar", 0), { primary: true }] commands[:baz] = [command_lambda("baz", 0), { primary: false }] expect(env.ui).to receive(:info).with(any_args) { |message, opts| expect(message).to include("foo") expect(message).to include("bar") expect(message.include?("baz")).to be(false) } subject.help end end end ================================================ FILE: test/unit/vagrant/config/loader_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/registry" describe Vagrant::Config::Loader do include_context "unit" # This is the current version of configuration for the tests. let(:current_version) { version_order.last } # This is just a dummy implementation of a configuration loader which # simply acts on hashes. let(:test_loader) do Class.new(Vagrant::Config::VersionBase) do def self.init {} end def self.load(proc) init.tap do |obj| proc.call(obj) end end def self.merge(old, new) old.merge(new) {|key, oldval, newval| oldval.concat(newval)} end end end let(:versions) do Vagrant::Registry.new.tap do |r| r.register("1") { test_loader } end end let(:version_order) { ["1"] } let(:instance) { described_class.new(versions, version_order) } describe "#set" do context "with an object that cannot be inspected" do # This represents the euro symbol in UTF-16LE. pack("c*") returns an ASCII # string and so we have to force the encoding UTF_16LE_STRING_THAT_CANNOT_BE_DOWNCAST_TO_ASCII = [0x20, 0xAC].pack("c*").force_encoding("UTF-16LE") let(:klass_with_bad_inspect_string) do Class.new do def inspect UTF_16LE_STRING_THAT_CANNOT_BE_DOWNCAST_TO_ASCII end end end let(:test_source) { Class.new do def initialize(collaborator) @foo = collaborator.new end end.new(klass_with_bad_inspect_string) } it "does not raise the ascii encoding exception" do expect { instance.set(:arbitrary, test_source) }.to raise_error(ArgumentError, /Unknown configuration source/) end end end describe "basic loading" do it "should ignore non-existent load order keys" do instance.load([:foo]) end it "should load and return the configuration" do proc = Proc.new do |config| config[:foo] = "yep" end instance.set(:proc, [[current_version, proc]]) config, warnings, errors = instance.load([:proc]) expect(config[:foo]).to eq("yep") expect(warnings).to eq([]) expect(errors).to eq([]) end it "should throw a NameError exception if invalid or undefined variable is used" do vagrantfile = <<-VF Vagrant.configure("2") do |config| config.ssh.port = variable end VF instance.set(:foo, temporary_file(vagrantfile)) expect { instance.load([:foo]) }.to raise_error(Vagrant::Errors::VagrantfileNameError, /invalid or undefined variable/) end end describe "finalization" do it "should finalize the configuration" do # Create the finalize method on our loader def test_loader.finalize(obj) obj[:finalized] = true obj end # Basic configuration proc proc = lambda do |config| config[:foo] = "yep" end # Run the actual configuration and assert that we get the proper result instance.set(:proc, [[current_version, proc]]) config, _ = instance.load([:proc]) expect(config[:foo]).to eq("yep") expect(config[:finalized]).to eq(true) end end describe "upgrading" do it "should do an upgrade to the latest version" do test_loader_v2 = Class.new(test_loader) do def self.upgrade(old) new = old.dup new[:v2] = true [new, [], []] end end versions.register("2") { test_loader_v2 } version_order << "2" # Load a version 1 proc, and verify it is upgraded to version 2 proc = lambda { |config| config[:foo] = "yep" } instance.set(:proc, [["1", proc]]) config, _ = instance.load([:proc]) expect(config[:foo]).to eq("yep") expect(config[:v2]).to eq(true) end it "should keep track of warnings and errors" do test_loader_v2 = Class.new(test_loader) do def self.upgrade(old) new = old.dup new[:v2] = true [new, ["foo!"], ["bar!"]] end end versions.register("2") { test_loader_v2 } version_order << "2" # Load a version 1 proc, and verify it is upgraded to version 2 proc = lambda { |config| config[:foo] = "yep" } instance.set(:proc, [["1", proc]]) config, warnings, errors = instance.load([:proc]) expect(config[:foo]).to eq("yep") expect(config[:v2]).to eq(true) expect(warnings).to eq(["foo!"]) expect(errors).to eq(["bar!"]) end end describe "loading edge cases" do it "should only run the same proc once" do count = 0 proc = Proc.new do |config| config[:foo] = "yep" count += 1 end instance.set(:proc, [[current_version, proc]]) 5.times do result, _ = instance.load([:proc]) # Verify the config result expect(result[:foo]).to eq("yep") # Verify the count is only one expect(count).to eq(1) end end it "should discard duplicate configs if :home and :root are the same" do proc = Proc.new do |config| config[:foo] = ["yep"] end order = [:root, :home] instance.set(:root, [[current_version, proc]]) instance.set(:home, [[current_version, proc]]) result, warnings, errors = instance.load(order) # Verify the config result expect(result[:foo]).to eq(["yep"]) expect(result[:foo].size).to eq(1) expect(warnings).to eq([]) expect(errors).to eq([]) end it "should only load configuration files once" do $_config_data = 0 # We test both setting a file multiple times as well as multiple # loads, since both should not cache the data. file = temporary_file("$_config_data += 1") 5.times { instance.set(:file, file) } 5.times { instance.load([:file]) } expect($_config_data).to eq(1) end it "should not clear the cache if setting to the same value multiple times" do $_config_data = 0 file = temporary_file("$_config_data += 1") instance.set(:proc, file) 5.times { instance.load([:proc]) } instance.set(:proc, file) 5.times { instance.load([:proc]) } expect($_config_data).to eq(1) end it "should raise proper error if there is a syntax error in a Vagrantfile" do expect { instance.set(:file, temporary_file("Vagrant:^Config")) }. to raise_exception(Vagrant::Errors::VagrantfileSyntaxError) end it "should raise a proper error if there is a problem with the Vagrantfile" do expect { instance.set(:file, temporary_file("foo")) }. to raise_exception(Vagrant::Errors::VagrantfileLoadError) end end end ================================================ FILE: test/unit/vagrant/config/v1/dummy_config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Config::V1::DummyConfig do it "should allow attribute setting" do expect { subject.foo = :bar }. to_not raise_error end it "should allow method calls that return more DummyConfigs" do expect(subject.foo).to be_kind_of(described_class) end it "should allow hash access" do expect { subject[:foo] }. to_not raise_error expect(subject[:foo]).to be_kind_of(described_class) end it "should allow setting hash values" do expect { subject[:foo] = :bar }. to_not raise_error end end ================================================ FILE: test/unit/vagrant/config/v1/loader_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ostruct" require File.expand_path("../../../../base", __FILE__) describe Vagrant::Config::V1::Loader do include_context "unit" before(:each) do # Force the V1 loader to believe that we are in V1 stub_const("Vagrant::Config::CURRENT_VERSION", "1") end describe "empty" do it "returns an empty configuration object" do result = described_class.init expect(result).to be_kind_of(Vagrant::Config::V1::Root) end it "returns an object with all configuration keys loaded if V1" do # Make sure we're version 1 stub_const("Vagrant::Config::CURRENT_VERSION", "1") # Register some config classes. register_plugin("1") do |p| p.config("foo") { OpenStruct } p.config("bar", true) { OpenStruct } end # Test that we have all keys result = described_class.init expect(result.foo).to be_kind_of(OpenStruct) expect(result.bar).to be_kind_of(OpenStruct) end it "returns only upgradable config objects if not V1" do # Make sure we're NOT version 1 stub_const("Vagrant::Config::CURRENT_VERSION", "2") # Register some config classes. register_plugin("1") do |p| p.config("foo") { OpenStruct } p.config("bar", true) { OpenStruct } end # Test that we have all keys result = described_class.init expect(result.bar).to be_kind_of(OpenStruct) end end describe "finalizing" do it "should call `#finalize` on the configuration object" do # Register a plugin for our test register_plugin("1") do |plugin| plugin.config "foo" do Class.new do attr_accessor :bar def finalize! @bar = "finalized" end end end end # Create the proc we're testing config_proc = Proc.new do |config| config.foo.bar = "value" end # Test that it works properly config = described_class.load(config_proc) expect(config.foo.bar).to eq("value") # Finalize it described_class.finalize(config) expect(config.foo.bar).to eq("finalized") end end describe "loading" do it "should configure with all plugin config keys loaded" do # Register a plugin for our test register_plugin("1") do |plugin| plugin.config("foo") { OpenStruct } end # Create the proc we're testing config_proc = Proc.new do |config| config.foo.bar = "value" end # Test that it works properly config = described_class.load(config_proc) expect(config.foo.bar).to eq("value") end end describe "merging" do it "should merge available configuration keys" do old = Vagrant::Config::V1::Root.new({ foo: Object }) new = Vagrant::Config::V1::Root.new({ bar: Object }) result = described_class.merge(old, new) expect(result.foo).to be_kind_of(Object) expect(result.bar).to be_kind_of(Object) end it "should merge instantiated objects" do config_class = Class.new do attr_accessor :value end old = Vagrant::Config::V1::Root.new({ foo: config_class }) old.foo.value = "old" new = Vagrant::Config::V1::Root.new({ bar: config_class }) new.bar.value = "new" result = described_class.merge(old, new) expect(result.foo.value).to eq("old") expect(result.bar.value).to eq("new") end it "should merge conflicting classes by calling `merge`" do config_class = Class.new do attr_accessor :value def merge(new) result = self.class.new result.value = @value + new.value result end end old = Vagrant::Config::V1::Root.new({ foo: config_class }) old.foo.value = 10 new = Vagrant::Config::V1::Root.new({ foo: config_class }) new.foo.value = 15 result = described_class.merge(old, new) expect(result.foo.value).to eq(25) end end end ================================================ FILE: test/unit/vagrant/config/v1/root_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Config::V1::Root do include_context "unit" it "should provide access to config objects" do foo_class = Class.new map = { foo: foo_class } instance = described_class.new(map) foo = instance.foo expect(foo).to be_kind_of(foo_class) expect(instance.foo).to eql(foo) end it "can be created with initial state" do instance = described_class.new({}, { foo: "bar" }) expect(instance.foo).to eq("bar") end it "should return internal state" do map = { "foo" => Object, "bar" => Object } instance = described_class.new(map) expect(instance.__internal_state).to eq({ "config_map" => map, "keys" => {}, "missing_key_calls" => Set.new }) end it "should record missing key calls" do instance = described_class.new({}) instance.foo.bar = false keys = instance.__internal_state["missing_key_calls"] expect(keys).to be_kind_of(Set) expect(keys.length).to eq(1) expect(keys.include?("foo")).to be end end ================================================ FILE: test/unit/vagrant/config/v2/dummy_config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Config::V2::DummyConfig do it "should allow attribute setting" do expect { subject.foo = :bar }. to_not raise_error end it "should allow method calls that return more DummyConfigs" do expect(subject.foo).to be_kind_of(described_class) end it "should allow hash access" do expect { subject[:foo] }. to_not raise_error expect(subject[:foo]).to be_kind_of(described_class) end it "should allow setting hash values" do expect { subject[:foo] = :bar }. to_not raise_error end it "should survive being the last arg to a method that captures kwargs without a ruby conversion error" do arg_capturer = lambda { |*args, **kwargs| } expect { arg_capturer.call(subject) }.to_not raise_error end end ================================================ FILE: test/unit/vagrant/config/v2/loader_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "ostruct" require File.expand_path("../../../../base", __FILE__) describe Vagrant::Config::V2::Loader do include_context "unit" before(:each) do # Force the V2 loader to believe that we are in V2 stub_const("Vagrant::Config::CURRENT_VERSION", "2") end describe "empty" do it "returns an empty configuration object" do result = described_class.init expect(result).to be_kind_of(Vagrant::Config::V2::Root) end end describe "finalizing" do it "should call `#finalize` on the configuration object" do # Register a plugin for our test register_plugin("2") do |plugin| plugin.config "foo" do Class.new(Vagrant.plugin("2", "config")) do attr_accessor :bar def finalize! @bar = "finalized" end end end end # Create the proc we're testing config_proc = Proc.new do |config| config.foo.bar = "value" end # Test that it works properly config = described_class.load(config_proc) expect(config.foo.bar).to eq("value") # Finalize it described_class.finalize(config) expect(config.foo.bar).to eq("finalized") end end describe "loading" do it "should configure with all plugin config keys loaded" do # Register a plugin for our test register_plugin("2") do |plugin| plugin.config("foo") { OpenStruct } end # Create the proc we're testing config_proc = Proc.new do |config| config.foo.bar = "value" end # Test that it works properly config = described_class.load(config_proc) expect(config.foo.bar).to eq("value") end end describe "merging" do it "should merge available configuration keys" do old = Vagrant::Config::V2::Root.new({ foo: Object }) new = Vagrant::Config::V2::Root.new({ bar: Object }) result = described_class.merge(old, new) expect(result.foo).to be_kind_of(Object) expect(result.bar).to be_kind_of(Object) end it "should merge instantiated objects" do config_class = Class.new do attr_accessor :value end old = Vagrant::Config::V2::Root.new({ foo: config_class }) old.foo.value = "old" new = Vagrant::Config::V2::Root.new({ bar: config_class }) new.bar.value = "new" result = described_class.merge(old, new) expect(result.foo.value).to eq("old") expect(result.bar.value).to eq("new") end it "should merge conflicting classes by calling `merge`" do config_class = Class.new do attr_accessor :value def merge(new) result = self.class.new result.value = @value + new.value result end end old = Vagrant::Config::V2::Root.new({ foo: config_class }) old.foo.value = 10 new = Vagrant::Config::V2::Root.new({ foo: config_class }) new.foo.value = 15 result = described_class.merge(old, new) expect(result.foo.value).to eq(25) end end describe "upgrading" do it "should continue fine if the key doesn't implement upgrade" do # Make an old V1 root object old = Vagrant::Config::V1::Root.new({ foo: Class.new }) # It should work fine expect { result = described_class.upgrade(old) }.to_not raise_error end it "should upgrade the config if it implements the upgrade method" do # Create the old V1 class that will be upgraded config_class = Class.new do attr_accessor :value def upgrade(new) new.foo.value = value * 2 [["foo"], ["bar"]] end end # Create the new V2 plugin it is writing to register_plugin("2") do |p| p.config("foo") { OpenStruct } end # Test it out! old = Vagrant::Config::V1::Root.new({ foo: config_class }) old.foo.value = 5 data = described_class.upgrade(old) expect(data[0].foo.value).to eq(10) expect(data[1]).to eq(["foo"]) expect(data[2]).to eq(["bar"]) end end end ================================================ FILE: test/unit/vagrant/config/v2/root_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "set" require File.expand_path("../../../../base", __FILE__) describe Vagrant::Config::V2::Root do include_context "unit" it "should provide access to config objects" do foo_class = Class.new map = { foo: foo_class } instance = described_class.new(map) foo = instance.foo expect(foo).to be_kind_of(foo_class) expect(instance.foo).to eql(foo) end it "record a missing key call if invalid key used" do instance = described_class.new({}) expect { instance.foo }.to_not raise_error expect(instance.__internal_state["missing_key_calls"].include?("foo")).to be end it "returns a dummy config for a missing key" do instance = described_class.new({}) expect { instance.foo.foo = "bar" }.to_not raise_error end it "can be created with initial state" do instance = described_class.new({}, { foo: "bar" }) expect(instance.foo).to eq("bar") end it "should return internal state" do map = { "foo" => Object, "bar" => Object } instance = described_class.new(map) expect(instance.__internal_state).to eq({ "config_map" => map, "keys" => {}, "missing_key_calls" => Set.new }) end describe "#finalize!" do it "should call #finalize!" do foo_class = Class.new(Vagrant.plugin("2", "config")) do attr_accessor :foo def finalize! @foo = "SET" end end map = { foo: foo_class } instance = described_class.new(map) instance.finalize! expect(instance.foo.foo).to eq("SET") end it "should call #_finalize!" do klass = Class.new(Vagrant.plugin("2", "config")) expect_any_instance_of(klass).to receive(:finalize!) expect_any_instance_of(klass).to receive(:_finalize!) map = { foo: klass } instance = described_class.new(map) instance.finalize! end end describe "validation" do let(:instance) do map = { foo: Object, bar: Object } described_class.new(map) end it "should return nil if valid" do expect(instance.validate({})).to eq({}) end it "should return errors if invalid" do errors = { "foo" => ["errors!"] } env = { "errors" => errors } foo = instance.foo def foo.validate(env) env["errors"] end expect(instance.validate(env)).to eq(errors) end context "with vms and ignoring provider validations" do let(:instance) do map = { vm: Object, bar: Object } described_class.new(map) end it "should pass along the ignore_provider flag for ignoring validations" do errors = { "vm" => ["errors!"] } env = { "errors" => errors } vm = instance.vm def vm.validate(env, param) env["errors"] end expect(instance.validate({}, true)).to eq({}) end end it "should merge errors via array concat if matching keys" do errors = { "foo" => ["errors!"] } env = { "errors" => errors } foo = instance.foo bar = instance.bar def foo.validate(env) env["errors"] end def bar.validate(env) env["errors"].merge({ "bar" => ["bar"] }) end expected_errors = { "foo" => ["errors!", "errors!"], "bar" => ["bar"] } expect(instance.validate(env)).to eq(expected_errors) end it "shouldn't count empty keys" do errors = { "foo" => [] } env = { "errors" => errors } foo = instance.foo def foo.validate(env) env["errors"] end expect(instance.validate(env)).to eq({}) end end end ================================================ FILE: test/unit/vagrant/config/v2/util_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require "vagrant/config/v2/util" describe Vagrant::Config::V2::Util do describe "merging errors" do it "should merge matching keys and leave the rest alone" do first = { "one" => ["foo"], "two" => ["two"] } second = { "one" => ["bar"], "three" => ["three"] } expected = { "one" => ["foo", "bar"], "two" => ["two"], "three" => ["three"] } result = described_class.merge_errors(first, second) expect(result).to eq(expected) end end end ================================================ FILE: test/unit/vagrant/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) describe Vagrant::Config do it "should not execute the proc on configuration" do described_class.run do raise Exception, "Failure." end end it "should capture calls to `Vagrant.configure`" do receiver = double() procs = described_class.capture_configures do Vagrant.configure("1") do receiver.one end Vagrant.configure("2") do receiver.two end end expect(procs).to be_kind_of(Array) expect(procs.length).to eq(2) expect(procs[0][0]).to eq("1") expect(procs[1][0]).to eq("2") # Verify the proper procs were captured expect(receiver).to receive(:one).once.ordered expect(receiver).to receive(:two).once.ordered procs[0][1].call procs[1][1].call end it "should work with integer configurations "do receiver = double() procs = described_class.capture_configures do Vagrant.configure(1) do receiver.one end Vagrant.configure("2") do receiver.two end end expect(procs).to be_kind_of(Array) expect(procs.length).to eq(2) expect(procs[0][0]).to eq("1") expect(procs[1][0]).to eq("2") # Verify the proper procs were captured expect(receiver).to receive(:one).once.ordered expect(receiver).to receive(:two).once.ordered procs[0][1].call procs[1][1].call end it "should capture configuration procs" do receiver = double() procs = described_class.capture_configures do described_class.run do receiver.hello! end end # Verify the structure of the result expect(procs).to be_kind_of(Array) expect(procs.length).to eq(1) # Verify that the proper proc was captured expect(receiver).to receive(:hello!).once expect(procs[0][0]).to eq("1") procs[0][1].call end it "should capture the proper version" do procs = described_class.capture_configures do described_class.run("1") {} described_class.run("2") {} end # Verify the structure of the result expect(procs).to be_kind_of(Array) expect(procs.length).to eq(2) expect(procs[0][0]).to eq("1") expect(procs[1][0]).to eq("2") end end ================================================ FILE: test/unit/vagrant/environment_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) require "json" require "pathname" require "tempfile" require "tmpdir" require "vagrant/util/file_mode" require "vagrant/util/platform" describe Vagrant::Environment do include_context "unit" include_context "capability_helpers" let(:env) do isolated_environment.tap do |e| e.box3("base", "1.0", :virtualbox) e.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vm.box = "base" end VF end end 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 allow(m).to receive(:hosts).and_return(plugin_hosts) allow(m).to receive(:host_capabilities).and_return(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 "#gems_path" do it "is set to Vagrant::Bundler defined path" do instance = described_class.new expect(instance.gems_path).to eq(Vagrant::Bundler.instance.plugin_gem_path) end end describe "#home_path" do it "is set to the home path given" do Dir.mktmpdir("vagrant-test-env-home-path-given") do |dir| instance = described_class.new(home_path: dir) expect(instance.home_path).to eq(Pathname.new(dir)) end end it "is set to the environmental variable VAGRANT_HOME" do Dir.mktmpdir("vagrant-test-env-home-env-var") do |dir| instance = with_temp_env("VAGRANT_HOME" => dir.to_s) do described_class.new end expect(instance.home_path).to eq(Pathname.new(dir)) end end it "throws an exception if inaccessible", skip_windows: true do expect { described_class.new(home_path: "/") }.to raise_error(Vagrant::Errors::HomeDirectoryNotAccessible) end context "with setup version file" do it "creates a setup version flie" do path = subject.home_path.join("setup_version") expect(path).to be_file expect(path.read).to eq(Vagrant::Environment::CURRENT_SETUP_VERSION) end it "is okay if it has the current version" do Dir.mktmpdir("vagrant-test-env-current-version") do |dir| Pathname.new(dir).join("setup_version").open("w") do |f| f.write(Vagrant::Environment::CURRENT_SETUP_VERSION) end instance = described_class.new(home_path: dir) path = instance.home_path.join("setup_version") expect(path).to be_file expect(path.read).to eq(Vagrant::Environment::CURRENT_SETUP_VERSION) end end it "raises an exception if the version is newer than ours" do Dir.mktmpdir("vagrant-test-env-newer-version") do |dir| Pathname.new(dir).join("setup_version").open("w") do |f| f.write("100.5") end expect { described_class.new(home_path: dir) }. to raise_error(Vagrant::Errors::HomeDirectoryLaterVersion) end end it "raises an exception if there is an unknown home directory version" do Dir.mktmpdir("vagrant-test-env-unknown-home") do |dir| Pathname.new(dir).join("setup_version").open("w") do |f| f.write("0.7") end expect { described_class.new(home_path: dir) }. to raise_error(Vagrant::Errors::HomeDirectoryUnknownVersion) end end end context "upgrading a v1.1 directory structure" do let(:env) { isolated_environment } before do env.homedir.join("setup_version").open("w") do |f| f.write("1.1") end allow_any_instance_of(Vagrant::UI::Silent). to receive(:ask) end it "replaces the setup version with the new version" do expect(subject.home_path.join("setup_version").read). to eq(Vagrant::Environment::CURRENT_SETUP_VERSION) end it "moves the boxes into the new directory structure" do # Kind of hacky but avoids two instantiations of BoxCollection allow(Vagrant::Environment).to receive(:boxes) .and_return(double("boxes")) collection = double("collection") expect(Vagrant::BoxCollection).to receive(:new).with( env.homedir.join("boxes"), anything).twice.and_return(collection) expect(collection).to receive(:upgrade_v1_1_v1_5).once subject end end end describe "#host" do let(:plugin_hosts) { {} } let(:plugin_host_caps) { {} } before do m = Vagrant.plugin("2").manager allow(m).to receive(:hosts).and_return(plugin_hosts) allow(m).to receive(:host_capabilities).and_return(plugin_host_caps) end it "should default to some host even if there are none" do env.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vagrant.host = nil end VF expect(subject.host).to be end it "should attempt to detect a host if no host is set" do env.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vagrant.host = nil end VF plugin_hosts[:foo] = [detect_class(true), nil] plugin_host_caps[:foo] = { bar: Class } result = subject.host expect(result.capability?(:bar)).to be(true) end it "should attempt to detect a host if host is :detect" do env.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vagrant.host = :detect end VF plugin_hosts[:foo] = [detect_class(true), nil] plugin_host_caps[:foo] = { bar: Class } result = subject.host expect(result.capability?(:bar)).to be(true) end it "should use an exact host if specified" do env.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vagrant.host = "foo" end VF plugin_hosts[:foo] = [detect_class(false), nil] plugin_hosts[:bar] = [detect_class(true), nil] plugin_host_caps[:foo] = { bar: Class } result = subject.host expect(result.capability?(:bar)).to be(true) end it "should raise an error if an exact match was specified but not found" do env.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vagrant.host = "bar" end VF expect { subject.host }. to raise_error(Vagrant::Errors::HostExplicitNotDetected) end end describe "#lock" do def lock_count subject.data_dir. children. find_all { |c| c.to_s.end_with?("lock") }. length end it "does nothing if no block is given" do subject.lock end it "locks the environment" do another = env.create_vagrant_env raised = false subject.lock do begin another.lock {} rescue Vagrant::Errors::EnvironmentLockedError raised = true end end expect(raised).to be(true) end it "allows nested locks on the same environment" do success = false subject.lock do subject.lock do success = true end end expect(success).to be(true) end it "cleans up all lock files" do inner_count = nil expect(lock_count).to eq(0) subject.lock do inner_count = lock_count end expect(inner_count).to_not be_nil expect(inner_count).to eq(2) expect(lock_count).to eq(1) end end describe "#machine" do # A helper to register a provider for use in tests. def register_provider(name, config_class=nil, options=nil) provider_cls = Class.new(VagrantTests::DummyProvider) register_plugin("2") do |p| p.provider(name, options) { provider_cls } if config_class p.config(name, :provider) { config_class } end end provider_cls end it "should return a machine object with the correct provider" do # Create a provider foo_provider = register_provider("foo") # Create the configuration isolated_env = isolated_environment do |e| e.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" config.vm.define "foo" end VF e.box3("base", "1.0", :foo) end # Verify that we can get the machine env = isolated_env.create_vagrant_env machine = env.machine(:foo, :foo) expect(machine).to be_kind_of(Vagrant::Machine) expect(machine.name).to eq(:foo) expect(machine.provider).to be_kind_of(foo_provider) expect(machine.provider_config).to be_nil end it "should return a machine object with the machine configuration" do # Create a provider foo_config = Class.new(Vagrant.plugin("2", :config)) do attr_accessor :value end foo_provider = register_provider("foo", foo_config) # Create the configuration isolated_env = isolated_environment do |e| e.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" config.vm.define "foo" config.vm.provider :foo do |fooconfig| fooconfig.value = 100 end end VF e.box3("base", "1.0", :foo) end # Verify that we can get the machine env = isolated_env.create_vagrant_env machine = env.machine(:foo, :foo) expect(machine).to be_kind_of(Vagrant::Machine) expect(machine.name).to eq(:foo) expect(machine.provider).to be_kind_of(foo_provider) expect(machine.provider_config.value).to eq(100) end it "should cache the machine objects by name and provider" do # Create a provider foo_provider = register_provider("foo") bar_provider = register_provider("bar") # Create the configuration isolated_env = isolated_environment do |e| e.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" config.vm.define "vm1" config.vm.define "vm2" end VF e.box3("base", "1.0", :foo) e.box3("base", "1.0", :bar) end env = isolated_env.create_vagrant_env vm1_foo = env.machine(:vm1, :foo) vm1_bar = env.machine(:vm1, :bar) vm2_foo = env.machine(:vm2, :foo) expect(vm1_foo).to eql(env.machine(:vm1, :foo)) expect(vm1_bar).to eql(env.machine(:vm1, :bar)) expect(vm1_foo).not_to eql(vm1_bar) expect(vm2_foo).to eql(env.machine(:vm2, :foo)) end it "should load a machine without a box" do register_provider("foo") environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "i-dont-exist" end VF end env = environment.create_vagrant_env machine = env.machine(:default, :foo) expect(machine.box).to be_nil end it "should load the machine configuration" do register_provider("foo") environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.ssh.port = 1 config.vm.box = "base" config.vm.define "vm1" do |inner| inner.ssh.port = 100 end end VF env.box3("base", "1.0", :foo) end env = environment.create_vagrant_env machine = env.machine(:vm1, :foo) expect(machine.config.ssh.port).to eq(100) expect(machine.config.vm.box).to eq("base") end it "should load the box configuration for a box" do register_provider("foo") environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" end VF env.box3("base", "1.0", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 100 end VF end env = environment.create_vagrant_env machine = env.machine(:default, :foo) expect(machine.config.ssh.port).to eq(100) end it "should load the box configuration for a box and custom Vagrantfile name" do register_provider("foo") environment = isolated_environment do |env| env.file("some_other_name", <<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" end VF env.box3("base", "1.0", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 100 end VF end env = with_temp_env("VAGRANT_VAGRANTFILE" => "some_other_name") do environment.create_vagrant_env end machine = env.machine(:default, :foo) expect(machine.config.ssh.port).to eq(100) end it "should load the box configuration for other formats for a box" do register_provider("foo", nil, box_format: "bar") environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" end VF env.box3("base", "1.0", :bar, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 100 end VF end env = environment.create_vagrant_env machine = env.machine(:default, :foo) expect(machine.config.ssh.port).to eq(100) end it "prefer sooner formats when multiple box formats are available" do register_provider("foo", nil, box_format: ["fA", "fB"]) environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" end VF env.box3("base", "1.0", :fA, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 100 end VF env.box3("base", "1.0", :fB, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 200 end VF end env = environment.create_vagrant_env machine = env.machine(:default, :foo) expect(machine.config.ssh.port).to eq(100) end it "should load the proper version of a box" do register_provider("foo") environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" config.vm.box_version = "~> 1.2" end VF env.box3("base", "1.0", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 100 end VF env.box3("base", "1.5", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 200 end VF end env = environment.create_vagrant_env machine = env.machine(:default, :foo) expect(machine.config.ssh.port).to eq(200) end it "should load the provider override if set" do register_provider("bar") register_provider("foo") isolated_env = isolated_environment do |e| e.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "foo" config.vm.provider :foo do |_, c| c.vm.box = "bar" end end VF end env = isolated_env.create_vagrant_env foo_vm = env.machine(:default, :foo) bar_vm = env.machine(:default, :bar) expect(foo_vm.config.vm.box).to eq("bar") expect(bar_vm.config.vm.box).to eq("foo") end it "should reload the cache if refresh is set" do # Create a provider foo_provider = register_provider("foo") # Create the configuration isolated_env = isolated_environment do |e| e.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" end VF e.box3("base", "1.0", :foo) end env = isolated_env.create_vagrant_env vm1 = env.machine(:default, :foo) vm2 = env.machine(:default, :foo, true) vm3 = env.machine(:default, :foo) expect(vm1).not_to eql(vm2) expect(vm2).to eql(vm3) end it "should raise an error if the VM is not found" do expect { instance.machine("i-definitely-dont-exist", :virtualbox) }. to raise_error(Vagrant::Errors::MachineNotFound) end it "should raise an error if the provider is not found" do expect { instance.machine(:default, :lol_no) }. to raise_error(Vagrant::Errors::ProviderNotFound) end end describe "#machine_index" do it "returns a machine index" do expect(subject.machine_index).to be_kind_of(Vagrant::MachineIndex) end it "caches the result" do result = subject.machine_index expect(subject.machine_index).to equal(result) end it "uses a directory within the home directory by default" do klass = double("machine_index") stub_const("Vagrant::MachineIndex", klass) # Need to stub the mappers here since the Vagrant::MachineIndex module is getting # stubbed as a double. This is causing the mapper definition to fail on account # of the Input to the mapper not being a module. The mapper has no impact on this # test otherwise. stub_mapper = Class.new {} stub_const("VagrantPlugins::CommandServe::Mappers", stub_mapper) expect(klass).to receive(:new).with(any_args) do |path| expect(path.to_s.start_with?(subject.home_path.to_s)).to be(true) true end subject.machine_index end end describe "active machines" do it "should be empty if there is no root path" do Dir.mktmpdir("vagrant-test-env-no-root-path") do |temp_dir| instance = described_class.new(cwd: temp_dir) expect(instance.active_machines).to be_empty end end it "should be empty if the machines folder doesn't exist" do folder = instance.local_data_path.join("machines") expect(folder).not_to be_exist expect(instance.active_machines).to be_empty end it "should return the name and provider of active machines" do machines = instance.local_data_path.join("machines") # Valid machine, with "foo" and virtualbox machine_foo = machines.join("foo/virtualbox") machine_foo.mkpath machine_foo.join("id").open("w+") { |f| f.write("") } # Invalid machine (no ID) machine_bar = machines.join("bar/virtualbox") machine_bar.mkpath expect(instance.active_machines).to eq([[:foo, :virtualbox]]) end end describe "batching" do let(:batch) do double("batch") do |b| allow(b).to receive(:run) end end context "without the disabling env var" do it "should run without disabling parallelization" do with_temp_env("VAGRANT_NO_PARALLEL" => nil) do expect(Vagrant::BatchAction).to receive(:new).with(true).and_return(batch) expect(batch).to receive(:run) instance.batch {} end end it "should run with disabling parallelization if explicit" do with_temp_env("VAGRANT_NO_PARALLEL" => nil) do expect(Vagrant::BatchAction).to receive(:new).with(false).and_return(batch) expect(batch).to receive(:run) instance.batch(false) {} end end end context "with the disabling env var" do it "should run with disabling parallelization" do with_temp_env("VAGRANT_NO_PARALLEL" => "yes") do expect(Vagrant::BatchAction).to receive(:new).with(false).and_return(batch) expect(batch).to receive(:run) instance.batch {} end end end end describe "current working directory" do it "is the cwd by default" do Dir.mktmpdir("vagrant-test-env-cwd-default") do |temp_dir| Dir.chdir(temp_dir) do with_temp_env("VAGRANT_CWD" => nil) do expect(described_class.new.cwd).to eq(Pathname.new(Dir.pwd)) end end end end it "is set to the cwd given" do Dir.mktmpdir("vagrant-test-env-set-cwd") do |directory| instance = described_class.new(cwd: directory) expect(instance.cwd).to eq(Pathname.new(directory)) end end it "is set to the environmental variable VAGRANT_CWD" do Dir.mktmpdir("vagrant-test-env-set-vagrant-cwd") do |directory| instance = with_temp_env("VAGRANT_CWD" => directory) do described_class.new end expect(instance.cwd).to eq(Pathname.new(directory)) end end it "raises an exception if the CWD doesn't exist" do expect { described_class.new(cwd: "doesntexist") }. to raise_error(Vagrant::Errors::EnvironmentNonExistentCWD) end end describe "default provider" do let(:plugin_providers) { {} } before do m = Vagrant.plugin("2").manager allow(m).to receive(:providers).and_return(plugin_providers) allow_any_instance_of(described_class).to receive(:process_configured_plugins) end it "is the highest matching usable provider" do plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 2 }] plugin_providers[:boom] = [provider_usable_class(true), { priority: 3 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider).to eq(:bar) end end it "is the highest matching usable provider that is defaultable" do plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [ provider_usable_class(true), { defaultable: false, priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 2 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider).to eq(:foo) end end it "is the highest matching usable provider that isn't excluded" do plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 2 }] plugin_providers[:boom] = [provider_usable_class(true), { priority: 3 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider(exclude: [:bar, :foo])).to eq(:boom) end end it "is the default provider set if usable" do plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 2 }] plugin_providers[:boom] = [provider_usable_class(true), { priority: 3 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => "baz", "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider).to eq(:baz) end end it "is the default provider set even if unusable" do plugin_providers[:baz] = [provider_usable_class(false), { priority: 5 }] plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => "baz", "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider).to eq(:baz) end end it "is the usable despite default if not forced" do plugin_providers[:baz] = [provider_usable_class(false), { priority: 5 }] plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => "baz", "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider(force_default: false)).to eq(:bar) end end it "prefers the default even if not forced" do plugin_providers[:baz] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => "baz", "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider(force_default: false)).to eq(:baz) end end it "uses the first usable provider that isn't the default if excluded" do plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 8 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => "baz", "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider( exclude: [:baz], force_default: false)).to eq(:bar) end end it "raise an error if nothing else is usable" do plugin_providers[:foo] = [provider_usable_class(false), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(false), { priority: 5 }] plugin_providers[:baz] = [provider_usable_class(false), { priority: 5 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect { subject.default_provider }.to raise_error( Vagrant::Errors::NoDefaultProvider) end end it "is the provider in the Vagrantfile that is preferred and usable" do subject.vagrantfile.config.vm.provider "foo" subject.vagrantfile.config.vm.provider "bar" subject.vagrantfile.config.vm.finalize! plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 2 }] plugin_providers[:boom] = [provider_usable_class(true), { priority: 3 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => 'baz,bar') do expect(subject.default_provider).to eq(:bar) end end it "is the provider in the Vagrantfile that is usable" do subject.vagrantfile.config.vm.provider "foo" subject.vagrantfile.config.vm.provider "bar" subject.vagrantfile.config.vm.finalize! plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 2 }] plugin_providers[:boom] = [provider_usable_class(true), { priority: 3 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider).to eq(:foo) end end it "is the provider in the Vagrantfile that is usable even if only one specified (1)" do subject.vagrantfile.config.vm.provider "foo" subject.vagrantfile.config.vm.finalize! plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider).to eq(:foo) end end it "is the provider in the Vagrantfile that is usable even if only one specified (2)" do subject.vagrantfile.config.vm.provider "bar" subject.vagrantfile.config.vm.finalize! plugin_providers[:foo] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 5 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider).to eq(:bar) end end it "is the preferred usable provider outside the Vagrantfile" do subject.vagrantfile.config.vm.provider "foo" subject.vagrantfile.config.vm.finalize! plugin_providers[:foo] = [provider_usable_class(false), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 2 }] plugin_providers[:boom] = [provider_usable_class(true), { priority: 3 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => 'boom,baz') do expect(subject.default_provider).to eq(:boom) end end it "is the highest usable provider outside the Vagrantfile" do subject.vagrantfile.config.vm.provider "foo" subject.vagrantfile.config.vm.finalize! plugin_providers[:foo] = [provider_usable_class(false), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 2 }] plugin_providers[:boom] = [provider_usable_class(true), { priority: 3 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider).to eq(:bar) end end it "is the provider in the Vagrantfile that is usable for a machine" do subject.vagrantfile.config.vm.provider "foo" subject.vagrantfile.config.vm.define "sub" do |v| v.vm.provider "bar" end subject.vagrantfile.config.vm.finalize! plugin_providers[:foo] = [provider_usable_class(true), { priority: 5 }] plugin_providers[:bar] = [provider_usable_class(true), { priority: 7 }] plugin_providers[:baz] = [provider_usable_class(true), { priority: 2 }] plugin_providers[:boom] = [provider_usable_class(true), { priority: 3 }] with_temp_env("VAGRANT_DEFAULT_PROVIDER" => nil, "VAGRANT_PREFERRED_PROVIDERS" => nil) do expect(subject.default_provider(machine: :sub)).to eq(:bar) end end end describe "local data path" do it "is set to the proper default" do default = instance.root_path.join(described_class::DEFAULT_LOCAL_DATA) expect(instance.local_data_path).to eq(default) end it "is expanded relative to the cwd" do Dir.mktmpdir("vagrant-test-env-relative-cwd") do |temp_dir| Dir.chdir(temp_dir) do instance = described_class.new(local_data_path: "foo") expect(instance.local_data_path).to eq(instance.cwd.join("foo")) end end end it "is set to the given value" do Dir.mktmpdir("vagrant-test-env-set-given") do |dir| instance = described_class.new(local_data_path: dir) expect(instance.local_data_path.to_s).to eq(dir) end end context "with environmental variable VAGRANT_DOTFILE_PATH set to the empty string" do it "is set to the default, from the work directory" do with_temp_env("VAGRANT_DOTFILE_PATH" => "") do instance = env.create_vagrant_env expect(instance.cwd).to eq(env.workdir) expect(instance.local_data_path.to_s).to eq(File.join(env.workdir, ".vagrant")) end end it "is set to the default, from a sub-directory of the work directory" do Dir.mktmpdir("sub-directory", env.workdir) do |temp_dir| with_temp_env("VAGRANT_DOTFILE_PATH" => "") do instance = env.create_vagrant_env(cwd: temp_dir) expect(instance.cwd.to_s).to eq(temp_dir) expect(instance.local_data_path.to_s).to eq(File.join(env.workdir, ".vagrant")) end end end end context "with environmental variable VAGRANT_DOTFILE_PATH set to an absolute path" do it "is set to VAGRANT_DOTFILE_PATH from the work directory" do Dir.mktmpdir("sub-directory", env.workdir) do |temp_dir| dotfile_path = File.join(temp_dir, ".vagrant-custom") with_temp_env("VAGRANT_DOTFILE_PATH" => dotfile_path) do instance = env.create_vagrant_env expect(instance.cwd).to eq(env.workdir) expect(instance.local_data_path.to_s).to eq(dotfile_path) end end end it "is set to VAGRANT_DOTFILE_PATH from a sub-directory of the work directory" do Dir.mktmpdir("sub-directory", env.workdir) do |temp_dir| dotfile_path = File.join(temp_dir, ".vagrant-custom") with_temp_env("VAGRANT_DOTFILE_PATH" => dotfile_path) do instance = env.create_vagrant_env(cwd: temp_dir) expect(instance.cwd.to_s).to eq(temp_dir) expect(instance.local_data_path.to_s).to eq(dotfile_path) end end end end context "with environmental variable VAGRANT_DOTFILE_PATH set to a relative path" do it "is set relative to the the work directory, from the work directory" do Dir.mktmpdir("sub-directory", env.workdir) do |temp_dir| with_temp_env("VAGRANT_DOTFILE_PATH" => ".vagrant-custom") do instance = env.create_vagrant_env expect(instance.cwd).to eq(env.workdir) expect(instance.local_data_path.to_s).to eq(File.join(env.workdir, ".vagrant-custom")) end end end it "is set relative to the the work directory, from a sub-directory of the work directory" do Dir.mktmpdir("sub-directory", env.workdir) do |temp_dir| with_temp_env("VAGRANT_DOTFILE_PATH" => ".vagrant-custom") do instance = env.create_vagrant_env(cwd: temp_dir) expect(instance.cwd.to_s).to eq(temp_dir) expect(instance.local_data_path.to_s).to eq(File.join(env.workdir, ".vagrant-custom")) end end end it "is set to the empty string when there is no valid work directory" do Dir.mktmpdir("out-of-tree-directory") do |temp_dir| with_temp_env("VAGRANT_DOTFILE_PATH" => ".vagrant-custom") do instance = env.create_vagrant_env(cwd: temp_dir) expect(instance.cwd.to_s).to eq(temp_dir) expect(instance.local_data_path.to_s).to eq("") end end end end context "with environmental variable VAGRANT_DOTFILE_PATH set with tilde" do it "is set relative to the user's home directory" do with_temp_env("VAGRANT_DOTFILE_PATH" => "~/.vagrant") do instance = env.create_vagrant_env expect(instance.cwd).to eq(env.workdir) expect(instance.local_data_path.to_s).to eq(File.join(Dir.home, ".vagrant")) end end end describe "upgrading V1 dotfiles" do let(:v1_dotfile_tempfile) do Tempfile.new("vagrant-upgrade-dotfile").tap do |f| f.close end end let(:v1_dotfile) { Pathname.new(v1_dotfile_tempfile.path) } let(:local_data_path) { v1_dotfile_tempfile.path } let(:instance) { described_class.new(local_data_path: local_data_path) } after do FileUtils.rm_rf(local_data_path) end it "should be fine if dotfile is empty" do v1_dotfile.open("w+") do |f| f.write("") end expect { instance }.to_not raise_error end it "should upgrade all active VMs" do active_vms = { "foo" => "foo_id", "bar" => "bar_id" } v1_dotfile.open("w+") do |f| f.write(JSON.dump({ "active" => active_vms })) end expect { instance }.to_not raise_error local_data_pathname = Pathname.new(local_data_path) foo_id_file = local_data_pathname.join("machines/foo/virtualbox/id") expect(foo_id_file).to be_file expect(foo_id_file.read).to eq("foo_id") bar_id_file = local_data_pathname.join("machines/bar/virtualbox/id") expect(bar_id_file).to be_file expect(bar_id_file.read).to eq("bar_id") end it "should raise an error if invalid JSON" do v1_dotfile.open("w+") do |f| f.write("bad") end expect { instance }. to raise_error(Vagrant::Errors::DotfileUpgradeJSONError) end end end describe "copying the private SSH key" do it "copies the SSH key into the home directory" do env = isolated_environment instance = described_class.new(home_path: env.homedir) pk = env.homedir.join("insecure_private_key") expect(pk).to be_exist if !Vagrant::Util::Platform.windows? expect(Vagrant::Util::FileMode.from_octal(pk.stat.mode)).to eq("600") end end end it "has a box collection pointed to the proper directory" do collection = instance.boxes expect(collection).to be_kind_of(Vagrant::BoxCollection) expect(collection.directory).to eq(instance.boxes_path) # Reach into some internal state here but not sure how else # to test this at the moment. expect(collection.instance_variable_get(:@hook)). to eq(instance.method(:hook)) end describe "action runner" do it "has an action runner" do expect(instance.action_runner).to be_kind_of(Vagrant::Action::Runner) end it "has a `ui` in the globals" do result = nil callable = lambda { |env| result = env[:ui] } instance.action_runner.run(callable) expect(result).to eql(instance.ui) end end describe "#pushes" do it "returns the pushes from the Vagrantfile config" do environment = isolated_environment do |env| env.vagrantfile(<<-VF.gsub(/^ {10}/, '')) Vagrant.configure("2") do |config| config.push.define "noop" end VF end env = environment.create_vagrant_env expect(env.pushes).to eq([:noop]) end end describe "#push" do let(:push_class) do Class.new(Vagrant.plugin("2", :push)) do def self.pushed? !!class_variable_get(:@@pushed) end def push self.class.class_variable_set(:@@pushed, true) end end end it "raises an exception when the push does not exist" do expect { instance.push("lolwatbacon") } .to raise_error(Vagrant::Errors::PushStrategyNotDefined) end it "raises an exception if the strategy does not exist" do environment = isolated_environment do |env| env.vagrantfile(<<-VF.gsub(/^ {10}/, '')) Vagrant.configure("2") do |config| config.push.define "lolwatbacon" end VF end env = environment.create_vagrant_env expect { env.push("lolwatbacon") } .to raise_error(Vagrant::Errors::PushStrategyNotLoaded) end it "executes the push action" do register_plugin("2") do |plugin| plugin.name "foo" plugin.push(:foo) do push_class end end environment = isolated_environment do |env| env.vagrantfile(<<-VF.gsub(/^ {10}/, '')) Vagrant.configure("2") do |config| config.push.define "foo" end VF end env = environment.create_vagrant_env env.push("foo") expect(push_class.pushed?).to be(true) end end describe "#hook" do it "should call the action runner with the proper hook" do hook_name = :foo expect(instance.action_runner).to receive(:run).with(any_args) { |callable, env| expect(env[:action_name]).to eq(hook_name) } instance.hook(hook_name) end it "should return the result of the action runner run" do expect(instance.action_runner).to receive(:run).and_return(:foo) expect(instance.hook(:bar)).to eq(:foo) end it "should allow passing in a custom action runner" do expect(instance.action_runner).not_to receive(:run) other_runner = double("runner") expect(other_runner).to receive(:run).and_return(:foo) expect(instance.hook(:bar, runner: other_runner)).to eq(:foo) end it "should allow passing in custom data" do expect(instance.action_runner).to receive(:run).with(any_args) { |callable, env| expect(env[:foo]).to eq(:bar) } instance.hook(:foo, foo: :bar) end it "should allow passing a custom callable" do expect(instance.action_runner).to receive(:run).with(any_args) { |callable, env| expect(callable).to eq(:what) } instance.hook(:foo, callable: :what) end end describe "primary machine name" do it "should be the only machine if not a multi-machine environment" do expect(instance.primary_machine_name).to eq(instance.machine_names.first) end it "should be the machine marked as the primary" do environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" config.vm.define :foo config.vm.define :bar, primary: true end VF env.box3("base", "1.0", :virtualbox) end env = environment.create_vagrant_env expect(env.primary_machine_name).to eq(:bar) end it "should be nil if no primary is specified in a multi-machine environment" do environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.box = "base" config.vm.define :foo config.vm.define :bar end VF env.box3("base", "1.0", :virtualbox) end env = environment.create_vagrant_env expect(env.primary_machine_name).to be_nil end end describe "loading configuration" do it "should load global configuration" do environment = isolated_environment do |env| env.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.ssh.port = 200 end VF end env = environment.create_vagrant_env expect(env.vagrantfile.config.ssh.port).to eq(200) end it "should load from a custom Vagrantfile" do environment = isolated_environment do |env| env.file("non_standard_name", <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 200 end VF end env = environment.create_vagrant_env(vagrantfile_name: "non_standard_name") expect(env.vagrantfile.config.ssh.port).to eq(200) end it "should load from a custom Vagrantfile specified by env var" do environment = isolated_environment do |env| env.file("some_other_name", <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 400 end VF end env = with_temp_env("VAGRANT_VAGRANTFILE" => "some_other_name") do environment.create_vagrant_env end expect(env.vagrantfile.config.ssh.port).to eq(400) end end describe "ui" do it "should be a silent UI by default" do expect(described_class.new.ui).to be_kind_of(Vagrant::UI::Silent) end it "should be a UI given in the constructor" do # Create a custom UI for our test class CustomUI < Vagrant::UI::Interface; end instance = described_class.new(ui_class: CustomUI) expect(instance.ui).to be_kind_of(CustomUI) end end describe "#unload" do it "should run the unload hook" do expect(instance).to receive(:hook).with(:environment_unload).once instance.unload end end describe "getting machine names" do it "should return the default machine if no multi-VM is used" do # Create the config isolated_env = isolated_environment do |e| e.vagrantfile(<<-VF) Vagrant.configure("2") do |config| end VF end env = isolated_env.create_vagrant_env expect(env.machine_names).to eq([:default]) end it "should return the machine names in order" do # Create the config isolated_env = isolated_environment do |e| e.vagrantfile(<<-VF) Vagrant.configure("2") do |config| config.vm.define "foo" config.vm.define "bar" end VF end env = isolated_env.create_vagrant_env expect(env.machine_names).to eq([:foo, :bar]) end end describe "guess_provider" do before { allow_any_instance_of(described_class).to receive(:process_configured_plugins) } it "should return the default provider by default" do expect(subject).to receive(:default_provider).and_return("default_provider") expect(subject.send(:guess_provider)).to eq("default_provider") end context "when provider is defined via command line argument" do before { stub_const("ARGV", argv) } context "when provider is given as single argument" do let(:argv) { ["--provider=single_arg"] } it "should return the provider name" do expect(subject.send(:guess_provider)).to eq(:single_arg) end end context "when provider is given as two arguments" do let(:argv) { ["--provider", "double_arg"] } it "should return the provider name" do expect(subject.send(:guess_provider)).to eq(:double_arg) end end end context "when no default provider is available" do before { expect(subject).to receive(:default_provider). and_raise(Vagrant::Errors::NoDefaultProvider) } it "should return a nil value" do expect(subject.send(:guess_provider)).to be_nil end end end describe "#find_configured_plugins" do before do allow_any_instance_of(described_class).to receive(:guess_provider).and_return(:dummy) allow_any_instance_of(described_class).to receive(:process_configured_plugins) end it "should find no plugins when no plugins are configured" do expect(subject.send(:find_configured_plugins)).to be_empty end context "when plugins are defined in the Vagrantfile" do before do env.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vagrant.plugins = "vagrant-plugin" end VF end it "should return the vagrant-plugin" do expect(subject.send(:find_configured_plugins).keys).to include("vagrant-plugin") end end context "when plugins are defined in the Vagrantfile of a box" do before do env.box3("foo", "1.0", :dummy, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.vagrant.plugins = "vagrant-plugin" end VF env.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vm.box = "foo" end VF end it "should return the vagrant-plugin" do expect(subject.send(:find_configured_plugins).keys).to include("vagrant-plugin") end end context "when the box does not match the provider" do before do env.box3("foo", "1.0", :other, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.vagrant.plugins = "vagrant-plugin" end VF env.vagrantfile <<-VF Vagrant.configure("2") do |config| config.vm.box = "foo" end VF end it "should not return the vagrant-plugin" do expect(subject.send(:find_configured_plugins).keys).not_to include("vagrant-plugin") end end end describe "#process_configured_plugins" do let(:env) do isolated_environment.tap do |e| e.box3("base", "1.0", :virtualbox) e.vagrantfile(vagrantfile) end end let(:vagrantfile) do 'Vagrant.configure("2"){ |config| config.vm.box = "base" }' end let(:plugin_manager) { double("plugin_manager", installed_plugins: installed_plugins, local_file: local_file) } let(:installed_plugins) { {} } let(:local_file) { double("local_file", installed_plugins: local_installed_plugins) } let(:local_installed_plugins) { {} } before do allow(Vagrant::Plugin::Manager).to receive(:instance).and_return(plugin_manager) allow(plugin_manager).to receive(:globalize!) allow(plugin_manager).to receive(:localize!) allow(plugin_manager).to receive(:load_plugins) end context "when local data directory does not exist" do let(:local_file) { nil } it "should properly return empty result" do expect(instance.send(:process_configured_plugins)).to be_empty end end context "plugins are disabled" do before{ allow(Vagrant).to receive(:plugins_enabled?).and_return(false) } it "should return empty result" do expect(instance.send(:process_configured_plugins)).to be_nil end end context "when vagrant is invalid" do let(:vagrantfile) { 'Vagrant.configure("2"){ |config| config.vagrant.bad_key = true }' } it "should raise a configuration error" do expect { instance.send(:process_configured_plugins) }.to raise_error(Vagrant::Errors::ConfigInvalid) end end context "with local plugins defined" do let(:vagrantfile) { 'Vagrant.configure("2"){ |config| config.vagrant.plugins = "vagrant" }' } let(:installed_plugins) { {"vagrant" => true} } context "with plugin already installed" do it "should not attempt to install a plugin" do expect(plugin_manager).not_to receive(:install_plugin) expect(instance.send(:process_configured_plugins)).to eq(local_installed_plugins) end end context "without plugin installed" do before { allow(instance).to receive(:exit) } it "should prompt user before installation" do expect(instance.ui).to receive(:ask).and_return("n") expect(plugin_manager).to receive(:installed_plugins).and_return({}) expect { instance.send(:process_configured_plugins) }.to raise_error(Vagrant::Errors::PluginMissingLocalError) end it "should install plugin" do expect(instance.ui).to receive(:ask).and_return("y") expect(plugin_manager).to receive(:installed_plugins).and_return({}) expect(plugin_manager).to receive(:install_plugin).and_return(double("spec", "name" => "vagrant", "version" => "1")) instance.send(:process_configured_plugins) end it "should exit after install" do expect(instance.ui).to receive(:ask).and_return("y") expect(plugin_manager).to receive(:installed_plugins).and_return({}) expect(plugin_manager).to receive(:install_plugin).and_return(double("spec", "name" => "vagrant", "version" => "1")) expect(instance).to receive(:exit) instance.send(:process_configured_plugins) end end end end describe "#setup_local_data_path" do before do allow(FileUtils).to receive(:mkdir_p).and_call_original allow(FileUtils).to receive(:cp).and_call_original end it "should create an rgloader path" do expect(FileUtils).to receive(:mkdir_p).with(/(?!home)rgloader/) instance end it "should write the rgloader file" do expect(FileUtils).to receive(:cp).with(anything, /(?!home)rgloader.*rb$/) instance end end end ================================================ FILE: test/unit/vagrant/errors_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) describe Vagrant::Errors::VagrantError do describe "subclass with error key" do let(:klass) do Class.new(described_class) do error_key("test_key") end end subject { klass.new } it "should use the translation for the message" do expect(subject.to_s).to eq("test value") end describe '#status_code' do subject { super().status_code } it { should eq(1) } end end describe "passing error key through options" do subject { described_class.new(_key: "test_key") } it "should use the translation for the message" do expect(subject.to_s).to eq("test value") end end describe "subclass with error message" do let(:klass) do Class.new(described_class) do error_message("foo") end end subject { klass.new(data: "yep") } it "should use the translation for the message" do expect(subject.to_s).to eq("foo") end it "should expose translation keys to the user" do expect(subject.extra_data.length).to eql(1) expect(subject.extra_data).to have_key(:data) expect(subject.extra_data[:data]).to eql("yep") end it "should use a symbol initializer as a key" do subject = klass.new(:test_key) expect(subject.extra_data).to be_empty expect(subject.to_s).to eql("test value") end end end ================================================ FILE: test/unit/vagrant/guest_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require File.expand_path("../../base", __FILE__) describe Vagrant::Guest do include_context "capability_helpers" let(:capabilities) { {} } let(:guests) { {} } let(:machine) do double("machine").tap do |m| allow(m).to receive(:inspect).and_return("machine") allow(m).to receive(:config).and_return(double("config")) allow(m.config).to receive(:vm).and_return(double("vm_config")) allow(m.config.vm).to receive(:guest).and_return(nil) end end subject { described_class.new(machine, guests, capabilities) } describe "#capability" do before(:each) do guests[:foo] = [detect_class(true), nil] capabilities[:foo] = { foo_cap: cap_instance(:foo_cap), corrupt_cap: cap_instance(:corrupt_cap, corrupt: true), } subject.detect! end it "executes existing capabilities" do expect { subject.capability(:foo_cap) }. to raise_error(RuntimeError, "cap: foo_cap [machine]") end it "raises user-friendly error for non-existing caps" do expect { subject.capability(:what_cap) }. to raise_error(Vagrant::Errors::GuestCapabilityNotFound) end it "raises a user-friendly error for corrupt caps" do expect { subject.capability(:corrupt_cap) }. to raise_error(Vagrant::Errors::GuestCapabilityInvalid) end end describe "#detect!" do it "auto-detects if no explicit guest name given" do allow(machine.config.vm).to receive(:guest).and_return(nil) expect(subject).to receive(:initialize_capabilities!). with(nil, guests, capabilities, machine) subject.detect! end it "uses the explicit guest name if specified" do allow(machine.config.vm).to receive(:guest).and_return(:foo) expect(subject).to receive(:initialize_capabilities!). with(:foo, guests, capabilities, machine) subject.detect! end it "raises a user-friendly error if specified guest doesn't exist" do allow(machine.config.vm).to receive(:guest).and_return(:foo) expect { subject.detect! }. to raise_error(Vagrant::Errors::GuestExplicitNotDetected) end it "raises a user-friendly error if auto-detected guest not found" do expect { subject.detect! }. to raise_error(Vagrant::Errors::GuestNotDetected) end end describe "#name" do it "should be the name of the detected guest" do guests[:foo] = [detect_class(true), nil] guests[:bar] = [detect_class(false), nil] subject.detect! expect(subject.name).to eql(:foo) end end describe "#ready?" do before(:each) do guests[:foo] = [detect_class(true), nil] end it "should not be ready by default" do expect(subject.ready?).not_to be end it "should be ready after detecting" do subject.detect! expect(subject.ready?).to be end end end ================================================ FILE: test/unit/vagrant/host_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require File.expand_path("../../base", __FILE__) describe Vagrant::Host do include_context "capability_helpers" let(:capabilities) { {} } let(:hosts) { {} } let(:env) { Object.new } it "initializes the capabilities" do expect_any_instance_of(described_class).to receive(:initialize_capabilities!). with(:foo, hosts, capabilities, env) described_class.new(:foo, hosts, capabilities, env) end end ================================================ FILE: test/unit/vagrant/machine_index_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" require "pathname" require "tempfile" require File.expand_path("../../base", __FILE__) require "vagrant/machine_index" describe Vagrant::MachineIndex do include_context "unit" let(:data_dir) { Pathname.new(Dir.mktmpdir("vagrant-test-machine-index-data-dir")) } let(:entry_klass) { Vagrant::MachineIndex::Entry } let(:new_entry) do entry_klass.new.tap do |e| e.name = "foo" e.vagrantfile_path = "/bar" end end subject { described_class.new(data_dir) } after do FileUtils.rm_rf(data_dir) end it "raises an exception if the data file is corrupt" do data_dir.join("index").open("w") do |f| f.write(JSON.dump({})) end expect { subject }. to raise_error(Vagrant::Errors::CorruptMachineIndex) end it "raises an exception if the JSON is invalid" do data_dir.join("index").open("w") do |f| f.write("foo") end expect { subject }. to raise_error(Vagrant::Errors::CorruptMachineIndex) end describe "#each" do before do 5.times do |i| e = entry_klass.new e.name = "entry-#{i}" e.vagrantfile_path = "/foo" subject.release(subject.set(e)) end end it "should iterate over all the elements" do items = [] subject = described_class.new(data_dir) subject.each do |entry| items << entry.name end items.sort! expect(items).to eq([ "entry-0", "entry-1", "entry-2", "entry-3", "entry-4", ]) end end describe "#get and #release" do before do data = { "version" => 1, "machines" => { "bar" => { "name" => "default", "provider" => "vmware", "local_data_path" => "/foo", "vagrantfile_path" => "/foo/bar/baz", "state" => "running", "updated_at" => "foo", }, "baz" => { "name" => "default", "provider" => "vmware", "vagrantfile_path" => "/foo/bar/baz", "state" => "running", "updated_at" => "foo", "extra_data" => { "foo" => "bar", }, }, } } data_dir.join("index").open("w") do |f| f.write(JSON.dump(data)) end end it "returns nil if the machine doesn't exist" do expect(subject.get("foo")).to be_nil expect(subject.get(nil)).to be_nil end it "returns nil if the machine doesn't exist (is an empty string)" do expect(subject.get("")).to be_nil expect(subject.get(nil)).to be_nil end it "returns a valid entry if the machine exists" do result = subject.get("bar") expect(result.id).to eq("bar") expect(result.name).to eq("default") expect(result.provider).to eq("vmware") expect(result.local_data_path).to eq(Pathname.new("/foo")) expect(result.vagrantfile_path).to eq(Pathname.new("/foo/bar/baz")) expect(result.state).to eq("running") expect(result.updated_at).to eq("foo") expect(result.extra_data).to eq({}) end it "returns a valid entry with extra data" do result = subject.get("baz") expect(result.id).to eq("baz") expect(result.extra_data).to eq({ "foo" => "bar", }) end it "returns a valid entry by unique prefix" do result = subject.get("b") expect(result).to_not be_nil expect(result.id).to eq("bar") end it "should include? by prefix" do expect(subject.include?("b")).to be(true) end it "should return false if given nil input" do expect(subject.include?(nil)).to be(false) end it "locks the entry so subsequent gets fail" do result = subject.get("bar") expect(result).to_not be_nil expect { subject.get("bar") }. to raise_error(Vagrant::Errors::MachineLocked) end it "can unlock a machine" do result = subject.get("bar") expect(result).to_not be_nil subject.release(result) result = subject.get("bar") expect(result).to_not be_nil end end describe "#include" do it "should not include non-existent things" do expect(subject.include?("foo")).to be(false) end it "should include created entries" do result = subject.set(new_entry) expect(result.id).to_not be_empty subject.release(result) subject = described_class.new(data_dir) expect(subject.include?(result.id)).to be(true) end end describe "#set and #get and #delete" do it "adds a new entry" do result = subject.set(new_entry) expect(result.id).to_not be_empty # It should be locked expect { subject.get(result.id) }. to raise_error(Vagrant::Errors::MachineLocked) # Get it froma new class and check the results subject.release(result) subject = described_class.new(data_dir) entry = subject.get(result.id) expect(entry).to_not be_nil expect(entry.name).to eq("foo") # TODO: test that updated_at is set end it "can delete an entry" do result = subject.set(new_entry) expect(result.id).to_not be_empty subject.delete(result) # Get it from a new class and check the results subject = described_class.new(data_dir) entry = subject.get(result.id) expect(entry).to be_nil end it "can delete an entry that doesn't exist" do e = entry_klass.new expect(subject.delete(e)).to be(true) end it "updates an existing entry" do entry = entry_klass.new entry.name = "foo" entry.vagrantfile_path = "/bar" result = subject.set(entry) expect(result.id).to_not be_empty result.name = "bar" result.extra_data["foo"] = "bar" nextresult = subject.set(result) expect(nextresult.id).to eq(result.id) # Release it so we can test the contents subject.release(nextresult) # Get it froma new class and check the results subject = described_class.new(data_dir) entry = subject.get(result.id) expect(entry).to_not be_nil expect(entry.name).to eq("bar") expect(entry.extra_data).to eq({ "foo" => "bar", }) end it "updates an existing directory if the name, provider, and path are the same" do entry = entry_klass.new entry.name = "foo" entry.provider = "bar" entry.vagrantfile_path = "/bar" entry.state = "foo" result = subject.set(entry) expect(result.id).to_not be_empty # Release it so we can modify it subject.release(result) entry2 = entry_klass.new entry2.name = entry.name entry2.provider = entry.provider entry2.vagrantfile_path = entry.vagrantfile_path entry2.state = "bar" expect(entry2.id).to be_nil nextresult = subject.set(entry2) expect(nextresult.id).to eq(result.id) # Release it so we can test the contents subject.release(nextresult) # Get it from a new class and check the results subject = described_class.new(data_dir) entry = subject.get(result.id) expect(entry).to_not be_nil expect(entry.name).to eq(entry2.name) expect(entry.state).to eq(entry2.state) end end describe "#recover" do it "recovers an entry if not in the index" do result = subject.recover(new_entry) expect(result.id).to_not be_empty expect { subject.get(result.id) }.to raise_error(Vagrant::Errors::MachineLocked) end it "returns an entry if in the index" do test_entry = entry_klass.new() entry = subject.set(test_entry) subject.release(entry) new_test_entry = entry_klass.new(id=entry.id, {}) result = subject.recover(new_test_entry) expect(result.id).to eq(entry.id) end end end describe Vagrant::MachineIndex::Entry do include_context "unit" let(:env) { iso_env = isolated_environment iso_env.vagrantfile(vagrantfile) iso_env.create_vagrant_env } let(:vagrantfile) { "" } describe "#valid?" do let(:machine) { env.machine(:default, :dummy) } subject do described_class.new.tap do |e| e.name = "default" e.provider = "dummy" e.vagrantfile_path = env.root_path end end it "should be valid with a valid entry" do machine.id = "foo" expect(subject).to be_valid(env.home_path) end it "should be invalid if no Vagrantfile path is set" do subject.vagrantfile_path = nil expect(subject).to_not be_valid(env.home_path) end it "should be invalid if the Vagrantfile path does not exist" do subject.vagrantfile_path = Pathname.new("/i/should/not/exist") expect(subject).to_not be_valid(env.home_path) end it "should be invalid if the machine is inactive" do machine.id = nil expect(subject).to_not be_valid(env.home_path) end it "should be invalid if machine is not created" do machine.id = "foo" machine.provider.state = Vagrant::MachineState::NOT_CREATED_ID expect(subject).to_not be_valid(env.home_path) end context "with another active machine" do let(:vagrantfile) do <<-VF Vagrant.configure("2") do |config| config.vm.define "web" config.vm.define "db" end VF end it "should be invalid if the wrong machine is active only" do m = env.machine(:web, :dummy) m.id = "foo" subject.name = "db" expect(subject).to_not be_valid(env.home_path) end it "should be valid if the correct machine is active" do env.machine(:web, :dummy).id = "foo" env.machine(:db, :dummy).id = "foo" subject.name = "db" expect(subject).to be_valid(env.home_path) end end end end ================================================ FILE: test/unit/vagrant/machine_state_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require File.expand_path("../../base", __FILE__) describe Vagrant::MachineState do include_context "unit" let(:id) { :some_state } let(:short) { "foo" } let(:long) { "I am a longer foo" } it "should give access to the id" do instance = described_class.new(id, short, long) expect(instance.id).to eq(id) end it "should give access to the short description" do instance = described_class.new(id, short, long) expect(instance.short_description).to eq(short) end it "should give access to the long description" do instance = described_class.new(id, short, long) expect(instance.long_description).to eq(long) end end ================================================ FILE: test/unit/vagrant/machine_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "pathname" require "tmpdir" require File.expand_path("../../base", __FILE__) require "vagrant/util/platform" describe Vagrant::Machine do include_context "unit" let(:name) { "foo" } let(:provider) { new_provider_mock } let(:provider_cls) do obj = double("provider_cls") allow(obj).to receive(:new).and_return(provider) obj end let(:provider_config) { Object.new } let(:provider_name) { :test } let(:provider_options) { {} } let(:base) { false } let(:box) do double("box").tap do |b| allow(b).to receive(:name).and_return("foo") allow(b).to receive(:provider).and_return(:dummy) allow(b).to receive(:architecture) allow(b).to receive(:version).and_return("1.0") end end let(:config) { env.vagrantfile.config } let(:data_dir) { Pathname.new(Dir.mktmpdir("vagrant-machine-data-dir")) } let(:env) do # We need to create a Vagrantfile so that this test environment # has a proper root path test_env.vagrantfile("") # Create the Vagrant::Environment instance test_env.create_vagrant_env end let(:test_env) { isolated_environment } let(:instance) { new_instance } after do FileUtils.rm_rf(data_dir) if data_dir end subject { instance } def new_provider_mock double("provider").tap do |obj| allow(obj).to receive(:_initialize) .with(provider_name, anything).and_return(nil) allow(obj).to receive(:machine_id_changed).and_return(nil) allow(obj).to receive(:state).and_return(Vagrant::MachineState.new( :created, "", "")) end end # Returns a new instance with the test data def new_instance described_class.new(name, provider_name, provider_cls, provider_config, provider_options, config, data_dir, box, env, env.vagrantfile, base) end describe "initialization" do it "should set the ID to nil if the state is not created" do subject.id = "foo" allow(provider).to receive(:state).and_return(Vagrant::MachineState.new( Vagrant::MachineState::NOT_CREATED_ID, "short", "long")) subject = new_instance expect(subject.state.id).to eq(Vagrant::MachineState::NOT_CREATED_ID) expect(subject.id).to be_nil end context "setting up triggers" do before do expect(provider).to receive(:_initialize) do |*args| machine = args.last @trigger_instance = machine.instance_variable_get(:@triggers) true end end it "should initialize the trigger object" do subject = new_instance expect(subject.instance_variable_get(:@triggers)) .to be_a(Vagrant::Plugin::V2::Trigger) expect(subject.instance_variable_get(:@triggers)) .to eq(@trigger_instance) end end describe "as a base" do let(:base) { true} it "should not insert key" do subject = new_instance expect(subject.config.ssh.insert_key).to be(false) end end describe "communicator loading" do it "doesn't eager load SSH" do config.vm.communicator = :ssh klass = Vagrant.plugin("2").manager.communicators[:ssh] expect(klass).to_not receive(:new) subject end it "eager loads WinRM" do config.vm.communicator = :winrm klass = Vagrant.plugin("2").manager.communicators[:winrm] instance = double("instance") expect(klass).to receive(:new).and_return(instance) subject end end describe "provider initialization" do # This is a helper that generates a test for provider initialization. # This is a separate helper method because it takes a block that can # be used to have additional tests on the received machine. # # @yield [machine] Yields the machine that the provider initialization # method received so you can run additional tests on it. def provider_init_test(instance=nil) received_machine = nil if !instance instance = new_provider_mock end provider_cls = double("provider_cls") expect(provider_cls).to receive(:new) { |machine| # Store this for later so we can verify that it is the # one we expected to receive. received_machine = machine # Sanity check expect(machine).to be # Yield our machine if we want to do additional tests yield machine if block_given? true }.and_return(instance) # Initialize a new machine and verify that we properly receive # the machine we expect. instance = described_class.new(name, provider_name, provider_cls, provider_config, provider_options, config, data_dir, box, env, env.vagrantfile) expect(received_machine).to eql(instance) end it "should initialize with the machine object" do # Just run the blank test provider_init_test end it "should have the machine name setup" do provider_init_test do |machine| expect(machine.name).to eq(name) end end it "should have the machine configuration" do provider_init_test do |machine| expect(machine.config).to eql(config) end end it "should have the box" do provider_init_test do |machine| expect(machine.box).to eql(box) end end it "should have the environment" do provider_init_test do |machine| expect(machine.env).to eql(env) end end it "should have the vagrantfile" do provider_init_test do |machine| expect(machine.vagrantfile).to equal(env.vagrantfile) end end it "should have access to the ID" do # Stub this because #id= calls it. allow(provider).to receive(:machine_id_changed) # Set the ID on the previous instance so that it is persisted instance.id = "foo" provider_init_test do |machine| expect(machine.id).to eq("foo") end end it "should NOT have access to the provider" do provider_init_test do |machine| expect(machine.provider).to be_nil end end it "should initialize the capabilities" do instance = new_provider_mock expect(instance).to receive(:_initialize).with(any_args) { |p, m| expect(p).to eq(provider_name) expect(m.name).to eq(name) true } provider_init_test(instance) end end end describe "attributes" do describe '#name' do subject { super().name } it { should eq(name) } end describe '#config' do subject { super().config } it { should eql(config) } end describe '#box' do subject { super().box } it { should eql(box) } end describe '#env' do subject { super().env } it { should eql(env) } end describe '#provider' do subject { super().provider } it { should eql(provider) } end describe '#provider_config' do subject { super().provider_config } it { should eql(provider_config) } end describe '#provider_options' do subject { super().provider_options } it { should eq(provider_options) } end end describe "#action" do before do allow(Vagrant::Plugin::Manager.instance).to receive(:installed_plugins).and_return({}) end it "should be able to run an action that exists" do action_name = :destroy called = false callable = lambda { |_env| called = true } expect(provider).to receive(:action).with(action_name).and_return(callable) instance.action(action_name) expect(called).to be end it "should provide the machine in the environment" do action_name = :destroy machine = nil callable = lambda { |env| machine = env[:machine] } allow(provider).to receive(:action).with(action_name).and_return(callable) instance.action(action_name) expect(machine).to eql(instance) end it "should pass any extra options to the environment" do action_name = :destroy foo = nil callable = lambda { |env| foo = env[:foo] } allow(provider).to receive(:action).with(action_name).and_return(callable) instance.action(action_name, foo: :bar) expect(foo).to eq(:bar) end it "should pass any extra options to the environment as strings" do action_name = :destroy foo = nil callable = lambda { |env| foo = env["foo"] } allow(provider).to receive(:action).with(action_name).and_return(callable) instance.action(action_name, "foo" => :bar) expect(foo).to eq(:bar) end it "should return the environment as a result" do action_name = :destroy callable = lambda { |env| env[:result] = "FOO" } allow(provider).to receive(:action).with(action_name).and_return(callable) result = instance.action(action_name) expect(result[:result]).to eq("FOO") end it "should raise an exception if the action is not implemented" do action_name = :destroy allow(provider).to receive(:action).with(action_name).and_return(nil) expect { instance.action(action_name) }. to raise_error(Vagrant::Errors::UnimplementedProviderAction) end it 'should not warn if the machines cwd has not changed' do initial_action_name = :destroy second_action_name = :reload callable = lambda { |_env| } allow(provider).to receive(:action).with(initial_action_name).and_return(callable) allow(provider).to receive(:action).with(second_action_name).and_return(callable) allow(subject.ui).to receive(:warn) instance.action(initial_action_name) expect(subject.ui).to_not have_received(:warn) instance.action(second_action_name) expect(subject.ui).to_not have_received(:warn) end it 'should warn if the machine was last run under a different directory' do action_name = :destroy callable = lambda { |_env| } original_cwd = env.cwd.to_s allow(provider).to receive(:action).with(action_name).and_return(callable) allow(subject.ui).to receive(:warn) instance.action(action_name) expect(subject.ui).to_not have_received(:warn) # Whenever the machine is run on a different directory, the user is warned allow(env).to receive(:root_path).and_return('/a/new/path') instance.action(action_name) expect(subject.ui).to have_received(:warn) do |warn_msg| expect(warn_msg).to include(original_cwd) expect(warn_msg).to include('/a/new/path') end end it 'should not warn if dirs are same but different cases' do action_name = :destroy callable = lambda { |_env| } original_cwd = env.cwd.to_s allow(provider).to receive(:action).with(action_name).and_return(callable) allow(subject.ui).to receive(:warn) instance.action(action_name) expect(subject.ui).to_not have_received(:warn) # In cygwin or other windows shell, it might have a path like # c:/path and C:/path # which are the same. allow(env).to receive(:root_path).and_return(original_cwd.upcase) expect(subject.ui).to_not have_received(:warn) instance.action(action_name) end context "if in a subdir" do let (:data_dir) { env.cwd } it 'should not warn if vagrant is run in subdirectory' do action_name = :destroy callable = lambda { |_env| } original_cwd = env.cwd.to_s allow(provider).to receive(:action).with(action_name).and_return(callable) allow(subject.ui).to receive(:warn) instance.action(action_name) expect(subject.ui).to_not have_received(:warn) # mock out cwd to be subdir and ensure no warn is printed allow(env).to receive(:cwd).and_return("#{original_cwd}/a/new/path") instance.action(action_name) expect(subject.ui).to_not have_received(:warn) end end end describe "#action_raw" do let(:callable) {lambda { |e| e[:called] = true @env = e }} before do @env = {} end it "should run the callable with the proper env" do subject.action_raw(:foo, callable) expect(@env[:called]).to be(true) expect(@env[:action_name]).to eq(:machine_action_foo) expect(@env[:machine]).to equal(subject) expect(@env[:machine_action]).to eq(:foo) expect(@env[:ui]).to equal(subject.ui) end it "should return the environment as a result" do result = subject.action_raw(:foo, callable) expect(result).to equal(@env) end it "should merge in any extra env" do subject.action_raw(:bar, callable, foo: :bar) expect(@env[:called]).to be(true) expect(@env[:foo]).to eq(:bar) end end describe "#communicate" do it "should return the SSH communicator by default" do expect(subject.communicate). to be_kind_of(VagrantPlugins::CommunicatorSSH::Communicator) end it "should return the specified communicator if given" do subject.config.vm.communicator = :winrm expect(subject.communicate). to be_kind_of(VagrantPlugins::CommunicatorWinRM::Communicator) end it "should memoize the result" do obj = subject.communicate expect(subject.communicate).to equal(obj) end it "raises an exception if an invalid communicator is given" do subject.config.vm.communicator = :foo expect { subject.communicate }. to raise_error(Vagrant::Errors::CommunicatorNotFound) end end describe "guest implementation" do let(:communicator) do result = double("communicator") allow(result).to receive(:ready?).and_return(true) allow(result).to receive(:test).and_return(false) result end before(:each) do test_guest = Class.new(Vagrant.plugin("2", :guest)) do def detect?(machine) true end end register_plugin do |p| p.guest(:test) { test_guest } end allow(instance).to receive(:communicate).and_return(communicator) end it "should raise an exception if communication is not ready" do expect(communicator).to receive(:ready?).and_return(false) expect { instance.guest }. to raise_error(Vagrant::Errors::MachineGuestNotReady) end it "should return the configured guest" do result = instance.guest expect(result).to be_kind_of(Vagrant::Guest) expect(result).to be_ready expect(result.capability_host_chain[0][0]).to eql(:test) end end describe "setting the ID" do before(:each) do allow(provider).to receive(:machine_id_changed) end it "should not have an ID by default" do expect(instance.id).to be_nil end it "should set an ID" do instance.id = "bar" expect(instance.id).to eq("bar") end it "should notify the machine that the ID changed" do expect(provider).to receive(:machine_id_changed).once instance.id = "bar" end it "should persist the ID" do instance.id = "foo" expect(new_instance.id).to eq("foo") end it "should delete the ID" do instance.id = "foo" second = new_instance expect(second.id).to eq("foo") second.id = nil expect(second.id).to be_nil third = new_instance expect(third.id).to be_nil end it "should set the UID that created the machine" do instance.id = "foo" second = new_instance expect(second.uid).to eq(Process.uid.to_s) end it "should delete the UID when the id is nil" do instance.id = "foo" instance.id = nil second = new_instance expect(second.uid).to be_nil end end describe "#index_uuid" do before(:each) do allow(provider).to receive(:machine_id_changed) end it "should not have an index UUID by default" do expect(subject.index_uuid).to be_nil end it "is set one when setting an ID" do # Stub the message we want allow(provider).to receive(:state).and_return(Vagrant::MachineState.new( :preparing, "preparing", "preparing")) # Setup the box information box = double("box") allow(box).to receive(:name).and_return("foo") allow(box).to receive(:provider).and_return(:bar) allow(box).to receive(:architecture) allow(box).to receive(:version).and_return("1.2.3") subject.box = box subject.id = "foo" uuid = subject.index_uuid expect(uuid).to_not be_nil expect(new_instance.index_uuid).to eq(uuid) # Test the entry itself entry = env.machine_index.get(uuid) expect(entry.name).to eq(subject.name) expect(entry.provider).to eq(subject.provider_name.to_s) expect(entry.state).to eq("preparing") expect(entry.vagrantfile_path).to eq(env.root_path) expect(entry.vagrantfile_name).to eq(env.vagrantfile_name) expect(entry.extra_data["box"]).to eq({ "name" => box.name, "provider" => box.provider.to_s, "architecture" => nil, "version" => box.version, }) env.machine_index.release(entry) end it "deletes the UUID when setting to nil" do subject.id = "foo" uuid = subject.index_uuid subject.id = nil expect(subject.index_uuid).to be_nil expect(env.machine_index.get(uuid)).to be_nil end end describe "#reload" do before do allow(provider).to receive(:machine_id_changed) subject.id = "foo" end it "should read the ID" do expect(provider).to_not receive(:machine_id_changed) subject.reload expect(subject.id).to eq("foo") end it "should read the updated ID" do new_instance.id = "bar" expect(provider).to receive(:machine_id_changed) subject.reload expect(subject.id).to eq("bar") end end describe "#ssh_info" do describe "with the provider returning nil" do it "should return nil if the provider returns nil" do expect(provider).to receive(:ssh_info).and_return(nil) expect(instance.ssh_info).to be_nil end end describe "with the provider returning data" do let(:provider_ssh_info) { {} } let(:ssh_klass) { Vagrant::Util::SSH } before(:each) do allow(provider).to receive(:ssh_info).and_return(provider_ssh_info) # Stub the check_key_permissions method so that even if we test incorrectly, # no side effects actually happen. allow(ssh_klass).to receive(:check_key_permissions) end [:host, :port, :username].each do |type| it "should return the provider data if not configured in Vagrantfile" do provider_ssh_info[type] = "foo" instance.config.ssh.send("#{type}=", nil) expect(instance.ssh_info[type]).to eq("foo") end it "should return the Vagrantfile value if provider data not given" do provider_ssh_info[type] = nil instance.config.ssh.send("#{type}=", "bar") expect(instance.ssh_info[type]).to eq("bar") end it "should use the default if no override and no provider" do provider_ssh_info[type] = nil instance.config.ssh.send("#{type}=", nil) instance.config.ssh.default.send("#{type}=", "foo") expect(instance.ssh_info[type]).to eq("foo") end it "should use the override if set even with a provider" do provider_ssh_info[type] = "baz" instance.config.ssh.send("#{type}=", "bar") instance.config.ssh.default.send("#{type}=", "foo") expect(instance.ssh_info[type]).to eq("bar") end end it "should set the configured forward agent settings" do provider_ssh_info[:forward_agent] = true instance.config.ssh.forward_agent = false expect(instance.ssh_info[:forward_agent]).to eq(false) end it "should set the configured forward X11 settings" do provider_ssh_info[:forward_x11] = true instance.config.ssh.forward_x11 = false expect(instance.ssh_info[:forward_x11]).to eq(false) end it "should return the provider private key if given" do provider_ssh_info[:private_key_path] = "/foo" expect(instance.ssh_info[:private_key_path]).to eq([File.expand_path("/foo", env.root_path)]) end it "should return the configured SSH key path if set" do provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = "/bar" expect(instance.ssh_info[:private_key_path]).to eq([File.expand_path("/bar", env.root_path)]) end it "should return the array of SSH keys if set" do provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = ["/foo", "/bar"] expect(instance.ssh_info[:private_key_path]).to eq([ File.expand_path("/foo", env.root_path), File.expand_path("/bar", env.root_path), ]) end it "should check and try to fix the permissions of the default private key file" do provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = nil instance.env.default_private_key_paths.each do |key_path| expect(ssh_klass).to receive(:check_key_permissions).once.with(Pathname.new(key_path.to_s)) end instance.ssh_info end it "should check and try to fix the permissions of given private key files" do provider_ssh_info[:private_key_path] = nil # Use __FILE__ to provide an existing file instance.config.ssh.private_key_path = [File.expand_path(__FILE__), File.expand_path(__FILE__)] expect(ssh_klass).to receive(:check_key_permissions).twice.with(Pathname.new(File.expand_path(__FILE__))) instance.ssh_info end it "should not check the permissions of a private key file that does not exist" do provider_ssh_info[:private_key_path] = "/foo" expect(ssh_klass).to_not receive(:check_key_permissions) instance.ssh_info end context "expanding path relative to the root path" do it "should with the provider key path" do provider_ssh_info[:private_key_path] = "~/foo" expect(instance.ssh_info[:private_key_path]).to eq( [File.expand_path("~/foo", env.root_path)] ) end it "should with the config private key path" do provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = "~/bar" expect(instance.ssh_info[:private_key_path]).to eq( [File.expand_path("~/bar", env.root_path)] ) end end it "should return the default private key path if provider and config doesn't have one" do provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = nil expect(instance.ssh_info[:private_key_path]).to eq( instance.env.default_private_key_paths ) end it "should return the default private key path with keys_only = false" do provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = nil instance.config.ssh.keys_only = false expect(instance.ssh_info[:private_key_path]).to eq( instance.env.default_private_key_paths ) end it "should not set any default private keys if a password is specified" do provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = nil instance.config.ssh.password = "" expect(instance.ssh_info[:private_key_path]).to be_empty expect(instance.ssh_info[:password]).to eql("") end it "should return the private key in the data dir above all else" do provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = nil instance.config.ssh.password = "" instance.data_dir.join("private_key").open("w+") do |f| f.write("hey") end expect(instance.ssh_info[:private_key_path]).to eql( [instance.data_dir.join("private_key").to_s]) expect(instance.ssh_info[:password]).to eql("") end it "should return the private key in the Vagrantfile if the data dir exists" do path = "/foo" path = "C:/foo" if Vagrant::Util::Platform.windows? provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = path instance.data_dir.join("private_key").open("w+") do |f| f.write("hey") end expect(instance.ssh_info[:private_key_path]).to eql([path]) end it "should return the remote_user when set" do instance.config.ssh.remote_user = "remote-user" expect(instance.ssh_info[:remote_user]).to eq("remote-user") end it "should return the config when set" do instance.config.ssh.config = "/path/to/ssh_config" expect(instance.ssh_info[:config]).to eq("/path/to/ssh_config") end it "should return the default connect_timeout" do expect(instance.ssh_info[:connect_timeout]). to eq(VagrantPlugins::Kernel_V2::SSHConnectConfig::DEFAULT_SSH_CONNECT_TIMEOUT) end it "should return the connect_timeout when set" do instance.config.ssh.connect_timeout = 2 expect(instance.ssh_info[:connect_timeout]).to eq(2) end context "with no data dir" do let(:base) { true } let(:data_dir) { nil } it "returns nil as the private key path" do provider_ssh_info[:private_key_path] = nil instance.config.ssh.private_key_path = nil instance.config.ssh.password = "" expect(instance.ssh_info[:private_key_path]).to be_empty expect(instance.ssh_info[:password]).to eql("") end end context "with custom ssh_info" do it "keys_only should be default" do expect(instance.ssh_info[:keys_only]).to be(true) end it "verify_host_key should be default" do expect(instance.ssh_info[:verify_host_key]).to be(:never) end it "extra_args should be nil" do expect(instance.ssh_info[:extra_args]).to be(nil) end it "extra_args should be set" do instance.config.ssh.extra_args = ["-L", "127.1.2.7:8008:127.1.2.7:8008"] expect(instance.ssh_info[:extra_args]).to eq(["-L", "127.1.2.7:8008:127.1.2.7:8008"]) end it "extra_args should be set as an array" do instance.config.ssh.extra_args = "-6" expect(instance.ssh_info[:extra_args]).to eq("-6") end it "keys_only should be overridden" do instance.config.ssh.keys_only = false expect(instance.ssh_info[:keys_only]).to be(false) end it "verify_host_key should be overridden" do instance.config.ssh.verify_host_key = true expect(instance.ssh_info[:verify_host_key]).to be(true) end end end end describe "#state" do it "should query state from the provider" do state = Vagrant::MachineState.new(:id, "short", "long") allow(provider).to receive(:state).and_return(state) expect(instance.state.id).to eq(:id) end it "should raise an exception if a MachineState is not returned" do expect(provider).to receive(:state).and_return(:old_school) expect { instance.state }. to raise_error(Vagrant::Errors::MachineStateInvalid) end it "should save the state with the index" do allow(provider).to receive(:machine_id_changed) subject.id = "foo" state = Vagrant::MachineState.new(:id, "short", "long") expect(provider).to receive(:state).and_return(state) subject.state entry = env.machine_index.get(subject.index_uuid) expect(entry).to_not be_nil expect(entry.state).to eq("short") env.machine_index.release(entry) end end describe "#recover_machine" do it "does not recover a machine already in the index" do subject.id = "foo" expected_entry = env.machine_index.get(subject.index_uuid) env.machine_index.release(expected_entry) entry = subject.recover_machine(:running) expect(entry.id).to eq(expected_entry.id) # Ensure entry is not locked env.machine_index.get(subject.index_uuid) end it "recovers a machine" do instance = new_instance instance.id = "foo" entry = instance.recover_machine(:running) expect(entry.id).to eq(instance.index_uuid) # Ensure entry is not locked env.machine_index.get(entry.id) env.machine_index.release(entry) query_entry = env.machine_index.get(instance.index_uuid) expect(entry.id).to eq(query_entry.id) end end describe "#with_ui" do it "temporarily changes the UI" do ui = Object.new changed_ui = nil subject.with_ui(ui) do changed_ui = subject.ui end expect(changed_ui).to equal(ui) expect(subject.ui).to_not equal(ui) end end describe "#reload" do context "when ID is unset and id file does not exist" do it "should remain nil" do expect(subject.id).to be_nil instance.reload expect(subject.id).to be_nil end end context "when id file is set" do let(:id_content) { "test-machine-id" } before do id_file = subject.data_dir.join("id") File.write(id_file.to_s, id_content) end it "should update the machine id" do expect(subject.id).to be_nil instance.reload expect(subject.id).to eq(id_content) end it "should notify of the id change when provider is set" do expect(provider).to receive(:machine_id_changed) instance.reload end context "when id file content includes whitespace" do let(:id_content) { " test-machine-id\n" } it "should remove all whitespace" do instance.reload expect(instance.id).to eq("test-machine-id") end end context "when id file content is all whitespace" do let(:id_content) { "\0\0\0\0\0\0" } it "should not update the id" do expect(instance.id).to be_nil instance.reload expect(instance.id).to be_nil end end context "when id is already set to value in id file" do it "should not notify of id change" do instance.instance_variable_set(:@id, id_content) expect(provider).not_to receive(:machine_id_changed) instance.reload expect(instance.id).to eq(id_content) end end end end end ================================================ FILE: test/unit/vagrant/plugin/manager_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" require "pathname" require "vagrant/plugin" require "vagrant/plugin/manager" require "vagrant/plugin/state_file" require "vagrant/util/deep_merge" require File.expand_path("../../../base", __FILE__) describe Vagrant::Plugin::Manager do include_context "unit" let(:path) do Pathname.new(Dir::Tmpname.create("vagrant-test-plugin-manager") {}) end let(:bundler) { double("bundler") } after do path.unlink if path.file? end before do allow(Vagrant::Bundler).to receive(:instance).and_return(bundler) end subject { described_class.new(path) } describe "#globalize!" do let(:plugins) { double("plugins") } before do allow(subject).to receive(:bundler_init) allow(subject).to receive(:installed_plugins).and_return(plugins) end it "should init bundler with installed plugins" do expect(subject).to receive(:bundler_init).with(plugins, anything) subject.globalize! end it "should return installed plugins" do expect(subject.globalize!).to eq(plugins) end end describe "#localize!" do let(:env) { double("env", local_data_path: local_data_path) } let(:local_data_path) { double("local_data_path") } let(:plugins) { double("plugins") } let(:state_file) { double("state_file", path: double("state_file_path"), installed_plugins: plugins) } before do allow(Vagrant::Plugin::StateFile).to receive(:new).and_return(state_file) allow(bundler).to receive(:environment_path=) allow(local_data_path).to receive(:join).and_return(local_data_path) if local_data_path allow(subject).to receive(:bundler_init) end context "without local data path defined" do let(:local_data_path) { nil } it "should not do any initialization" do expect(subject).not_to receive(:bundler_init) subject.localize!(env) end it "should return nil" do expect(subject.localize!(env)).to be_nil end end it "should run bundler initialization" do expect(subject).to receive(:bundler_init).with(plugins, anything) subject.localize!(env) end it "should return plugins" do expect(subject.localize!(env)).to eq(plugins) end end describe "#ready?" do let(:plugins) { double("plugins") } let(:env) { double("env", local_data_path: nil) } before do allow(subject).to receive(:bundler_init) end it "should be false by default" do expect(subject.ready?).to be_falsey end it "should be false when only globalize! has been called" do subject.globalize! expect(subject.ready?).to be_falsey end it "should be false when only localize! has been called" do subject.localize!(env) expect(subject.ready?).to be_falsey end it "should be true when both localize! and globalize! have been called" do subject.globalize! subject.localize!(env) expect(subject.ready?).to be_truthy end end describe "#bundler_init" do let(:plugins) { {"plugin_name" => {}} } before do allow(Vagrant).to receive(:plugins_init?).and_return(true) allow(bundler).to receive(:init!) end it "should init the bundler instance with plugins" do expect(bundler).to receive(:init!).with(plugins, any_args) subject.bundler_init(plugins) end it "should return nil" do expect(subject.bundler_init(plugins)).to be_nil end context "with plugin init disabled" do before { expect(Vagrant).to receive(:plugins_init?).and_return(false) } it "should return nil" do expect(subject.bundler_init(plugins)).to be_nil end it "should not init the bundler instance" do expect(bundler).not_to receive(:init!).with(plugins) subject.bundler_init(plugins) end end end describe "#plugin_installed?" do let(:ready) { true } let(:specs) { [] } before do allow(subject).to receive(:ready?).and_return(ready) allow(subject).to receive(:installed_specs).and_return(specs) end context "when manager is ready" do it "should return false when plugin is not found" do expect(subject.plugin_installed?("vagrant-plugin")).to be_falsey end context "when plugin is installed" do let(:specs) { [Gem::Specification.new("vagrant-plugin", "1.2.3")] } it "should return true" do expect(subject.plugin_installed?("vagrant-plugin")).to be_truthy end it "should return true when version matches installed version" do expect(subject.plugin_installed?("vagrant-plugin", "1.2.3")).to be_truthy end it "should return true when version requirement is satisified by version" do expect(subject.plugin_installed?("vagrant-plugin", "> 1.0")).to be_truthy end it "should return false when version requirement is not satisified by version" do expect(subject.plugin_installed?("vagrant-plugin", "2.0")).to be_falsey end end end context "when manager is not ready" do let(:ready) { false } let(:plugins) { {} } before { allow(subject).to receive(:installed_plugins).and_return(plugins) } it "should check installed plugin data" do expect(subject).to receive(:installed_plugins).and_return(plugins) subject.plugin_installed?("vagrant-plugin") end it "should return false when plugin is not found" do expect(subject.plugin_installed?("vagrant-plugin")).to be_falsey end context "when plugin is installed" do let(:plugins) { {"vagrant-plugin" => {"installed_gem_version" => "1.2.3"}} } it "should return true" do expect(subject.plugin_installed?("vagrant-plugin")).to be_truthy end it "should return true when version matches installed version" do expect(subject.plugin_installed?("vagrant-plugin", "1.2.3")).to be_truthy end it "should return true when version requirement is satisified by version" do expect(subject.plugin_installed?("vagrant-plugin", "> 1.0")).to be_truthy end it "should return false when version requirement is not satisified by version" do expect(subject.plugin_installed?("vagrant-plugin", "2.0")).to be_falsey end end end end describe "#install_plugin" do it "installs the plugin and adds it to the state file" do specs = Array.new(5) { Gem::Specification.new } specs[3].name = "foo" expect(bundler).to receive(:install).once.with(any_args) { |plugins, local| expect(plugins).to have_key("foo") expect(local).to be_falsey }.and_return(specs) expect(bundler).to receive(:clean) result = subject.install_plugin("foo") # It should return the spec of the installed plugin expect(result).to eql(specs[3]) # It should've added the plugin to the state expect(subject.installed_plugins).to have_key("foo") end it "masks GemNotFound with our error" do expect(bundler).to receive(:install).and_raise(Gem::GemNotFoundException) expect { subject.install_plugin("foo") }. to raise_error(Vagrant::Errors::PluginGemNotFound) end it "masks bundler errors with our own error" do expect(bundler).to receive(:install).and_raise(Gem::InstallError) expect { subject.install_plugin("foo") }. to raise_error(Vagrant::Errors::BundlerError) end it "can install a local gem" do name = "foo.gem" version = "1.0" local_spec = Gem::Specification.new local_spec.name = "bar" local_spec.version = version expect(bundler).to receive(:install_local).with(name, {}). ordered.and_return(local_spec) expect(bundler).not_to receive(:install) expect(bundler).to receive(:clean) subject.install_plugin(name) plugins = subject.installed_plugins expect(plugins).to have_key("bar") expect(plugins["bar"]["gem_version"]).to eql("1.0") end context "with existing activation" do let(:value) { double("value") } before do expect(bundler).to receive(:install).and_return([]) allow(bundler).to receive(:clean) end it "should locate existing activation if available" do expect(Gem::Specification).to receive(:find).and_return(value) expect(subject.install_plugin("foo")).to eq(value) end it "should raise an error if no activation is located" do expect(Gem::Specification).to receive(:find).and_return(nil) expect { subject.install_plugin("foo") }.to raise_error(Vagrant::Errors::PluginInstallFailed) end end describe "installation options" do let(:specs) do specs = Array.new(5) { Gem::Specification.new } specs[3].name = "foo" specs end before do allow(bundler).to receive(:install).and_return(specs) end it "installs a version with constraints" do expect(bundler).to receive(:install).once.with(any_args) { |plugins, local| expect(plugins).to have_key("foo") expect(plugins["foo"]["gem_version"]).to eql(">= 0.1.0") expect(local).to be_falsey }.and_return(specs) expect(bundler).to receive(:clean) subject.install_plugin("foo", version: ">= 0.1.0") plugins = subject.installed_plugins expect(plugins).to have_key("foo") expect(plugins["foo"]["gem_version"]).to eql(">= 0.1.0") end it "installs with an exact version but doesn't constrain" do expect(bundler).to receive(:install).once.with(any_args) { |plugins, local| expect(plugins).to have_key("foo") expect(plugins["foo"]["gem_version"]).to eql("0.1.0") expect(local).to be_falsey }.and_return(specs) expect(bundler).to receive(:clean) subject.install_plugin("foo", version: "0.1.0") plugins = subject.installed_plugins expect(plugins).to have_key("foo") expect(plugins["foo"]["gem_version"]).to eql("0.1.0") end end end describe "#uninstall_plugin" do it "removes the plugin from the state" do sf = Vagrant::Plugin::StateFile.new(path) sf.add_plugin("foo") # Sanity expect(subject.installed_plugins).to have_key("foo") # Test expect(bundler).to receive(:clean).once.with({}) # Remove it subject.uninstall_plugin("foo") expect(subject.installed_plugins).to_not have_key("foo") end it "masks bundler errors with our own error" do sf = Vagrant::Plugin::StateFile.new(path) sf.add_plugin("foo") expect(bundler).to receive(:clean).and_raise(Gem::InstallError) expect { subject.uninstall_plugin("foo") }. to raise_error(Vagrant::Errors::BundlerError) end context "with a system file" do let(:systems_path) { temporary_file } before do systems_path.unlink allow(described_class).to receive(:system_plugins_file).and_return(systems_path) sf = Vagrant::Plugin::StateFile.new(systems_path) sf.add_plugin("foo", version: "0.2.0") sf.add_plugin("bar") end it "uninstalls the user plugin if it exists" do sf = Vagrant::Plugin::StateFile.new(path) sf.add_plugin("bar") # Test expect(bundler).to receive(:clean).once.with(anything) # Remove it subject.uninstall_plugin("bar") plugins = subject.installed_plugins expect(plugins["foo"]["system"]).to be(true) end it "raises an error if uninstalling a system gem" do expect { subject.uninstall_plugin("bar") }. to raise_error(Vagrant::Errors::PluginUninstallSystem) end end end describe "#update_plugins" do it "masks bundler errors with our own error" do expect(bundler).to receive(:update).and_raise(Gem::InstallError) expect { subject.update_plugins([]) }. to raise_error(Vagrant::Errors::BundlerError) end end context "without state" do describe "#installed_plugins" do it "is empty initially" do expect(subject.installed_plugins).to be_empty end end end context "with state" do before do sf = Vagrant::Plugin::StateFile.new(path) sf.add_plugin("foo", version: "0.1.0") end describe "#installed_plugins" do it "has the plugins" do plugins = subject.installed_plugins expect(plugins.length).to eql(1) expect(plugins).to have_key("foo") end end describe "#installed_specs" do it "has the plugins" do # We just add "i18n" because it is a dependency of Vagrant and # we know it will be there. sf = Vagrant::Plugin::StateFile.new(path) sf.add_plugin("i18n") specs = subject.installed_specs expect(specs.length).to eql(1) expect(specs.first.name).to eql("i18n") end end context "with system plugins" do let(:systems_path) { temporary_file } before do systems_path.unlink allow(described_class).to receive(:system_plugins_file).and_return(systems_path) sf = Vagrant::Plugin::StateFile.new(systems_path) sf.add_plugin("foo", version: "0.2.0") sf.add_plugin("bar") end describe "#installed_plugins" do it "has the plugins" do plugins = subject.installed_plugins expect(plugins.length).to eql(2) expect(plugins).to have_key("foo") expect(plugins["foo"]["gem_version"]).to eq("0.1.0") expect(plugins["foo"]["system"]).to be_truthy expect(plugins).to have_key("bar") expect(plugins["bar"]["system"]).to be(true) end end end end end ================================================ FILE: test/unit/vagrant/plugin/state_file_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "json" require "pathname" require File.expand_path("../../../base", __FILE__) describe Vagrant::Plugin::StateFile do let(:path) do Pathname.new(Dir::Tmpname.create("vagrant-test-statefile") {}) end after do path.unlink if path.file? end subject { described_class.new(path) } context "new usage" do it "should have no plugins without saving some" do expect(subject.installed_plugins).to be_empty end it "should have plugins when saving" do subject.add_plugin("foo") instance = described_class.new(path) plugins = instance.installed_plugins expect(plugins.length).to eql(1) expect(plugins["foo"]).to eql({ "ruby_version" => RUBY_VERSION, "vagrant_version" => Vagrant::VERSION, "gem_version" => "", "require" => "", "sources" => [], "installed_gem_version" => nil, "env_local" => false, }) end it "should check for plugins" do expect(subject.has_plugin?("foo")).to be(false) subject.add_plugin("foo") expect(subject.has_plugin?("foo")).to be(true) end it "should remove plugins" do subject.add_plugin("foo") subject.remove_plugin("foo") instance = described_class.new(path) expect(instance.installed_plugins).to be_empty end it "should store plugins uniquely" do subject.add_plugin("foo") subject.add_plugin("foo") instance = described_class.new(path) expect(instance.installed_plugins.keys).to eql(["foo"]) end it "should store metadata" do subject.add_plugin("foo", version: "1.2.3") expect(subject.installed_plugins["foo"]["gem_version"]).to eql("1.2.3") end describe "sources" do it "should have no sources" do expect(subject.sources).to be_empty end it "should add sources" do subject.add_source("foo") expect(subject.sources).to eql(["foo"]) end it "should de-dup sources" do subject.add_source("foo") subject.add_source("foo") expect(subject.sources).to eql(["foo"]) end it "can remove sources" do subject.add_source("foo") subject.remove_source("foo") expect(subject.sources).to be_empty end end end context "with an old-style file" do before do data = { "installed" => ["foo"], } path.open("w+") do |f| f.write(JSON.dump(data)) end end it "should have the right installed plugins" do plugins = subject.installed_plugins expect(plugins.keys).to eql(["foo"]) expect(plugins["foo"]["ruby_version"]).to eql("0") expect(plugins["foo"]["vagrant_version"]).to eql("0") end end context "with parse errors" do before do path.open("w+") do |f| f.write("I'm not json") end end it "should raise a VagrantError" do expect { subject }. to raise_error(Vagrant::Errors::PluginStateFileParseError) end end end ================================================ FILE: test/unit/vagrant/plugin/v1/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require 'optparse' describe Vagrant::Plugin::V1::Command do describe "parsing options" do let(:klass) do Class.new(described_class) do # Make the method public since it is normally protected public :parse_options end end it "returns the remaining arguments" do options = {} opts = OptionParser.new do |opts| opts.on("-f") do |f| options[:f] = f end end result = klass.new(["-f", "foo"], nil).parse_options(opts) # Check the results expect(options[:f]).to be expect(result).to eq(["foo"]) end it "creates an option parser if none is given" do result = klass.new(["foo"], nil).parse_options(nil) expect(result).to eq(["foo"]) end ["-h", "--help"].each do |help_string| it "returns nil and prints the help if '#{help_string}' is given" do instance = klass.new([help_string], nil) expect(instance).to receive(:safe_puts) expect(instance.parse_options(OptionParser.new)).to be_nil end end it "raises an error if invalid options are given" do instance = klass.new(["-f"], nil) expect { instance.parse_options(OptionParser.new) }. to raise_error(Vagrant::Errors::CLIInvalidOptions) end end describe "target VMs" do let(:klass) do Class.new(described_class) do # Make the method public since it is normally protected public :with_target_vms end end let(:environment) do env = double("environment") allow(env).to receive(:root_path).and_return("foo") env end let(:instance) { klass.new([], environment) } it "should raise an exception if a root_path is not available" do allow(environment).to receive(:root_path).and_return(nil) expect { instance.with_target_vms }. to raise_error(Vagrant::Errors::NoEnvironmentError) end it "should yield every VM in order is no name is given" do foo_vm = double("foo") allow(foo_vm).to receive(:name).and_return("foo") bar_vm = double("bar") allow(bar_vm).to receive(:name).and_return("bar") allow(environment).to receive(:multivm?).and_return(true) allow(environment).to receive(:vms).and_return({ "foo" => foo_vm, "bar" => bar_vm }) allow(environment).to receive(:vms_ordered).and_return([foo_vm, bar_vm]) vms = [] instance.with_target_vms do |vm| vms << vm end expect(vms).to eq([foo_vm, bar_vm]) end it "raises an exception if the named VM doesn't exist" do allow(environment).to receive(:multivm?).and_return(true) allow(environment).to receive(:vms).and_return({}) expect { instance.with_target_vms("foo") }. to raise_error(Vagrant::Errors::VMNotFoundError) end it "yields the given VM if a name is given" do foo_vm = double("foo") allow(foo_vm).to receive(:name).and_return(:foo) allow(environment).to receive(:multivm?).and_return(true) allow(environment).to receive(:vms).and_return({ foo: foo_vm, bar: nil }) vms = [] instance.with_target_vms("foo") { |vm| vms << vm } expect(vms).to eq([foo_vm]) end end describe "splitting the main and subcommand args" do let(:instance) do Class.new(described_class) do # Make the method public since it is normally protected public :split_main_and_subcommand end.new(nil, nil) end it "should work when given all 3 parts" do result = instance.split_main_and_subcommand(["-v", "status", "-h", "-v"]) expect(result).to eq([["-v"], "status", ["-h", "-v"]]) end it "should work when given only a subcommand and args" do result = instance.split_main_and_subcommand(["status", "-h"]) expect(result).to eq([[], "status", ["-h"]]) end it "should work when given only main flags" do result = instance.split_main_and_subcommand(["-v", "-h"]) expect(result).to eq([["-v", "-h"], nil, []]) end it "should work when given only a subcommand" do result = instance.split_main_and_subcommand(["status"]) expect(result).to eq([[], "status", []]) end it "works when there are other non-flag args after the subcommand" do result = instance.split_main_and_subcommand(["-v", "box", "add", "-h"]) expect(result).to eq([["-v"], "box", ["add", "-h"]]) end end end ================================================ FILE: test/unit/vagrant/plugin/v1/communicator_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V1::Communicator do let(:machine) { Object.new } it "should not match by default" do expect(described_class.match?(machine)).not_to be end end ================================================ FILE: test/unit/vagrant/plugin/v1/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V1::Config do include_context "unit" let(:foo_class) do Class.new(described_class) do attr_accessor :one attr_accessor :two end end it "has an UNSET_VALUE constant" do value = described_class.const_get("UNSET_VALUE") expect(value).to be_kind_of Object expect(value).to eql(described_class.const_get("UNSET_VALUE")) end describe "merging" do it "should merge by default by simply copying each instance variable" do one = foo_class.new one.one = 2 one.two = 1 two = foo_class.new two.two = 5 result = one.merge(two) expect(result.one).to eq(2) expect(result.two).to eq(5) end it "doesn't merge values that start with a double underscore" do one = foo_class.new one.one = 1 one.two = 1 one.instance_variable_set(:@__bar, "one") two = foo_class.new two.two = 2 two.instance_variable_set(:@__bar, "two") # Merge and verify result = one.merge(two) expect(result.one).to eq(1) expect(result.two).to eq(2) expect(result.instance_variable_get(:@__bar)).to be_nil end end end ================================================ FILE: test/unit/vagrant/plugin/v1/host_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V1::Host do # No tests. end ================================================ FILE: test/unit/vagrant/plugin/v1/manager_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V1::Manager do include_context "unit" let(:instance) { described_class.new } def plugin p = Class.new(Vagrant.plugin("1")) yield p p end it "should enumerate registered communicator classes" do pA = plugin do |p| p.communicator("foo") { "bar" } end pB = plugin do |p| p.communicator("bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.communicators.length).to eq(2) expect(instance.communicators[:foo]).to eq("bar") expect(instance.communicators[:bar]).to eq("baz") end it "should enumerate registered configuration classes" do pA = plugin do |p| p.config("foo") { "bar" } end pB = plugin do |p| p.config("bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.config.length).to eq(2) expect(instance.config[:foo]).to eq("bar") expect(instance.config[:bar]).to eq("baz") end it "should enumerate registered upgrade safe config classes" do pA = plugin do |p| p.config("foo", true) { "bar" } end pB = plugin do |p| p.config("bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.config_upgrade_safe.length).to eq(1) expect(instance.config_upgrade_safe[:foo]).to eq("bar") end it "should enumerate registered guest classes" do pA = plugin do |p| p.guest("foo") { "bar" } end pB = plugin do |p| p.guest("bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.guests.length).to eq(2) expect(instance.guests[:foo]).to eq("bar") expect(instance.guests[:bar]).to eq("baz") end it "should enumerate registered host classes" do pA = plugin do |p| p.host("foo") { "bar" } end pB = plugin do |p| p.host("bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.hosts.length).to eq(2) expect(instance.hosts[:foo]).to eq("bar") expect(instance.hosts[:bar]).to eq("baz") end it "should enumerate registered provider classes" do pA = plugin do |p| p.provider("foo") { "bar" } end pB = plugin do |p| p.provider("bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.providers.length).to eq(2) expect(instance.providers[:foo]).to eq("bar") expect(instance.providers[:bar]).to eq("baz") end end ================================================ FILE: test/unit/vagrant/plugin/v1/plugin_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V1::Plugin do after(:each) do # We want to make sure that the registered plugins remains empty # after each test. described_class.manager.reset! end it "should be able to set and get the name" do plugin = Class.new(described_class) do name "foo" end expect(plugin.name).to eq("foo") end it "should be able to set and get the description" do plugin = Class.new(described_class) do description "bar" end expect(plugin.description).to eq("bar") end describe "action hooks" do it "should register action hooks" do plugin = Class.new(described_class) do action_hook("foo") { "bar" } end hooks = plugin.action_hook("foo") expect(hooks.length).to eq(1) expect(hooks[0].call).to eq("bar") end end describe "commands" do it "should register command classes" do plugin = Class.new(described_class) do command("foo") { "bar" } end expect(plugin.command[:foo]).to eq("bar") end ["spaces bad", "sym^bols"].each do |bad| it "should not allow bad command name: #{bad}" do plugin = Class.new(described_class) expect { plugin.command(bad) {} }. to raise_error(Vagrant::Plugin::V1::InvalidCommandName) end end it "should lazily register command classes" do # Below would raise an error if the value of the command class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do command("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the command key that # a proper error is raised. expect { plugin.command[:foo] }.to raise_error(StandardError) end end describe "communicators" do it "should register communicator classes" do plugin = Class.new(described_class) do communicator("foo") { "bar" } end expect(plugin.communicator[:foo]).to eq("bar") end it "should lazily register communicator classes" do # Below would raise an error if the value of the class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do communicator("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.communicator[:foo] }.to raise_error(StandardError) end end describe "configuration" do it "should register configuration classes" do plugin = Class.new(described_class) do config("foo") { "bar" } end expect(plugin.config[:foo]).to eq("bar") end it "should lazily register configuration classes" do # Below would raise an error if the value of the config class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do config("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.config[:foo] }.to raise_error(StandardError) end end describe "guests" do it "should register guest classes" do plugin = Class.new(described_class) do guest("foo") { "bar" } end expect(plugin.guest[:foo]).to eq("bar") end it "should lazily register guest classes" do # Below would raise an error if the value of the guest class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do guest("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the guest key that # a proper error is raised. expect { plugin.guest[:foo] }.to raise_error(StandardError) end end describe "hosts" do it "should register host classes" do plugin = Class.new(described_class) do host("foo") { "bar" } end expect(plugin.host[:foo]).to eq("bar") end it "should lazily register host classes" do # Below would raise an error if the value of the host class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do host("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the host key that # a proper error is raised. expect { plugin.host[:foo] }.to raise_error(StandardError) end end describe "providers" do it "should register provider classes" do plugin = Class.new(described_class) do provider("foo") { "bar" } end expect(plugin.provider[:foo]).to eq("bar") end it "should lazily register provider classes" do # Below would raise an error if the value of the config class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do provider("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.provider[:foo] }.to raise_error(StandardError) end end describe "provisioners" do it "should register provisioner classes" do plugin = Class.new(described_class) do provisioner("foo") { "bar" } end expect(plugin.provisioner[:foo]).to eq("bar") end it "should lazily register provisioner classes" do # Below would raise an error if the value of the config class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do provisioner("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.provisioner[:foo] }.to raise_error(StandardError) end end describe "plugin registration" do let(:manager) { described_class.manager } it "should have no registered plugins" do expect(manager.registered).to be_empty end it "should register a plugin when a name is set" do plugin = Class.new(described_class) do name "foo" end expect(manager.registered).to eq([plugin]) end it "should register a plugin only once" do plugin = Class.new(described_class) do name "foo" name "bar" end expect(manager.registered).to eq([plugin]) end end end ================================================ FILE: test/unit/vagrant/plugin/v1/provider_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V1::Provider do let(:machine) { Object.new } let(:instance) { described_class.new(machine) } it "should return nil by default for actions" do expect(instance.action(:whatever)).to be_nil end it "should return nil by default for ssh info" do expect(instance.ssh_info).to be_nil end it "should return nil by default for state" do expect(instance.state).to be_nil end end ================================================ FILE: test/unit/vagrant/plugin/v2/command_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require 'optparse' require 'vagrant/machine_index' describe Vagrant::Plugin::V2::Command do include_context "unit" describe "parsing options" do let(:klass) do Class.new(described_class) do # Make the method public since it is normally protected public :parse_options end end it "returns the remaining arguments" do options = {} opts = OptionParser.new do |o| o.on("-f") do |f| options[:f] = f end end result = klass.new(["-f", "foo"], nil).parse_options(opts) # Check the results expect(options[:f]).to be expect(result).to eq(["foo"]) end it "creates an option parser if none is given" do result = klass.new(["foo"], nil).parse_options(nil) expect(result).to eq(["foo"]) end ["-h", "--help"].each do |help_string| it "returns nil and prints the help if '#{help_string}' is given" do instance = klass.new([help_string], nil) expect(instance).to receive(:safe_puts) expect(instance.parse_options(OptionParser.new)).to be_nil end end it "raises an error if invalid options are given" do instance = klass.new(["-f"], nil) expect { instance.parse_options(OptionParser.new) }. to raise_error(Vagrant::Errors::CLIInvalidOptions) end it "raises an error if ambiguous options are given" do instance = klass.new(["-provision"], nil) expect { instance.parse_options(OptionParser.new) }. to raise_error(Vagrant::Errors::CLIInvalidOptions) end it "raises an error if options without a value are given" do opts = OptionParser.new do |o| o.on("--provision-with x,y,z", Array, "Example") { |f| } end instance = klass.new(["--provision-with"], nil) expect { instance.parse_options(opts) }. to raise_error(Vagrant::Errors::CLIInvalidOptions) end end describe "target VMs" do let(:klass) do Class.new(described_class) do # Make the method public since it is normally protected public :with_target_vms end end let(:environment) do # We have to create a Vagrantfile so there is a root path test_iso_env.vagrantfile("") test_iso_env.create_vagrant_env end let(:test_iso_env) { isolated_environment } let(:instance) { klass.new([], environment) } subject { instance } it "should raise an exception if a root_path is not available" do allow(environment).to receive(:root_path).and_return(nil) expect { instance.with_target_vms }. to raise_error(Vagrant::Errors::NoEnvironmentError) end it "should yield every VM in order if no name is given" do foo_vm = double("foo") allow(foo_vm).to receive(:name).and_return("foo") allow(foo_vm).to receive(:provider).and_return(:foobarbaz) allow(foo_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(foo_vm).to receive(:state).and_return(nil) bar_vm = double("bar") allow(bar_vm).to receive(:name).and_return("bar") allow(bar_vm).to receive(:provider).and_return(:foobarbaz) allow(bar_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(bar_vm).to receive(:state).and_return(nil) allow(environment).to receive(:machine_names).and_return([:foo, :bar]) allow(environment).to receive(:machine).with(:foo, environment.default_provider).and_return(foo_vm) allow(environment).to receive(:machine).with(:bar, environment.default_provider).and_return(bar_vm) vms = [] instance.with_target_vms do |vm| vms << vm end expect(vms).to eq([foo_vm, bar_vm]) end it "raises an exception if the named VM doesn't exist" do allow(environment).to receive(:machine_names).and_return([:default]) allow(environment).to receive(:machine).with(:foo, anything).and_return(nil) expect { instance.with_target_vms("foo") }. to raise_error(Vagrant::Errors::VMNotFoundError) end it "yields the given VM if a name is given" do foo_vm = double("foo") allow(foo_vm).to receive(:name).and_return("foo") allow(foo_vm).to receive(:provider).and_return(:foobarbaz) allow(foo_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(foo_vm).to receive(:state).and_return(nil) allow(environment).to receive(:machine).with(:foo, environment.default_provider).and_return(foo_vm) vms = [] instance.with_target_vms("foo") { |vm| vms << vm } expect(vms).to eq([foo_vm]) end it "calls state after yielding the vm to update the machine index" do foo_vm = double("foo") allow(foo_vm).to receive(:name).and_return("foo") allow(foo_vm).to receive(:provider).and_return(:foobarbaz) allow(foo_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(foo_vm).to receive(:state).and_return(nil) allow(environment).to receive(:machine).with(:foo, environment.default_provider).and_return(foo_vm) vms = [] expect(foo_vm).to receive(:state) instance.with_target_vms("foo") { |vm| vms << vm } end it "does not recover the vm if it has no uuid" do foo_vm = double("foo") provider = :foobarbaz state_id = :some_state allow(foo_vm).to receive(:name).and_return("foo") allow(foo_vm).to receive(:provider).and_return(provider) allow(foo_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(foo_vm).to receive(:state).and_return(double("state", id: state_id)) allow(foo_vm).to receive(:index_uuid).and_return(nil) allow(environment).to receive(:machine).with(:foo, provider).and_return(foo_vm) expect(foo_vm).not_to receive(:recover_machine).with(state_id) vms = [] instance.with_target_vms("foo", provider: provider) { |vm| vms << vm } expect(vms).to eq([foo_vm]) end it "recovers the vm" do foo_vm = double("foo") provider = :foobarbaz state_id = :some_state allow(foo_vm).to receive(:name).and_return("foo") allow(foo_vm).to receive(:provider).and_return(provider) allow(foo_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(foo_vm).to receive(:state).and_return(double("state", id: state_id)) allow(foo_vm).to receive(:index_uuid).and_return("someuuid") allow(environment).to receive(:machine).with(:foo, provider).and_return(foo_vm) expect(foo_vm).to receive(:recover_machine).with(state_id) vms = [] instance.with_target_vms("foo", provider: provider) { |vm| vms << vm } expect(vms).to eq([foo_vm]) end it "yields the given VM with proper provider if given" do foo_vm = double("foo") provider = :foobarbaz allow(foo_vm).to receive(:name).and_return("foo") allow(foo_vm).to receive(:provider).and_return(provider) allow(foo_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(foo_vm).to receive(:state).and_return(nil) allow(environment).to receive(:machine).with(:foo, provider).and_return(foo_vm) vms = [] instance.with_target_vms("foo", provider: provider) { |vm| vms << vm } expect(vms).to eq([foo_vm]) end it "should raise an exception if an active machine exists with a different provider" do name = :foo allow(environment).to receive(:active_machines).and_return([[name, :vmware]]) expect { instance.with_target_vms(name.to_s, provider: :foo) }. to raise_error Vagrant::Errors::ActiveMachineWithDifferentProvider end it "should default to the active machine provider if no explicit provider requested" do name = :foo provider = :vmware vmware_vm = double("vmware_vm") allow(environment).to receive(:active_machines).and_return([[name, provider]]) allow(environment).to receive(:machine).with(name, provider).and_return(vmware_vm) allow(vmware_vm).to receive(:name).and_return(name) allow(vmware_vm).to receive(:provider).and_return(provider) allow(vmware_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(vmware_vm).to receive(:state).and_return(nil) vms = [] instance.with_target_vms(name.to_s) { |vm| vms << vm } expect(vms).to eq([vmware_vm]) end it "should use the explicit provider if it maches the active machine" do name = :foo provider = :vmware vmware_vm = double("vmware_vm") allow(environment).to receive(:active_machines).and_return([[name, provider]]) allow(environment).to receive(:machine).with(name, provider).and_return(vmware_vm) allow(vmware_vm).to receive(:name).and_return(name) allow(vmware_vm).to receive(:provider).and_return(provider) allow(vmware_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(vmware_vm).to receive(:state).and_return(nil) vms = [] instance.with_target_vms(name.to_s, provider: provider) { |vm| vms << vm } expect(vms).to eq([vmware_vm]) end it "should use the default provider if none is given and none are active" do name = :foo machine = double("machine") allow(environment).to receive(:machine).with(name, environment.default_provider).and_return(machine) allow(machine).to receive(:name).and_return(name) allow(machine).to receive(:provider).and_return(environment.default_provider) allow(machine).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(machine).to receive(:state).and_return(nil) results = [] instance.with_target_vms(name.to_s) { |m| results << m } expect(results).to eq([machine]) end it "should use the primary machine with the active provider" do name = :foo provider = :vmware vmware_vm = double("vmware_vm") allow(environment).to receive(:active_machines).and_return([[name, provider]]) allow(environment).to receive(:machine).with(name, provider).and_return(vmware_vm) allow(environment).to receive(:machine_names).and_return([]) allow(environment).to receive(:primary_machine_name).and_return(name) allow(vmware_vm).to receive(:name).and_return(name) allow(vmware_vm).to receive(:provider).and_return(provider) allow(vmware_vm).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(vmware_vm).to receive(:state).and_return(nil) vms = [] instance.with_target_vms(nil, single_target: true) { |vm| vms << vm } expect(vms).to eq([vmware_vm]) end it "should use the primary machine with the default provider" do name = :foo machine = double("machine") allow(environment).to receive(:active_machines).and_return([]) allow(environment).to receive(:machine).with(name, environment.default_provider).and_return(machine) allow(environment).to receive(:machine_names).and_return([]) allow(environment).to receive(:primary_machine_name).and_return(name) allow(machine).to receive(:name).and_return(name) allow(machine).to receive(:provider).and_return(environment.default_provider) allow(machine).to receive(:ui).and_return(Vagrant::UI::Silent.new) allow(machine).to receive(:state).and_return(nil) vms = [] instance.with_target_vms(nil, single_target: true) { |vm| vms << machine } expect(vms).to eq([machine]) end it "should yield machines from another environment" do iso_env = isolated_environment iso_env.vagrantfile("") other_env = iso_env.create_vagrant_env( home_path: environment.home_path) other_machine = other_env.machine( other_env.machine_names[0], other_env.default_provider) # Set an ID on it so that it is "created" in the index other_machine.id = "foo" # Make sure we don't have a root path, to test allow(environment).to receive(:root_path).and_return(nil) results = [] subject.with_target_vms(other_machine.index_uuid) { |m| results << m } expect(results.length).to eq(1) expect(results[0].id).to eq(other_machine.id) end it "should yield machines from another environment" do iso_env = isolated_environment iso_env.vagrantfile("") other_env = iso_env.create_vagrant_env( home_path: environment.home_path) other_machine = other_env.machine( other_env.machine_names[0], other_env.default_provider) # Set an ID on it so that it is "created" in the index other_machine.id = "foo" # Grab the uuid so we know what it is index_uuid = other_machine.index_uuid # Remove the working directory FileUtils.rm_rf(iso_env.workdir) # Make sure we don't have a root path, to test allow(environment).to receive(:root_path).and_return(nil) # Run the command expect { subject.with_target_vms(index_uuid) { |*args| } }.to raise_error(Vagrant::Errors::EnvironmentNonExistentCWD) # Verify that it no longer exists in the index expect(other_env.machine_index.get(index_uuid)).to be_nil end end describe "splitting the main and subcommand args" do let(:instance) do Class.new(described_class) do # Make the method public since it is normally protected public :split_main_and_subcommand end.new(nil, nil) end it "should work when given all 3 parts" do result = instance.split_main_and_subcommand(["-v", "status", "-h", "-v"]) expect(result).to eq([["-v"], "status", ["-h", "-v"]]) end it "should work when given only a subcommand and args" do result = instance.split_main_and_subcommand(["status", "-h"]) expect(result).to eq([[], "status", ["-h"]]) end it "should work when given only main flags" do result = instance.split_main_and_subcommand(["-v", "-h"]) expect(result).to eq([["-v", "-h"], nil, []]) end it "should work when given only a subcommand" do result = instance.split_main_and_subcommand(["status"]) expect(result).to eq([[], "status", []]) end it "works when there are other non-flag args after the subcommand" do result = instance.split_main_and_subcommand(["-v", "box", "add", "-h"]) expect(result).to eq([["-v"], "box", ["add", "-h"]]) end end end ================================================ FILE: test/unit/vagrant/plugin/v2/communicator_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V2::Communicator do end ================================================ FILE: test/unit/vagrant/plugin/v2/components_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require "vagrant/registry" describe Vagrant::Plugin::V2::Components do subject { described_class.new } it "should have synced folders" do expect(subject.synced_folders).to be_kind_of(Vagrant::Registry) end describe "configs" do it "should have configs" do expect(subject.configs).to be_kind_of(Hash) end it "should default the values to registries" do expect(subject.configs[:i_probably_dont_exist]).to be_kind_of(Vagrant::Registry) end end end ================================================ FILE: test/unit/vagrant/plugin/v2/config_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V2::Config do include_context "unit" let(:foo_class) do Class.new(described_class) do attr_accessor :one attr_accessor :two end end let(:unset_value) { described_class.const_get("UNSET_VALUE") } subject { foo_class.new } describe "#merge" do it "should merge by default by simply copying each instance variable" do one = foo_class.new one.one = 2 one.two = 1 two = foo_class.new two.two = 5 result = one.merge(two) expect(result.one).to eq(2) expect(result.two).to eq(5) end it "prefers any set value over an UNSET_VALUE" do one = foo_class.new one.one = 1 one.two = 2 two = foo_class.new two.one = unset_value two.two = 5 result = one.merge(two) expect(result.one).to eq(1) expect(result.two).to eq(5) end it "doesn't merge values that start with a double underscore" do one = foo_class.new one.one = 1 one.two = 1 one.instance_variable_set(:@__bar, "one") two = foo_class.new two.two = 2 two.instance_variable_set(:@__bar, "two") # Merge and verify result = one.merge(two) expect(result.one).to eq(1) expect(result.two).to eq(2) expect(result.instance_variable_get(:@__bar)).to be_nil end end describe "#method_missing" do it "returns a DummyConfig object" do expect(subject.i_should_not_exist). to be_kind_of(Vagrant::Config::V2::DummyConfig) end it "raises an error if finalized (internally)" do subject._finalize! expect { subject.i_should_not_exist }. to raise_error(NoMethodError) end it "should survive being the last arg to a method that captures kwargs without a ruby conversion error" do arg_capturer = lambda { |*args, **kwargs| } expect { arg_capturer.call(subject) }.to_not raise_error end end end ================================================ FILE: test/unit/vagrant/plugin/v2/host_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V2::Host do # No tests. end ================================================ FILE: test/unit/vagrant/plugin/v2/manager_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V2::Manager do include_context "unit" let(:instance) { described_class.new } def plugin p = Class.new(Vagrant.plugin("2")) yield p p end describe "#generate_hook_keys" do it "should return array with one value" do expect(subject.generate_hook_keys(:test_value)).to eq(["test_value"]) end it "should return array with two values when key is camel cased" do result = subject.generate_hook_keys("TestValue") expect(result.size).to eq(2) expect(result).to include("TestValue") expect(result).to include("test_value") end it "should handle class/module value" do result = subject.generate_hook_keys(Vagrant) expect(result.size).to eq(2) expect(result).to include("Vagrant") expect(result).to include("vagrant") end it "should handle namespaced value" do result = subject.generate_hook_keys(Vagrant::Plugin) expect(result.size).to eq(4) expect(result).to include("Vagrant::Plugin") expect(result).to include("Plugin") expect(result).to include("vagrant_plugin") expect(result).to include("plugin") end end describe "#find_action_hooks" do let(:hook_name) { "Vagrant::Plugin" } before do h_name = hook_name pA = plugin do |p| p.action_hook(:test, h_name) { "hook_called" } end subject.register(pA) end it "should find hook with full namespace" do hooks = subject.find_action_hooks(Vagrant::Plugin) expect(hooks).not_to be_empty expect(hooks.first.call).to eq("hook_called") end it "should not find hook with short class name" do hooks = subject.find_action_hooks("Plugin") expect(hooks).to be_empty end it "should not find hook with full snake cased name" do hooks = subject.find_action_hooks(:vagrant_plugin) expect(hooks).to be_empty end it "should not find hook with short snake cased name" do hooks = subject.find_action_hooks("plugin") expect(hooks).to be_empty end context "when hook uses full snake cased name" do let(:hook_name) { :vagrant_plugin } it "should find hook with full namespace" do hooks = subject.find_action_hooks(Vagrant::Plugin) expect(hooks).not_to be_empty expect(hooks.first.call).to eq("hook_called") end it "should find hook with full snake cased name" do hooks = subject.find_action_hooks(:vagrant_plugin) expect(hooks).not_to be_empty expect(hooks.first.call).to eq("hook_called") end it "should not find hook with short class name" do hooks = subject.find_action_hooks("Plugin") expect(hooks).to be_empty end it "should not find hook with short snake cased name" do hooks = subject.find_action_hooks("plugin") expect(hooks).to be_empty end end context "when hook uses short snake cased name" do let(:hook_name) { :plugin } it "should find hook with full namespace" do hooks = subject.find_action_hooks(Vagrant::Plugin) expect(hooks).not_to be_empty expect(hooks.first.call).to eq("hook_called") end it "should find hook with short class name" do hooks = subject.find_action_hooks("Plugin") expect(hooks).not_to be_empty expect(hooks.first.call).to eq("hook_called") end it "should find hook with short snake cased name" do hooks = subject.find_action_hooks("plugin") expect(hooks).not_to be_empty expect(hooks.first.call).to eq("hook_called") end it "should not find hook with full snake cased name" do hooks = subject.find_action_hooks(:vagrant_plugin) expect(hooks).to be_empty end end end describe "#action_hooks" do it "should contain globally registered hooks" do pA = plugin do |p| p.action_hook("foo") { "bar" } end pB = plugin do |p| p.action_hook("bar") { "baz" } end instance.register(pA) instance.register(pB) result = instance.action_hooks(nil) expect(result.length).to eq(2) expect(result[0].call).to eq("bar") expect(result[1].call).to eq("baz") end it "should contain specific hooks with globally registered hooks" do pA = plugin do |p| p.action_hook("foo") { "bar" } p.action_hook("foo", :foo) { "bar_foo" } p.action_hook("foo", :bar) { "bar_bar" } end pB = plugin do |p| p.action_hook("bar") { "baz" } end instance.register(pA) instance.register(pB) result = instance.action_hooks(:foo) expect(result.length).to eq(3) expect(result[0].call).to eq("bar") expect(result[1].call).to eq("bar_foo") expect(result[2].call).to eq("baz") end end it "should enumerate registered communicator classes" do pA = plugin do |p| p.communicator("foo") { "bar" } end pB = plugin do |p| p.communicator("bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.communicators.to_hash.length).to eq(2) expect(instance.communicators[:foo]).to eq("bar") expect(instance.communicators[:bar]).to eq("baz") end it "should enumerate registered configuration classes" do pA = plugin do |p| p.config("foo") { "bar" } end pB = plugin do |p| p.config("bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.config.to_hash.length).to eq(2) expect(instance.config[:foo]).to eq("bar") expect(instance.config[:bar]).to eq("baz") end it "should enumerate registered guest classes" do pA = plugin do |p| p.guest("foo") { "bar" } end pB = plugin do |p| p.guest("bar", "foo") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.guests.to_hash.length).to eq(2) expect(instance.guests[:foo]).to eq(["bar", nil]) expect(instance.guests[:bar]).to eq(["baz", :foo]) end it "should enumerate registered guest capabilities" do pA = plugin do |p| p.guest_capability("foo", "foo") { "bar" } end pB = plugin do |p| p.guest_capability("bar", "foo") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.guest_capabilities.length).to eq(2) expect(instance.guest_capabilities[:foo][:foo]).to eq("bar") expect(instance.guest_capabilities[:bar][:foo]).to eq("baz") end it "should enumerate registered host classes" do pA = plugin do |p| p.host("foo") { "bar" } end pB = plugin do |p| p.host("bar", "foo") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.hosts.to_hash.length).to eq(2) expect(instance.hosts[:foo]).to eq(["bar", nil]) expect(instance.hosts[:bar]).to eq(["baz", :foo]) end it "should enumerate registered host capabilities" do pA = plugin do |p| p.host_capability("foo", "foo") { "bar" } end pB = plugin do |p| p.host_capability("bar", "foo") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.host_capabilities.length).to eq(2) expect(instance.host_capabilities[:foo][:foo]).to eq("bar") expect(instance.host_capabilities[:bar][:foo]).to eq("baz") end it "should enumerate registered provider classes" do pA = plugin do |p| p.provider("foo") { "bar" } end pB = plugin do |p| p.provider("bar", foo: "bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.providers.to_hash.length).to eq(2) expect(instance.providers[:foo]).to eq(["bar", { priority: 5 }]) expect(instance.providers[:bar]).to eq(["baz", { foo: "bar", priority: 5 }]) end it "provides the collection of registered provider configs" do pA = plugin do |p| p.config("foo", :provider) { "foo" } end pB = plugin do |p| p.config("bar", :provider) { "bar" } p.config("baz") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.provider_configs.to_hash.length).to eq(2) expect(instance.provider_configs[:foo]).to eq("foo") expect(instance.provider_configs[:bar]).to eq("bar") end it "should enumerate registered push classes" do pA = plugin do |p| p.push("foo") { "bar" } end pB = plugin do |p| p.push("bar", foo: "bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.pushes.to_hash.length).to eq(2) expect(instance.pushes[:foo]).to eq(["bar", nil]) expect(instance.pushes[:bar]).to eq(["baz", { foo: "bar" }]) end it "provides the collection of registered push configs" do pA = plugin do |p| p.config("foo", :push) { "foo" } end pB = plugin do |p| p.config("bar", :push) { "bar" } p.config("baz") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.push_configs.to_hash.length).to eq(2) expect(instance.push_configs[:foo]).to eq("foo") expect(instance.push_configs[:bar]).to eq("bar") end it "should enumerate all registered synced folder implementations" do pA = plugin do |p| p.synced_folder("foo") { "bar" } end pB = plugin do |p| p.synced_folder("bar", 50) { "baz" } end instance.register(pA) instance.register(pB) expect(instance.synced_folders.to_hash.length).to eq(2) expect(instance.synced_folders[:foo]).to eq(["bar", 10]) expect(instance.synced_folders[:bar]).to eq(["baz", 50]) end it "should enumerate registered synced_folder_capabilities classes" do pA = plugin do |p| p.synced_folder_capability("foo", "foo") { "bar" } end pB = plugin do |p| p.synced_folder_capability("bar", "bar") { "baz" } end instance.register(pA) instance.register(pB) expect(instance.synced_folder_capabilities.to_hash.length).to eq(2) expect(instance.synced_folder_capabilities[:foo][:foo]).to eq("bar") expect(instance.synced_folder_capabilities[:bar][:bar]).to eq("baz") end end ================================================ FILE: test/unit/vagrant/plugin/v2/plugin_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V2::Plugin do before do allow(described_class).to receive(:manager) .and_return(Vagrant::Plugin::V2::Manager.new) end it "should be able to set and get the name" do plugin = Class.new(described_class) do name "foo" end expect(plugin.name).to eq("foo") end it "should be able to set and get the description" do plugin = Class.new(described_class) do description "bar" end expect(plugin.description).to eq("bar") end describe "action hooks" do it "should register on all actions by default" do plugin = Class.new(described_class) do action_hook("foo") { "bar" } end hooks_registry = plugin.components.action_hooks hooks = hooks_registry[described_class.const_get("ALL_ACTIONS")] expect(hooks.length).to eq(1) expect(hooks[0].call).to eq("bar") end it "should register for a specific action by default" do plugin = Class.new(described_class) do action_hook("foo", :bar) { "bar" } end hooks_registry = plugin.components.action_hooks hooks = hooks_registry[:bar] expect(hooks.length).to eq(1) expect(hooks[0].call).to eq("bar") end end describe "commands" do it "should register command classes" do plugin = Class.new(described_class) do command("foo") { "bar" } end expect(plugin.components.commands.keys).to be_include(:foo) expect(plugin.components.commands[:foo][0].call).to eql("bar") end it "should register command classes with options" do plugin = Class.new(described_class) do command("foo", opt: :bar) { "bar" } end expect(plugin.components.commands.keys).to be_include(:foo) expect(plugin.components.commands[:foo][0].call).to eql("bar") expect(plugin.components.commands[:foo][1][:opt]).to eql(:bar) end it "should register commands as primary by default" do plugin = Class.new(described_class) do command("foo") { "bar" } command("bar", primary: false) { "bar" } end expect(plugin.components.commands[:foo][1][:primary]).to be(true) expect(plugin.components.commands[:bar][1][:primary]).to be(false) end ["spaces bad", "sym^bols"].each do |bad| it "should not allow bad command name: #{bad}" do plugin = Class.new(described_class) expect { plugin.command(bad) {} }. to raise_error(Vagrant::Plugin::V2::InvalidCommandName) end end it "should lazily register command classes" do # Below would raise an error if the value of the command class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do command("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the command key that # a proper error is raised. expect { plugin.components.commands[:foo][0].call }.to raise_error(StandardError, "FAIL!") end end describe "communicators" do it "should register communicator classes" do plugin = Class.new(described_class) do communicator("foo") { "bar" } end expect(plugin.communicator[:foo]).to eq("bar") end it "should lazily register communicator classes" do # Below would raise an error if the value of the class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do communicator("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.communicator[:foo] }.to raise_error(StandardError) end end describe "configuration" do it "should register configuration classes" do plugin = Class.new(described_class) do config("foo") { "bar" } end expect(plugin.components.configs[:top][:foo]).to eq("bar") end it "should lazily register configuration classes" do # Below would raise an error if the value of the config class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do config("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.components.configs[:top][:foo] }.to raise_error(StandardError) end it "should register configuration classes for providers" do plugin = Class.new(described_class) do config("foo", :provider) { "bar" } end expect(plugin.components.configs[:provider][:foo]).to eq("bar") end end describe "guests" do it "should register guest classes" do plugin = Class.new(described_class) do guest("foo") { "bar" } end expect(plugin.components.guests[:foo]).to eq(["bar", nil]) end it "should lazily register guest classes" do # Below would raise an error if the value of the guest class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do guest("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the guest key that # a proper error is raised. expect { plugin.guest[:foo] }.to raise_error(StandardError) end end describe "guest capabilities" do it "should register guest capabilities" do plugin = Class.new(described_class) do guest_capability("foo", "bar") { "baz" } end expect(plugin.components.guest_capabilities[:foo][:bar]).to eq("baz") end end describe "hosts" do it "should register host classes" do plugin = Class.new(described_class) do host("foo") { "bar" } end expect(plugin.components.hosts[:foo]).to eq(["bar", nil]) end it "should lazily register host classes" do # Below would raise an error if the value of the host class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do host("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the host key that # a proper error is raised. expect { plugin.host[:foo] }.to raise_error(StandardError) end end describe "host capabilities" do it "should register host capabilities" do plugin = Class.new(described_class) do host_capability("foo", "bar") { "baz" } end expect(plugin.components.host_capabilities[:foo][:bar]).to eq("baz") end end describe "providers" do it "should register provider classes" do plugin = Class.new(described_class) do provider("foo") { "bar" } end result = plugin.components.providers[:foo] expect(result[0]).to eq("bar") expect(result[1][:priority]).to eq(5) end it "should register provider classes with options" do plugin = Class.new(described_class) do provider("foo", foo: "yep") { "bar" } end result = plugin.components.providers[:foo] expect(result[0]).to eq("bar") expect(result[1][:priority]).to eq(5) expect(result[1][:foo]).to eq("yep") end it "should lazily register provider classes" do # Below would raise an error if the value of the config class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do provider("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.components.providers[:foo] }.to raise_error(StandardError) end end describe "provider capabilities" do it "should register host capabilities" do plugin = Class.new(described_class) do provider_capability("foo", "bar") { "baz" } end expect(plugin.components.provider_capabilities[:foo][:bar]).to eq("baz") end end describe "provisioners" do it "should register provisioner classes" do plugin = Class.new(described_class) do provisioner("foo") { "bar" } end expect(plugin.provisioner[:foo]).to eq("bar") end it "should lazily register provisioner classes" do # Below would raise an error if the value of the config class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do provisioner("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.provisioner[:foo] }.to raise_error(StandardError) end end describe "pushes" do it "should register implementations" do plugin = Class.new(described_class) do push("foo") { "bar" } end expect(plugin.components.pushes[:foo]).to eq(["bar", nil]) end it "should be able to specify priorities" do plugin = Class.new(described_class) do push("foo", bar: 1) { "bar" } end expect(plugin.components.pushes[:foo]).to eq(["bar", bar: 1]) end it "should lazily register implementations" do # Below would raise an error if the value of the config class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do push("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.components.pushes[:foo] }.to raise_error(StandardError) end end describe "synced folders" do it "should register implementations" do plugin = Class.new(described_class) do synced_folder("foo") { "bar" } end expect(plugin.components.synced_folders[:foo]).to eq(["bar", 10]) end it "should be able to specify priorities" do plugin = Class.new(described_class) do synced_folder("foo", 50) { "bar" } end expect(plugin.components.synced_folders[:foo]).to eq(["bar", 50]) end it "should lazily register implementations" do # Below would raise an error if the value of the config class was # evaluated immediately. By asserting that this does not raise an # error, we verify that the value is actually lazily loaded plugin = nil expect { plugin = Class.new(described_class) do synced_folder("foo") { raise StandardError, "FAIL!" } end }.to_not raise_error # Now verify when we actually get the configuration key that # a proper error is raised. expect { plugin.components.synced_folders[:foo] }.to raise_error(StandardError) end end describe "plugin registration" do let(:manager) { described_class.manager } it "should have no registered plugins" do expect(manager.registered).to be_empty end it "should register a plugin when a name is set" do plugin = Class.new(described_class) do name "foo" end expect(manager.registered).to eq([plugin]) end it "should register a plugin only once" do plugin = Class.new(described_class) do name "foo" name "bar" end expect(manager.registered).to eq([plugin]) end end end ================================================ FILE: test/unit/vagrant/plugin/v2/provider_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V2::Provider do include_context "unit" let(:machine) { Object.new } let(:instance) { described_class.new(machine) } subject { instance } it "should be usable by default" 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 it "should return nil by default for ssh info" do expect(instance.ssh_info).to be_nil end it "should return nil by default for state" do expect(instance.state).to be_nil end context "capabilities" do before do register_plugin("2") do |p| p.provider_capability("bar", "foo") {} p.provider_capability("foo", "bar") do Class.new do def self.bar(machine) raise "bar #{machine.id}" end end end end allow(machine).to receive(:id).and_return("YEAH") instance._initialize("foo", machine) end it "can execute capabilities" do expect(subject.capability?(:foo)).to be(false) expect(subject.capability?(:bar)).to be(true) expect { subject.capability(:bar) }. to raise_error("bar YEAH") end end end ================================================ FILE: test/unit/vagrant/plugin/v2/synced_folder_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) describe Vagrant::Plugin::V2::SyncedFolder::Collection do include_context "unit" let(:folders) { described_class[ :nfs=> {"/other"=> {:type=>:nfs, :guestpath=>"/other", :hostpath=>"/other", :disabled=>false, :__vagrantfile=>true, plugin:"someclass"}, "/tests"=> {:type=>:nfs, :guestpath=>"/tests", :hostpath=>"/tests", :disabled=>false, :__vagrantfile=>true, plugin:"someclass"}}, :virtualbox=> {"/vagrant"=> {:guestpath=>"/vagrant", :hostpath=>"/vagrant", :disabled=>false, :__vagrantfile=>true, plugin:"someotherclass"}} ]} describe "#types" do it "gets all the types of synced folders" do expect(folders.types).to eq([:nfs, :virtualbox]) end end describe "#type" do it "returns the plugin for a type" do expect(folders.type(:nfs)).to eq("someclass") expect(folders.type(:virtualbox)).to eq("someotherclass") end end describe "to_h" do it "removed plugin key" do original_folders = folders folders_h = folders.to_h folders_h.values.each do |v| v.values.each do |w| expect(w).not_to include(:plugin) end end original_folders.values.each do |v| v.values.each do |w| expect(w).to include(:plugin) end end end end end ================================================ FILE: test/unit/vagrant/plugin/v2/trigger_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../../base", __FILE__) require Vagrant.source_root.join("plugins/kernel_v2/config/trigger") describe Vagrant::Plugin::V2::Trigger do include_context "unit" let(:iso_env) do # We have to create a Vagrantfile so there is a root path isolated_environment.tap do |env| env.vagrantfile("") end end let(:iso_vagrant_env) { iso_env.create_vagrant_env } let(:state) { double("state", id: :running) } let(:machine) do iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy).tap do |m| allow(m).to receive(:state).and_return(state) end end let(:ui) { Vagrant::UI::Silent.new } let(:env) { { machine: machine, ui: ui, } } let(:triggers) { @triggers ||= VagrantPlugins::Kernel_V2::TriggerConfig.new.tap do |triggers| triggers.before(:up, hash_block) triggers.before(:destroy, hash_block) triggers.before(:halt, hash_block_two) triggers.after(:up, hash_block) triggers.after(:destroy, hash_block) triggers.finalize! end } let(:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} } let(:hash_block_two) { {warn: "WARNING!!", run_remote: {inline: "echo 'hi'"}} } let(:subject) { described_class.new(env, triggers, machine, ui) } describe "#fire" do it "raises an error if an improper stage is given" do expect{ subject.fire(:up, :not_real, "guest", :action) }. to raise_error(Vagrant::Errors::TriggersNoStageGiven) end it "does not fire triggers if community plugin is detected" do allow(subject).to receive(:community_plugin_detected?).and_return(true) expect(subject).not_to receive(:execute) subject.fire(:up, :before, "guest", :action) end it "does fire triggers if community plugin is not detected" do allow(subject).to receive(:community_plugin_detected?).and_return(false) expect(subject).to receive(:execute) subject.fire(:up, :before, "guest", :action) end end describe "#find" do it "raises an error if an improper stage is given" do expect { subject.find(:up, :not_real, "guest", :action) }. to raise_error(Vagrant::Errors::TriggersNoStageGiven) end it "returns empty array when no triggers are found" do expect(subject.find(:halt, :after, "guest", :action)).to be_empty end it "returns items in array when triggers are found" do expect(subject.find(:halt, :before, "guest", :action)).not_to be_empty end it "returns the execpted number of items in the array when triggers are found" do expect(subject.find(:halt, :before, "guest", :action).count).to eq(1) end it "filters all found triggers" do expect(subject).to receive(:filter_triggers) subject.find(:halt, :before, "guest", :action) end it "should not attempt to match hook name with non-hook type" do expect(subject).not_to receive(:matched_hook?) subject.find(:halt, :before, "guest", :action) end context "with :all special value" do let(:triggers) { VagrantPlugins::Kernel_V2::TriggerConfig.new } let(:ignores) { [] } before do triggers.before(:all, hash_block.merge(ignore: ignores)) triggers.after(:all, hash_block.merge(ignore: ignores)) triggers.finalize! end [:destroy, :halt, :provision, :reload, :resume, :suspend, :up].each do |supported_action| it "should locate trigger when before #{supported_action} action is requested" do expect(subject.find(supported_action, :before, "guest", :action, all: true)).not_to be_empty end it "should locate trigger when after #{supported_action} action is requested" do expect(subject.find(supported_action, :after, "guest", :action, all: true)).not_to be_empty end end context "with ignores" do let(:ignores) { [:halt, :up] } it "should not locate trigger when before command is ignored" do expect(subject.find(:up, :before, "guest", :action, all: true)).to be_empty end it "should not locate trigger when after command is ignored" do expect(subject.find(:halt, :after, "guest", :action, all: true)).to be_empty end it "should locate trigger when before command is not ignored" do expect(subject.find(:provision, :before, "guest", :action, all: true)).not_to be_empty end it "should locate trigger when after command is not ignored" do expect(subject.find(:provision, :after, "guest", :action, all: true)).not_to be_empty end end end context "with hook type" do before do triggers.before(:environment_load, hash_block.merge(type: :hook)) triggers.before(Vagrant::Action::Builtin::SyncedFolders, hash_block.merge(type: :hook)) triggers.finalize! end it "returns empty array when no triggers are found" do expect(subject.find(:environment_unload, :before, "guest", :hook)).to be_empty end it "returns items in array when triggers are found" do expect(subject.find(:environment_load, :before, "guest", :hook).size).to eq(1) end it "should locate hook trigger using class constant" do expect(subject.find(Vagrant::Action::Builtin::SyncedFolders, :before, "guest", :hook)). not_to be_empty end it "should locate hook trigger using string" do expect(subject.find("environment_load", :before, "guest", :hook)).not_to be_empty end it "should locate hook trigger using full converted name" do expect(subject.find(:vagrant_action_builtin_synced_folders, :before, "guest", :hook)). not_to be_empty end it "should locate hook trigger using partial suffix converted name" do expect(subject.find(:builtin_synced_folders, :before, "guest", :hook)). not_to be_empty end it "should not locate hook trigger using partial prefix converted name" do expect(subject.find(:vagrant_action, :before, "guest", :hook)). to be_empty end end end describe "#filter_triggers" do it "returns all triggers if no constraints" do before_triggers = triggers.before_triggers filtered_triggers = subject.send(:filter_triggers, before_triggers, "guest", :action) expect(filtered_triggers).to eq(before_triggers) end it "filters a trigger if it doesn't match guest_name" do trigger_config = {info: "no", only_on: "notrealguest"} triggers.after(:up, trigger_config) triggers.finalize! after_triggers = triggers.after_triggers expect(after_triggers.size).to eq(3) subject.send(:filter_triggers, after_triggers, :ubuntu, :action) expect(after_triggers.size).to eq(2) end it "keeps a trigger that has a restraint that matches guest name" do trigger_config = {info: "no", only_on: /guest/} triggers.after(:up, trigger_config) triggers.finalize! after_triggers = triggers.after_triggers expect(after_triggers.size).to eq(3) subject.send(:filter_triggers, after_triggers, "ubuntu-guest", :action) expect(after_triggers.size).to eq(3) end it "keeps a trigger that has multiple restraints that matches guest name" do trigger_config = {info: "no", only_on: ["debian", /guest/]} triggers.after(:up, trigger_config) triggers.finalize! after_triggers = triggers.after_triggers expect(after_triggers.size).to eq(3) subject.send(:filter_triggers, after_triggers, "ubuntu-guest", :action) expect(after_triggers.size).to eq(3) end end describe "#execute" do it "calls the corresponding trigger methods if options set" do expect(subject).to receive(:info).twice expect(subject).to receive(:warn).once expect(subject).to receive(:run).twice expect(subject).to receive(:run_remote).once subject.send(:execute, triggers.before_triggers) end end describe "#info" do let(:message) { "Printing some info" } it "prints messages at INFO" do expect(ui).to receive(:info).with(message).and_call_original subject.send(:info, message) end end describe "#warn" do let(:message) { "Printing some warnings" } it "prints messages at WARN" do expect(ui).to receive(:warn).with(message).and_call_original subject.send(:warn, message) end end describe "#run" do let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new } let(:shell_block) { {info: "hi", run: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} } let(:shell_block_exit_codes) { {info: "hi", run: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}, exit_codes: [0,50]} } let(:path_block) { {warn: "bye", run: {path: "path/to the/script.sh", args: "HELLO", env: {"KEY"=>"VALUE"}}, on_error: :continue} } let(:path_block_ps1) { {warn: "bye", run: {path: "script.ps1", args: ["HELLO", "THERE"], env: {"KEY"=>"VALUE"}}, on_error: :continue} } let(:exit_code) { 0 } let(:options) { {:notify=>[:stdout, :stderr]} } let(:subprocess_result) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(exit_code) allow(result).to receive(:stderr).and_return("") end end let(:subprocess_result_failure) do double("subprocess_result_failure").tap do |result| allow(result).to receive(:exit_code).and_return(1) allow(result).to receive(:stderr).and_return("") end end let(:subprocess_result_custom) do double("subprocess_result_custom").tap do |result| allow(result).to receive(:exit_code).and_return(50) allow(result).to receive(:stderr).and_return("") end end before do trigger_run.after(:up, shell_block) trigger_run.after(:up, shell_block_exit_codes) trigger_run.before(:destroy, path_block) trigger_run.before(:destroy, path_block_ps1) trigger_run.finalize! end it "executes an inline script with powershell if windows" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) allow(Vagrant::Util::PowerShell).to receive(:execute_inline). and_yield(:stderr, "some output"). and_return(subprocess_result) trigger = trigger_run.after_triggers.first shell_config = trigger.run on_error = trigger.on_error exit_codes = trigger.exit_codes expect(Vagrant::Util::PowerShell).to receive(:execute_inline). with("echo 'hi'", options) subject.send(:run, shell_config, on_error, exit_codes) end it "executes a path script with powershell if windows" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) allow(Vagrant::Util::PowerShell).to receive(:execute). and_yield(:stdout, "more output"). and_return(subprocess_result) allow(env).to receive(:root_path).and_return("/vagrant/home") trigger = trigger_run.before_triggers[1] shell_config = trigger.run on_error = trigger.on_error exit_codes = trigger.exit_codes expect(Vagrant::Util::PowerShell).to receive(:execute). with("/vagrant/home/script.ps1", "HELLO", "THERE", options) subject.send(:run, shell_config, on_error, exit_codes) end it "executes an inline script" do allow(Vagrant::Util::Subprocess).to receive(:execute). and_return(subprocess_result) trigger = trigger_run.after_triggers.first shell_config = trigger.run on_error = trigger.on_error exit_codes = trigger.exit_codes expect(Vagrant::Util::Subprocess).to receive(:execute). with("echo", "hi", options) subject.send(:run, shell_config, on_error, exit_codes) end it "executes a path script" do allow(Vagrant::Util::Subprocess).to receive(:execute). and_return(subprocess_result) allow(env).to receive(:root_path).and_return("/vagrant/home") allow(FileUtils).to receive(:chmod).and_return(true) trigger = trigger_run.before_triggers.first shell_config = trigger.run on_error = trigger.on_error exit_codes = trigger.exit_codes expect(Vagrant::Util::Subprocess).to receive(:execute). with("/vagrant/home/path/to the/script.sh", "HELLO", options) subject.send(:run, shell_config, on_error, exit_codes) end it "continues on error" do allow(Vagrant::Util::Subprocess).to receive(:execute). and_raise("Fail!") allow(env).to receive(:root_path).and_return("/vagrant/home") allow(FileUtils).to receive(:chmod).and_return(true) trigger = trigger_run.before_triggers.first shell_config = trigger.run on_error = trigger.on_error exit_codes = trigger.exit_codes expect(Vagrant::Util::Subprocess).to receive(:execute). with("/vagrant/home/path/to the/script.sh", "HELLO", options) subject.send(:run, shell_config, on_error, exit_codes) end it "halts on error" do allow(Vagrant::Util::Subprocess).to receive(:execute). and_raise("Fail!") trigger = trigger_run.after_triggers.first shell_config = trigger.run on_error = trigger.on_error exit_codes = trigger.exit_codes expect(Vagrant::Util::Subprocess).to receive(:execute). with("echo", "hi", options) expect { subject.send(:run, shell_config, on_error, exit_codes) }.to raise_error("Fail!") end it "allows for acceptable exit codes" do allow(Vagrant::Util::Subprocess).to receive(:execute). and_return(subprocess_result_custom) trigger = trigger_run.after_triggers[1] shell_config = trigger.run on_error = trigger.on_error exit_codes = trigger.exit_codes expect(Vagrant::Util::Subprocess).to receive(:execute). with("echo", "hi", options) subject.send(:run, shell_config, on_error, exit_codes) end it "exits if given a bad exit code" do allow(Vagrant::Util::Subprocess).to receive(:execute). and_return(subprocess_result_custom) trigger = trigger_run.after_triggers.first shell_config = trigger.run on_error = trigger.on_error exit_codes = trigger.exit_codes expect(Vagrant::Util::Subprocess).to receive(:execute). with("echo", "hi", options) expect { subject.send(:run, shell_config, on_error, exit_codes) }.to raise_error(Vagrant::Errors::TriggersBadExitCodes) end end describe "#run_remote" do let (:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new } let (:shell_block) { {info: "hi", run_remote: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} } let (:path_block) { {warn: "bye", run_remote: {path: "script.sh", env: {"KEY"=>"VALUE"}}, on_error: :continue} } let(:provision) { double("provision") } before do trigger_run.after(:up, shell_block) trigger_run.before(:destroy, path_block) trigger_run.finalize! end context "with no machine existing" do let(:machine) { nil } it "raises an error and halts if guest does not exist" do trigger = trigger_run.after_triggers.first shell_config = trigger.run_remote on_error = trigger.on_error exit_codes = trigger.exit_codes expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }. to raise_error(Vagrant::Errors::TriggersGuestNotExist) end it "continues on if guest does not exist but is configured to continue on error" do trigger = trigger_run.before_triggers.first shell_config = trigger.run_remote on_error = trigger.on_error exit_codes = trigger.exit_codes subject.send(:run_remote, shell_config, on_error, exit_codes) end end it "raises an error and halts if guest is not running" do allow(machine.state).to receive(:id).and_return(:not_running) trigger = trigger_run.after_triggers.first shell_config = trigger.run_remote on_error = trigger.on_error exit_codes = trigger.exit_codes expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }. to raise_error(Vagrant::Errors::TriggersGuestNotRunning) end it "continues on if guest is not running but is configured to continue on error" do allow(machine.state).to receive(:id).and_return(:not_running) allow(env).to receive(:root_path).and_return("/vagrant/home") allow(FileUtils).to receive(:chmod).and_return(true) trigger = trigger_run.before_triggers.first shell_config = trigger.run_remote on_error = trigger.on_error exit_codes = trigger.exit_codes subject.send(:run_remote, shell_config, on_error, exit_codes) end it "calls the provision function on the shell provisioner" do allow(machine.state).to receive(:id).and_return(:running) allow(provision).to receive(:provision).and_return("Provision!") allow(VagrantPlugins::Shell::Provisioner).to receive(:new). and_return(provision) trigger = trigger_run.after_triggers.first shell_config = trigger.run_remote on_error = trigger.on_error exit_codes = trigger.exit_codes subject.send(:run_remote, shell_config, on_error, exit_codes) end it "continues on if provision fails" do allow(machine.state).to receive(:id).and_return(:running) allow(provision).to receive(:provision).and_raise("Nope!") allow(VagrantPlugins::Shell::Provisioner).to receive(:new). and_return(provision) trigger = trigger_run.before_triggers.first shell_config = trigger.run_remote on_error = trigger.on_error exit_codes = trigger.exit_codes subject.send(:run_remote, shell_config, on_error, exit_codes) end it "fails if it encounters an error" do allow(machine.state).to receive(:id).and_return(:running) allow(provision).to receive(:provision).and_raise("Nope!") allow(VagrantPlugins::Shell::Provisioner).to receive(:new). and_return(provision) trigger = trigger_run.after_triggers.first shell_config = trigger.run_remote on_error = trigger.on_error exit_codes = trigger.exit_codes expect { subject.send(:run_remote, shell_config, on_error, exit_codes) }. to raise_error("Nope!") end end describe "#trigger_abort" do it "system exits when called" do allow(Process).to receive(:exit!).and_return(true) expect(Process).to receive(:exit!).with(3) subject.send(:trigger_abort, 3) end context "when running in parallel" do let(:thread) { @t ||= Thread.new do Thread.current[:batch_parallel_action] = true Thread.stop subject.send(:trigger_abort, exit_code) end } let(:exit_code) { 22 } before do expect(Process).not_to receive(:exit!) sleep(0.1) until thread.stop? end after { @t = nil } it "should terminate the thread" do expect(thread).to receive(:terminate).and_call_original thread.wakeup thread.join(1) while thread.alive? end it "should set the exit code into the thread data" do expect(thread).to receive(:terminate).and_call_original thread.wakeup thread.join(1) while thread.alive? expect(thread[:exit_code]).to eq(exit_code) end end end describe "#ruby" do let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new } let(:block) { proc{var = 1+1} } let(:ruby_trigger) { {info: "hi", ruby: block} } before do trigger_run.after(:up, ruby_trigger) trigger_run.finalize! end it "executes a ruby block" do expect(block).to receive(:call) subject.send(:execute_ruby, block) end end describe "#nameify" do it "should return empty string when object" do expect(subject.send(:nameify, "")).to eq("") end it "should return name of class" do expect(subject.send(:nameify, String)).to eq("String") end it "should return empty string when class has no name" do expect(subject.send(:nameify, Class.new)).to eq("") end end end ================================================ FILE: test/unit/vagrant/registry_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) describe Vagrant::Registry do let(:instance) { described_class.new } it "should return nil for nonexistent items" do expect(instance.get("foo")).to be_nil end it "should register a simple key/value" do instance.register("foo") { "value" } expect(instance.get("foo")).to eq("value") end it "should register an item without calling the block yet" do expect do instance.register("foo") do raise Exception, "BOOM!" end end.to_not raise_error end it "should raise an error if no block is given" do expect { instance.register("foo") }. to raise_error(ArgumentError) end it "should call and return the result of a block when asking for the item" do object = Object.new instance.register("foo") do object end expect(instance.get("foo")).to eql(object) end it "should be able to get the item with []" do object = Object.new instance.register("foo") { object } expect(instance["foo"]).to eql(object) end it "should be able to get keys with #keys" do instance.register("foo") { "bar" } instance.register("baz") { raise "BOOM" } expect(instance.keys.sort).to eq([ 'baz', 'foo' ]) end it "should cache the result of the item so they can be modified" do # Make the proc generate a NEW array each time instance.register("foo") { [] } # Test that modifying the result modifies the actual cached # value. This verifies we're caching. expect(instance.get("foo")).to eq([]) instance.get("foo") << "value" expect(instance.get("foo")).to eq(["value"]) end it "should be able to check if a key exists" do instance.register("foo") { "bar" } expect(instance).to have_key("foo") expect(instance).not_to have_key("bar") end it "should be enumerable" do instance.register("foo") { "foovalue" } instance.register("bar") { "barvalue" } keys = [] values = [] instance.each do |key, value| keys << key values << value end expect(keys.sort).to eq(["bar", "foo"]) expect(values.sort).to eq(["barvalue", "foovalue"]) end it "should be able to convert to a hash" do instance.register("foo") { "foovalue" } instance.register("bar") { "barvalue" } result = instance.to_hash expect(result).to be_a(Hash) expect(result["foo"]).to eq("foovalue") expect(result["bar"]).to eq("barvalue") end describe "#length" do it "should return 0 when the registry is empty" do expect(instance.length).to eq(0) end it "should return the number of items in the registry" do instance.register("foo") { } instance.register("bar") { } expect(instance.length).to eq(2) end end describe "#size" do it "should be an alias to #length" do size = described_class.instance_method(:size) length = described_class.instance_method(:length) expect(size).to eq(length) end end describe "#empty" do it "should return true when the registry is empty" do expect(instance.empty?).to be(true) end it "should return false when there is at least one element" do instance.register("foo") { } expect(instance.empty?).to be(false) end end describe "merging" do it "should merge in another registry" do one = described_class.new two = described_class.new one.register("foo") { raise "BOOM!" } two.register("bar") { raise "BAM!" } three = one.merge(two) expect { three["foo"] }.to raise_error("BOOM!") expect { three["bar"] }.to raise_error("BAM!") end it "should NOT merge in the cache" do one = described_class.new two = described_class.new one.register("foo") { [] } one["foo"] << 1 two.register("bar") { [] } two["bar"] << 2 three = one.merge(two) expect(three["foo"]).to eq([]) expect(three["bar"]).to eq([]) end end describe "merge!" do it "should merge into self" do one = described_class.new two = described_class.new one.register("foo") { "foo" } two.register("bar") { "bar" } one.merge!(two) expect(one["foo"]).to eq("foo") expect(one["bar"]).to eq("bar") end end end ================================================ FILE: test/unit/vagrant/shared_helpers_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) require "vagrant/shared_helpers" require "vagrant/util/platform" describe Vagrant do include_context "unit" subject { described_class } describe ".global_lock" do it "yields to the block" do result = subject.global_lock { 42 } expect(result).to eq(42) end end describe ".in_installer?" do it "is not if env is not set" do with_temp_env("VAGRANT_INSTALLER_ENV" => nil) do expect(subject.in_installer?).to be(false) end end it "is if env is set" do with_temp_env("VAGRANT_INSTALLER_ENV" => "/foo") do expect(subject.in_installer?).to be(true) end end end describe ".installer_embedded_dir" do it "returns nil if not in an installer" do allow(Vagrant).to receive(:in_installer?).and_return(false) expect(subject.installer_embedded_dir).to be_nil end it "returns the set directory" do allow(Vagrant).to receive(:in_installer?).and_return(true) with_temp_env("VAGRANT_INSTALLER_EMBEDDED_DIR" => "/foo") do expect(subject.installer_embedded_dir).to eq("/foo") end end end describe ".plugins_enabled?" do it "returns true if the env is not set" do with_temp_env("VAGRANT_NO_PLUGINS" => nil) do expect(subject.plugins_enabled?).to be(true) end end it "returns false if the env is set" do with_temp_env("VAGRANT_NO_PLUGINS" => "1") do expect(subject.plugins_enabled?).to be(false) end end end describe ".server_url" do it "defaults to the default value" do with_temp_env("VAGRANT_SERVER_URL" => nil) do expect(subject.server_url).to eq( Vagrant::DEFAULT_SERVER_URL) end end it "defaults if the string is empty" do with_temp_env("VAGRANT_SERVER_URL" => "") do expect(subject.server_url).to eq( Vagrant::DEFAULT_SERVER_URL) end end it "is the VAGRANT_SERVER_URL value" do with_temp_env("VAGRANT_SERVER_URL" => "foo") do expect(subject.server_url).to eq("foo") end end it "is the VAGRANT_SERVER_URL value if the server url is configured" do with_temp_env("VAGRANT_SERVER_URL" => "foo") do expect(subject.server_url('bar')).to eq("foo") end end it "is the configured server url if VAGRANT_SERVER_URL is not set" do with_temp_env("VAGRANT_SERVER_URL" => nil) do expect(subject.server_url("bar")).to eq("bar") end end end describe ".user_data_path" do around do |example| env = { "USERPROFILE" => nil, "VAGRANT_HOME" => nil, } with_temp_env(env) { example.run } end it "defaults to ~/.vagrant.d" do expect(subject.user_data_path).to eql(Pathname.new("~/.vagrant.d").expand_path) end it "is VAGRANT_HOME if set" do with_temp_env("VAGRANT_HOME" => "/foo") do expected = Pathname.new("/foo").expand_path expect(subject.user_data_path).to eql(expected) end end it "is USERPROFILE/.vagrant.d if set" do with_temp_env("USERPROFILE" => "/bar") do expected = Pathname.new("/bar/.vagrant.d").expand_path expect(subject.user_data_path).to eql(expected) end end it "prefers VAGRANT_HOME over USERPROFILE if both are set" do env = { "USERPROFILE" => "/bar", "VAGRANT_HOME" => "/foo", } with_temp_env(env) do expected = Pathname.new("/foo").expand_path expect(subject.user_data_path).to eql(expected) end end end describe ".prerelease?" do it "should return true when Vagrant version is development" do stub_const("Vagrant::VERSION", "1.0.0.dev") expect(subject.prerelease?).to be(true) end it "should return false when Vagrant version is release" do stub_const("Vagrant::VERSION", "1.0.0") expect(subject.prerelease?).to be(false) end end describe ".allow_prerelease_dependencies?" do context "with environment variable set" do before { allow(ENV).to receive(:[]).with("VAGRANT_ALLOW_PRERELEASE").and_return("1") } it "should return true" do expect(subject.allow_prerelease_dependencies?).to be(true) end end context "with environment variable unset" do before { allow(ENV).to receive(:[]).with("VAGRANT_ALLOW_PRERELEASE").and_return(nil) } it "should return false" do expect(subject.allow_prerelease_dependencies?).to be(false) end end end describe ".enable_resolv_replace" do it "should not attempt to require resolv-replace by default" do expect(subject).not_to receive(:require).with("resolv-replace") subject.enable_resolv_replace end it "should require resolv-replace when VAGRANT_ENABLE_RESOLV_REPLACE is set" do expect(subject).to receive(:require).with("resolv-replace") with_temp_env("VAGRANT_ENABLE_RESOLV_REPLACE" => "1"){ subject.enable_resolv_replace } end it "should not require resolv-replace when VAGRANT_DISABLE_RESOLV_REPLACE is set" do expect(subject).not_to receive(:require).with("resolv-replace") with_temp_env("VAGRANT_ENABLE_RESOLV_REPLACE" => "1", "VAGRANT_DISABLE_RESOLV_REPLACE" => "1") do subject.enable_resolv_replace end end end describe ".global_logger" do after{ subject.global_logger = nil } it "should return a logger when none have been provided" do expect(subject.global_logger).not_to be_nil end it "should return previously set logger" do logger = double("logger") expect(subject.global_logger = logger).to eq(logger) expect(subject.global_logger).to eq(logger) end end describe ".add_default_cli_options" do it "should raise a type error when no provided with proc" do expect { subject.add_default_cli_options(true) }. to raise_error(TypeError) end it "should raise an argument error when proc does not accept argument" do expect { subject.add_default_cli_options(proc{}) }. to raise_error(ArgumentError) end it "should accept a proc type argument" do expect(subject.add_default_cli_options(proc{|o|})).to be_nil end end describe ".default_cli_options" do it "should return array of items" do expect(subject.default_cli_options).to be_a(Array) end end end ================================================ FILE: test/unit/vagrant/ui_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) describe Vagrant::UI::Basic do context "in general" do it "outputs within the a new thread" do current = Thread.current.object_id expect(subject).to receive(:safe_puts).with(any_args) { |*args| expect(Thread.current.object_id).to_not eq(current) true } subject.output("foo") end it "outputs using `puts` by default" do expect(subject).to receive(:safe_puts).with(any_args) { |message, opts| expect(opts[:printer]).to eq(:puts) true } subject.output("foo") end it "outputs using `print` if new_line is false" do expect(subject).to receive(:safe_puts).with(any_args) { |message, opts| expect(opts[:printer]).to eq(:print) true } subject.output("foo", new_line: false) end it "outputs using `print` if new_line is false" do expect(subject).to receive(:safe_puts).with(any_args) { |message, opts| expect(opts[:printer]).to eq(:print) true } subject.output("foo", new_line: false) end it "outputs to the assigned stdout" do stdout = StringIO.new subject.stdout = stdout expect(subject).to receive(:safe_puts).with(any_args) { |message, opts| expect(opts[:io]).to be(stdout) true } subject.output("foo") end it "outputs to stdout by default" do expect(subject.stdout).to be($stdout) end it "outputs to the assigned stderr for errors" do stderr = StringIO.new subject.stderr = stderr expect(subject).to receive(:safe_puts).with(any_args) { |message, opts| expect(opts[:io]).to be(stderr) true } subject.error("foo") end it "outputs to stderr for errors by default" do expect(subject.stderr).to be($stderr) end end context "#color?" do it "returns false" do expect(subject.color?).to be(false) end end context "#detail" do it "outputs details" do expect(subject).to receive(:safe_puts).with(any_args) { |message, opts| expect(message).to eq("foo") true } subject.detail("foo") end it "doesn't output details if disabled" do expect(subject).to receive(:safe_puts).never subject.opts[:hide_detail] = true subject.detail("foo") end end context "with sensitive data" do let(:password){ "my-birthday" } let(:output){ "You're password is: #{password}" } before{ Vagrant::Util::CredentialScrubber.sensitive(password) } it "should remove sensitive information from the output" do expect(subject).to receive(:safe_puts).with(any_args) do |message, opts| expect(message).not_to include(password) end subject.detail(output) end end context "#rewriting" do it "does output progress" do expect { |b| subject.rewriting(&b) }.to yield_control end end end describe Vagrant::UI::NonInteractive do describe "#ask" do it "raises an exception" do expect{subject.ask("foo")}.to raise_error(Vagrant::Errors::UIExpectsTTY) end end describe "#report_progress" do it "does not output progress" do expect(subject).to_not receive(:info) subject.report_progress(1, 1) end end describe "#rewriting" do it "does not output progress" do expect { |b| subject.rewriting(&b) }.to_not yield_control end end end describe Vagrant::UI::Colored do include_context "unit" describe "#color?" do it "returns true" do expect(subject.color?).to be(true) end end describe "#detail" do it "colors output nothing by default" do expect(subject).to receive(:safe_puts).with("\033[0mfoo\033[0m", anything) subject.detail("foo") end it "does not bold by default with a color" do expect(subject).to receive(:safe_puts).with(any_args) { |message, *args| expect(message).to start_with("\033[0;31m") expect(message).to end_with("\033[0m") } subject.detail("foo", color: :red) end end describe "#error" do it "colors red" do expect(subject).to receive(:safe_puts).with(any_args) { |message, *args| expect(message).to start_with("\033[0;31m") expect(message).to end_with("\033[0m") } subject.error("foo") end end describe "#output" do it "colors output nothing by default, no bold" do expect(subject).to receive(:safe_puts).with("\033[0mfoo\033[0m", anything) subject.output("foo") end it "doesn't use a color if default color" do expect(subject).to receive(:safe_puts).with("\033[0mfoo\033[0m", anything) subject.output("foo", color: :default) end it "bolds output without color if specified" do expect(subject).to receive(:safe_puts).with("\033[1mfoo\033[0m", anything) subject.output("foo", bold: true) end it "colors output to color specified in global opts" do subject.opts[:color] = :red expect(subject).to receive(:safe_puts).with(any_args) { |message, *args| expect(message).to start_with("\033[0;31m") expect(message).to end_with("\033[0m") } subject.output("foo") end it "colors output to specified color over global opts" do subject.opts[:color] = :red expect(subject).to receive(:safe_puts).with(any_args) { |message, *args| expect(message).to start_with("\033[0;32m") expect(message).to end_with("\033[0m") } subject.output("foo", color: :green) end it "bolds the output if specified" do subject.opts[:color] = :red expect(subject).to receive(:safe_puts).with(any_args) { |message, *args| expect(message).to start_with("\033[1;31m") expect(message).to end_with("\033[0m") } subject.output("foo", bold: true) end end describe "#success" do it "colors green" do expect(subject).to receive(:safe_puts).with(any_args) { |message, *args| expect(message).to start_with("\033[0;32m") expect(message).to end_with("\033[0m") } subject.success("foo") end end describe "#warn" do it "colors yellow" do expect(subject).to receive(:safe_puts).with(any_args) { |message, *args| expect(message).to start_with("\033[0;33m") expect(message).to end_with("\033[0m") } subject.warn("foo") end end end describe Vagrant::UI::MachineReadable do describe "#ask" do it "raises an exception" do expect { subject.ask("foo") }. to raise_error(Vagrant::Errors::UIExpectsTTY) 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(any_args) { |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(any_args) { |message| parts = message.split(",") expect(parts.length).to eq(5) expect(parts[1]).to eq("") expect(parts[2]).to eq("type") expect(parts[3]).to eq("data") expect(parts[4]).to eq("another") true } subject.machine(:type, "data", "another") end it "includes a target if given" do expect(subject).to receive(:safe_puts).with(any_args) { |message| parts = message.split(",") expect(parts.length).to eq(4) expect(parts[1]).to eq("boom") expect(parts[2]).to eq("type") expect(parts[3]).to eq("data") true } subject.machine(:type, "data", target: "boom") end it "replaces commas" do expect(subject).to receive(:safe_puts).with(any_args) { |message| parts = message.split(",") expect(parts.length).to eq(4) expect(parts[3]).to eq("foo%!(VAGRANT_COMMA)bar") true } subject.machine(:type, "foo,bar") end it "replaces newlines" do expect(subject).to receive(:safe_puts).with(any_args) { |message| parts = message.split(",") expect(parts.length).to eq(4) expect(parts[3]).to eq("foo\\nbar\\r") true } subject.machine(:type, "foo\nbar\r") end # This is for a bug where JSON parses are frozen and an # exception was being raised. it "works properly with frozen string arguments" do expect(subject).to receive(:safe_puts).with(any_args) { |message| parts = message.split(",") expect(parts.length).to eq(4) expect(parts[3]).to eq("foo\\nbar\\r") true } subject.machine(:type, "foo\nbar\r".freeze) end end end describe Vagrant::UI::Prefixed do let(:prefix) { "foo" } let(:ui) { Vagrant::UI::Basic.new } subject { described_class.new(ui, prefix) } describe "#initialize_copy" do it "duplicates the underlying ui too" do another = subject.dup expect(another.opts).to_not equal(subject.opts) end end describe "#ask" do it "does not request bolding" do expect(ui).to receive(:ask).with(" #{prefix}: foo", bold: false, target: prefix) subject.ask("foo") end end describe "#detail" do it "prefixes with spaces and the message" do expect(ui).to receive(:safe_puts).with(" #{prefix}: foo", anything) subject.detail("foo") end it "prefixes every line" do expect(ui).to receive(:detail).with( " #{prefix}: foo\n #{prefix}: bar", bold: false, target: prefix) subject.detail("foo\nbar") end it "doesn't prefix if requested" do expect(ui).to receive(:detail).with("foo", prefix: false, bold: false, target: prefix) subject.detail("foo", prefix: false) end end describe "#machine" do it "sets the target option" do expect(ui).to receive(:machine).with(:foo, { target: prefix }) subject.machine(:foo) end it "preserves existing options" do expect(ui).to receive(:machine).with(:foo, :bar, { foo: :bar, target: prefix }) subject.machine(:foo, :bar, foo: :bar) end end describe "#opts" do it "is the parent's opts" do allow(ui).to receive(:opts).and_return(Object.new) expect(subject.opts).to be(ui.opts) end end describe "#output" do it "prefixes with an arrow and the message" do expect(ui).to receive(:output).with("==> #{prefix}: foo", anything) subject.output("foo") end it "prefixes with spaces if requested" do expect(ui).to receive(:output).with(" #{prefix}: foo", anything) subject.output("foo", prefix_spaces: true) end it "prefixes every line" do expect(ui).to receive(:output).with("==> #{prefix}: foo\n==> #{prefix}: bar", anything) subject.output("foo\nbar") end it "doesn't prefix if requested" do expect(ui).to receive(:output).with("foo", prefix: false, bold: true, target: prefix) subject.output("foo", prefix: false) end it "requests bolding" do expect(ui).to receive(:output).with("==> #{prefix}: foo", bold: true, target: prefix) subject.output("foo") end it "does not request bolding if class-level disabled" do ui.opts[:bold] = false expect(ui).to receive(:output).with("==> #{prefix}: foo", target: prefix) subject.output("foo") end it "prefixes with another prefix if requested" do expect(ui).to receive(:output).with("==> bar: foo", anything) subject.output("foo", target: "bar") end end describe "#format_message" do it "should return the same number of new lines as given" do ["no new line", "one\nnew line", "two\nnew lines\n", "three\nnew lines\n\n"].each do |msg| expect(subject.format_message(:detail, msg).count("\n")).to eq(msg.count("\n")) end end it "should properly format a blank message" do expect(subject.format_message(:detail, "", target: "default", prefix: true)). to match(/\s+default:\s+/) end end end ================================================ FILE: test/unit/vagrant/util/ansi_escape_code_remover_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/ansi_escape_code_remover" describe Vagrant::Util::ANSIEscapeCodeRemover do let(:klass) do Class.new do extend Vagrant::Util::ANSIEscapeCodeRemover end end it "should remove ANSI escape codes" do expect(klass.remove_ansi_escape_codes("\e[Hyo")).to eq("yo") end end ================================================ FILE: test/unit/vagrant/util/caps_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/caps" describe Vagrant::Util::Caps do describe "BuildISO" do class TestSubject extend Vagrant::Util::Caps::BuildISO BUILD_ISO_CMD = "test".freeze end let(:subject) { TestSubject } let(:env) { double("env") } describe ".build_iso" do let(:file_destination) { Pathname.new("/woo/out.iso") } before do allow(file_destination).to receive(:exists?).and_return(false) allow(FileUtils).to receive(:mkdir_p) end it "should run command" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("test", "cmd").and_return(double(exit_code: 0)) subject.build_iso(["test", "cmd"], "/src/dir", file_destination) end it "raise an error if command fails" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("test", "cmd").and_return(double(exit_code: 1, stdout: "oh no", stderr: "oh no")) expect{ subject.build_iso(["test", "cmd"], "/src/dir", file_destination) }.to raise_error(Vagrant::Errors::ISOBuildFailed) end end end end ================================================ FILE: test/unit/vagrant/util/checkpoint_client_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/checkpoint_client" describe Vagrant::Util::CheckpointClient do include_context "unit" let(:iso_env) { isolated_environment } let(:env) { iso_env.create_vagrant_env } let(:result) { {} } subject{ Vagrant::Util::CheckpointClient.instance } after{ subject.reset! } before do allow(subject).to receive(:result).and_return(result) end it "should not be enabled by default" do expect(subject.enabled).to be(false) end describe "#setup" do let(:environment){ {} } before{ with_temp_env(environment){ subject.setup(env) } } it "should enable after setup" do expect(subject.enabled).to be(true) end it "should generate required paths" do expect(subject.files).not_to be_empty end context "with VAGRANT_CHECKPOINT_DISABLE set" do let(:environment){ {"VAGRANT_CHECKPOINT_DISABLE" => "1"} } it "should not be enabled after setup" do expect(subject.enabled).to be(false) end end end describe "#check" do context "without #setup" do it "should not start the check" do expect(Thread).not_to receive(:new) subject.check end end context "with setup" do before{ subject.setup(env) } it "should start the check" do expect(Thread).to receive(:new) subject.check end it "should call checkpoint" do expect(Thread).to receive(:new).and_yield expect(Checkpoint).to receive(:check) subject.check end end end describe "#display" do it "should only display once" do expect(subject).to receive(:version_check).once expect(subject).to receive(:alerts_check).once 2.times{ subject.display } end it "should not display cached information" do expect(subject).to receive(:result).and_return("cached" => true).at_least(:once) expect(subject).not_to receive(:version_check) expect(subject).not_to receive(:alerts_check) subject.display end end describe "#alerts_check" do let(:critical){ [{"level" => "critical", "message" => "critical message", "url" => "http://example.com", "date" => Time.now.to_i}] } let(:warn){ [{"level" => "warn", "message" => "warn message", "url" => "http://example.com", "date" => Time.now.to_i}] } let(:info){ [{"level" => "info", "message" => "info message", "url" => "http://example.com", "date" => Time.now.to_i}] } before{ subject.setup(env) } context "with no alerts" do it "should not display alerts" do expect(env.ui).not_to receive(:info) subject.alerts_check end end context "with critical alerts" do let(:result) { {"alerts" => critical} } it "should display critical alert" do expect(env.ui).to receive(:error) subject.alerts_check end end context "with warn alerts" do let(:result) { {"alerts" => warn} } it "should display warn alerts" do expect(env.ui).to receive(:warn) subject.alerts_check end end context "with info alerts" do let(:result) { {"alerts" => info} } it "should display info alerts" do expect(env.ui).to receive(:info).at_least(:once) subject.alerts_check end end context "with mixed alerts" do let(:result) { {"alerts" => info + warn + critical} } it "should display all alert types" do expect(env.ui).to receive(:info).at_least(:once) expect(env.ui).to receive(:warn).at_least(:once) expect(env.ui).to receive(:error).at_least(:once) subject.alerts_check end end end describe "#version_check" do before{ subject.setup(env) } let(:new_version){ Gem::Version.new(Vagrant::VERSION).bump.to_s } let(:old_version){ Gem::Version.new("1.0.0") } context "latest version is same as current version" do let(:result) { {"current_version" => Vagrant::VERSION } } it "should not display upgrade information" do expect(env.ui).not_to receive(:info) subject.version_check end end context "latest version is older than current version" do let(:result) { {"current_version" => old_version} } it "should not display upgrade information" do expect(env.ui).not_to receive(:info) subject.version_check end end context "latest version is newer than current version" do let(:result) { {"current_version" => new_version} } it "should display upgrade information" do expect(env.ui).to receive(:info).at_least(:once) subject.version_check end it "should display upgrade information on error channel" do expect(env.ui).to receive(:info).with(any_args, hash_including(channel: :error)).at_least(:once) subject.version_check end end end end ================================================ FILE: test/unit/vagrant/util/command_deprecation_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/command_deprecation" describe Vagrant::Util do include_context "unit" let(:app){ lambda{|env|} } let(:argv){[]} let(:env){ {ui: Vagrant::UI::Silent.new} } let(:command_class) do Class.new(Vagrant.plugin("2", :command)) do def self.synopsis "base synopsis" end def self.name "VagrantPlugins::CommandTest::Command" end def execute @env[:ui].info("COMMAND CONTENT") 0 end end end let(:command){ command_class.new(app, env) } describe Vagrant::Util::CommandDeprecation do before{ command_class.include(Vagrant::Util::CommandDeprecation) } it "should add deprecation warning to synopsis" do expect(command_class.synopsis).to include('[DEPRECATED]') command.class.synopsis end it "should add deprecation warning to #execute" do expect(env[:ui]).to receive(:warn).with(/DEPRECATION WARNING/) command.execute end it "should execute original command" do expect(env[:ui]).to receive(:info).with("COMMAND CONTENT") command.execute end it "should return with a 0 value" do expect(command.execute).to eq(0) end context "with custom name defined" do before do command_class.class_eval do def deprecation_command_name "custom-name" end end end it "should use custom name within warning message" do expect(env[:ui]).to receive(:warn).with(/custom-name/) command.execute end end context "with deprecated subcommand" do let(:command_class) do Class.new(Vagrant.plugin("2", :command)) do def self.name "VagrantPlugins::CommandTest::Command::Action" end def execute @env[:ui].info("COMMAND CONTENT") 0 end end end it "should not modify empty synopsis" do expect(command_class.synopsis.to_s).to be_empty end it "should extract command name and subname" do expect(command.deprecation_command_name).to eq("test action") end end end describe Vagrant::Util::CommandDeprecation::Complete do before{ command_class.include(Vagrant::Util::CommandDeprecation::Complete) } it "should add deprecation warning to synopsis" do expect(command_class.synopsis).to include('[DEPRECATED]') command.class.synopsis end it "should raise a deprecation error when executed" do expect{ command.execute }.to raise_error(Vagrant::Errors::CommandDeprecated) end it "should not run original command" do expect(env[:ui]).not_to receive(:info).with("COMMAND CONTENT") expect{ command.execute }.to raise_error(Vagrant::Errors::CommandDeprecated) end end end ================================================ FILE: test/unit/vagrant/util/credential_scrubber_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/credential_scrubber" describe Vagrant::Util::CredentialScrubber do subject{ Vagrant::Util::CredentialScrubber } after{ subject.reset! } describe ".url_scrubber" do let(:user){ "vagrant-user" } let(:password){ "vagrant-pass" } let(:url){ "http://#{user}:#{password}@example.com" } it "should remove user credentials from URL" do result = subject.url_scrubber(url) expect(result).not_to include(user) expect(result).not_to include(password) end end describe ".sensitive" do it "should return a nil value" do expect(subject.sensitive("value")).to be_nil end it "should add value to list of strings" do subject.sensitive("value") expect(subject.sensitive_strings).to include("value") end it "should remove duplicates" do subject.sensitive("value") subject.sensitive("value") expect(subject.sensitive_strings.count("value")).to eq(1) end it "should not add an empty string" do subject.sensitive("") expect(subject.sensitive_strings).to be_empty end it "should type cast input to string" do subject.sensitive(2) expect(subject.sensitive_strings.first).to eq("2") end end describe ".unsensitive" do it "should return a nil value" do expect(subject.unsensitive("value")).to be_nil end it "should remove value from list" do subject.sensitive("value") expect(subject.sensitive_strings).to include("value") subject.unsensitive("value") expect(subject.sensitive_strings).not_to include("value") end end describe ".sensitive_strings" do it "should always return the same array" do expect(subject.sensitive_strings).to be(subject.sensitive_strings) end end describe ".desensitize" do let(:to_scrub){ [] } let(:string){ "a line of text with my-birthday and my-cats-birthday embedded" } before{ to_scrub.each{|s| subject.sensitive(s) }} context "with no sensitive strings registered" do it "should not modify the string" do expect(subject.desensitize(string)).to eq(string) end end context "with single value registered" do let(:to_scrub){ ["my-birthday"] } it "should remove the registered value" do expect(subject.desensitize(string)).not_to include(to_scrub.first) end end context "with multiple values registered" do let(:to_scrub){ ["my-birthday", "my-cats-birthday"] } it "should remove all registered values" do result = subject.desensitize(string) to_scrub.each do |registered_value| expect(result).not_to include(registered_value) end end end context "with sensitive words that are part of non-sensitive words" do let(:to_scrub){ ["a"] } it "should not remove parts of words" do result = subject.desensitize(string) to_scrub.each do |registered_value| expect(result).not_to match(/(\W|^)#{registered_value}(\W|$)/) end expect(result).to include("my-birthday") expect(result).to include("my-cats-birthday") end end context "with sensitive words that are part of non-sensitive words" do let(:to_scrub){ ["avery@strange/string^indeed!"] } let(:string){ "a line of text with avery@strange/string^indeed! my-birthday and my-cats-birthday embedded" } it "should work for strings with escape characters" do result = subject.desensitize(string) to_scrub.each do |registered_value| expect(result).not_to include(registered_value) end end end end end ================================================ FILE: test/unit/vagrant/util/curl_helper_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/curl_helper" describe Vagrant::Util::CurlHelper do end ================================================ FILE: test/unit/vagrant/util/deep_merge_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'vagrant/util/deep_merge' describe Vagrant::Util::DeepMerge do it "should deep merge hashes" do original = { "foo" => { "bar" => "baz", }, "bar" => "blah", } other = { "foo" => { "bar" => "new", }, } result = described_class.deep_merge(original, other) expect(result).to_not equal(original) expect(result).to eq({ "foo" => { "bar" => "new", }, "bar" => "blah", }) end end ================================================ FILE: test/unit/vagrant/util/directory_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/directory" require "time" describe Vagrant::Util::Directory do include_context "unit" let(:subject){ Vagrant::Util::Directory } describe ".directory_changed?" do it "should return false if the threshold time is larger the all mtimes" do t = Time.new("3008", "09", "09") expect(subject.directory_changed?(Dir.getwd, t)).to eq(false) end it "should return true if the threshold time is less than any mtimes" do t = Time.new("1990", "06", "06") expect(subject.directory_changed?(Dir.getwd, t)).to eq(true) end end end ================================================ FILE: test/unit/vagrant/util/downloader_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/downloader" describe Vagrant::Util::Downloader do let(:source) { "foo" } let(:destination) { "bar" } let(:exit_code) { 0 } let(:options) { {} } let(:subprocess_result) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(exit_code) allow(result).to receive(:stderr).and_return("") end end subject { described_class.new(source, destination, options) } before :each do allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(subprocess_result) allow(Vagrant).to receive(:in_installer?).and_return(false) end describe "USER_AGENT" do it "should not include a trailing space" do expect(described_class.const_get(:USER_AGENT)).not_to end_with(" ") end end describe "#download!" do let(:curl_options) { ["-q", "--fail", "--location", "--max-redirs", "10", "--verbose", "--user-agent", described_class::USER_AGENT, "--output", destination, source, {}] } context "on Windows" do before do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) end it "should use best effort for ssl revocation check by default" do expect(subject).to receive(:execute_curl) do |opts, *_| expect(opts).to include("--ssl-revoke-best-effort") end subject.download! end context "when ssl revoke best effort is disabled" do let(:options) { {disable_ssl_revoke_best_effort: true} } it "should not use best effort for ssl revocation check" do expect(subject).to receive(:execute_curl) do |opts, _| expect(opts).not_to include("--ssl-revoke-best-effort") end subject.download! end end end context "with UI" do let(:ui) { Vagrant::UI::Silent.new } let(:options) { {ui: ui} } let(:source) { "http://example.org/vagrant.box" } let(:redirect) { nil } let(:progress_data) { "Location: #{redirect}" } after do expect(subject).to receive(:execute_curl) do |*_, &data_proc| expect(data_proc).not_to be_nil data_proc.call(:stderr, progress_data) end subject.download! end context "with Location header at same host" do let(:redirect) { "http://example.org/other-vagrant.box" } it "should not output redirection information" do expect(ui).not_to receive(:detail) end end context "with Location header at different host" do let(:redirect) { "http://example.com/vagrant.box" } it "should output redirection information" do expect(ui).to receive(:detail).with(/example.com/).and_call_original end end context "with Location header at different subdomain" do let(:redirect) { "http://downloads.example.org/vagrant.box" } it "should output redirection information" do expect(ui).to receive(:detail).with(/downloads.example.org/).and_call_original end end context "with custom header including Location name" do let(:custom_redirect) { "http://example.com/vagrant.box" } let(:progress_data) { "X-Custom-Location: #{custom_redirect}" } it "should not output redirection information" do expect(ui).not_to receive(:detail) end context "with Location header at different host" do let(:redirect) { "http://downloads.example.com/vagrant.box" } let(:progress_data) { "X-Custom-Location: #{custom_redirect}\nLocation: #{redirect}" } it "should output redirection information" do expect(ui).to receive(:detail).with(/downloads.example.com/).and_call_original end end end end context "with a good exit status" do let(:exit_code) { 0 } it "downloads the file and returns true" do expect(Vagrant::Util::Subprocess).to receive(:execute). with("curl", *curl_options). and_return(subprocess_result) expect(subject.download!).to be end end context "with a bad exit status" do let(:exit_code) { 1 } let(:subprocess_result_416) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(exit_code) allow(result).to receive(:stderr).and_return("curl: (416) The download is fine") end end it "continues on if a 416 was received" do expect(Vagrant::Util::Subprocess).to receive(:execute). with("curl", *curl_options). and_return(subprocess_result_416) expect(subject.download!).to be(true) end it "raises an exception" do expect(Vagrant::Util::Subprocess).to receive(:execute). with("curl", *curl_options). and_return(subprocess_result) expect { subject.download! }. to raise_error(Vagrant::Errors::DownloaderError) end end context "with a username and password" do it "downloads the file with the proper flags" do original_source = source source = "http://foo:bar@example.com/box.box" subject = described_class.new(source, destination) i = curl_options.index(original_source) curl_options[i] = "http://example.com/box.box" i = curl_options.index("--output") curl_options.insert(i, "foo:bar") curl_options.insert(i, "-u") expect(Vagrant::Util::Subprocess).to receive(:execute). with("curl", *curl_options). and_return(subprocess_result) expect(subject.download!).to be(true) end end context "with an urlescaped username and password" do it "downloads the file with unescaped credentials" do original_source = source source = "http://fo%5Eo:b%40r@example.com/box.box" subject = described_class.new(source, destination) i = curl_options.index(original_source) curl_options[i] = "http://example.com/box.box" i = curl_options.index("--output") curl_options.insert(i, "fo^o:b@r") curl_options.insert(i, "-u") expect(Vagrant::Util::Subprocess).to receive(:execute). with("curl", *curl_options). and_return(subprocess_result) expect(subject.download!).to be(true) end end context "with checksum" do let(:checksum_expected_value){ 'MD5_CHECKSUM_VALUE' } let(:checksum_invalid_value){ 'INVALID_VALUE' } let(:filechecksum) { double("filechecksum", checksum: checksum_value) } let(:checksum_value) { double("checksum_value") } before { allow(FileChecksum).to receive(:new).with(any_args).and_return(filechecksum) } [Digest::MD5, Digest::SHA1, Digest::SHA256, Digest::SHA384, Digest::SHA512].each do |klass| short_name = klass.to_s.split("::").last.downcase context "using #{short_name} digest" do subject { described_class.new(source, destination, short_name.to_sym => checksum_expected_value) } context "that matches expected value" do let(:checksum_value) { checksum_expected_value } it "should not raise an exception" do expect(subject.download!).to be(true) end end context "that does not match expected value" do let(:checksum_value) { checksum_invalid_value } it "should raise an exception" do expect{ subject.download! }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end end end context "using both md5 and sha1 digests" do context "that both match expected values" do let(:checksum_value) { checksum_expected_value } subject { described_class.new(source, destination, md5: checksum_expected_value, sha1: checksum_expected_value) } it "should not raise an exception" do expect(subject.download!).to be(true) end end context "that only sha1 matches expected value" do subject { described_class.new(source, destination, md5: checksum_expected_value, sha1: checksum_expected_value) } let(:valid_checksum) { double("valid_checksum", checksum: checksum_expected_value) } let(:invalid_checksum) { double("invalid_checksum", checksum: checksum_invalid_value) } before do allow(FileChecksum).to receive(:new).with(anything, :sha1).and_return(valid_checksum) allow(FileChecksum).to receive(:new).with(anything, :md5).and_return(invalid_checksum) end it "should raise an exception" do expect{ subject.download! }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that only md5 matches expected value" do subject { described_class.new(source, destination, md5: checksum_expected_value, sha1: checksum_expected_value) } let(:valid_checksum) { double("valid_checksum", checksum: checksum_expected_value) } let(:invalid_checksum) { double("invalid_checksum", checksum: checksum_invalid_value) } before do allow(FileChecksum).to receive(:new).with(anything, :md5).and_return(valid_checksum) allow(FileChecksum).to receive(:new).with(anything, :sha1).and_return(invalid_checksum) end it "should raise an exception" do expect{ subject.download! }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end context "that none match expected value" do let(:checksum_value) { checksum_expected_value } subject { described_class.new(source, destination, md5: checksum_invalid_value, sha1: checksum_invalid_value) } it "should raise an exception" do expect{ subject.download! }.to raise_error(Vagrant::Errors::DownloaderChecksumError) end end end context "when extra download options specified" do let(:options) { {:box_extra_download_options => ["--test", "arbitrary"]} } subject { described_class.new(source, destination, options) } it "inserts the extra download options" do i = curl_options.index("--output") curl_options.insert(i, "arbitrary") curl_options.insert(i, "--test") expect(Vagrant::Util::Subprocess).to receive(:execute). with("curl", *curl_options). and_return(subprocess_result) expect(subject.download!).to be(true) end end end end describe "#head" do let(:curl_options) { ["-q", "-I", "--fail", "--location", "--max-redirs", "10", "--verbose", "--user-agent", described_class::USER_AGENT, source, {}] } it "returns the output" do allow(subprocess_result).to receive(:stdout).and_return("foo") expect(Vagrant::Util::Subprocess).to receive(:execute). with("curl", *curl_options).and_return(subprocess_result) expect(subject.head).to eq("foo") end end describe "#options" do describe "CURL_CA_BUNDLE" do let(:ca_bundle){ "CUSTOM_CA_BUNDLE" } context "when running within the installer" do before do allow(Vagrant).to receive(:in_installer?).and_return(true) allow(ENV).to receive(:[]).with("CURL_CA_BUNDLE").and_return(ca_bundle) end it "should set custom CURL_CA_BUNDLE in subprocess ENV" do _, subprocess_opts = subject.send(:options) expect(subprocess_opts[:env]).not_to be_nil expect(subprocess_opts[:env]["CURL_CA_BUNDLE"]).to eql(ca_bundle) end end context "when not running within the installer" do before{ allow(Vagrant).to receive(:installer?).and_return(false) } it "should not set custom CURL_CA_BUNDLE in subprocess ENV" do _, subprocess_opts = subject.send(:options) expect(subprocess_opts[:env]).to be_nil end end end end end ================================================ FILE: test/unit/vagrant/util/env_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'vagrant/util/env' describe Vagrant::Util::Env do context "with valid environment variables" do before do ENV["VAGRANT_TEST"] = "1" end after do ENV.delete("VAGRANT_TEST") end it "should execute block with original environment variables" do Vagrant::Util::Env.with_original_env do expect(ENV["VAGRANT_TEST"]).to be_nil end end it "should replace environment variables after executing block" do Vagrant::Util::Env.with_original_env do expect(ENV["VAGRANT_TEST"]).to be_nil end expect(ENV["VAGRANT_TEST"]).to eq("1") end end context "with invalid environment variables" do it "should not attempt to restore invalid environment variable" do invalid_vars = ENV.to_hash.merge("VAGRANT_OLD_ENV_" => "INVALID") mock = expect(ENV).to receive(:each) invalid_vars.each do |k,v| mock.and_yield(k, v) end expect do Vagrant::Util::Env.with_original_env do expect(ENV["VAGRANT_TEST"]).to be_nil end end.not_to raise_error end end end ================================================ FILE: test/unit/vagrant/util/experimental_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/experimental" describe Vagrant::Util::Experimental do include_context "unit" before(:all) { described_class.reset! } after(:each) { described_class.reset! } subject { described_class } describe "#enabled?" do it "returns true if enabled with '1'" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("1") expect(subject.enabled?).to eq(true) end it "returns true if enabled with a list of features" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("list,of,features") expect(subject.enabled?).to eq(true) end it "returns false if disabled" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("0") expect(subject.enabled?).to eq(false) end it "returns false if not set" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return(nil) expect(subject.enabled?).to eq(false) end end describe "#global_enabled?" do it "returns true if enabled with '1'" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("1") expect(subject.global_enabled?).to eq(true) end it "returns false if enabled with a partial list of features" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("list,of,features") expect(subject.global_enabled?).to eq(false) end it "returns false if disabled" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("0") expect(subject.global_enabled?).to eq(false) end it "returns false if not set" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return(nil) expect(subject.global_enabled?).to eq(false) end end describe "#feature_enabled?" do it "returns true if flag set to 1" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("1") expect(subject.feature_enabled?("anything")).to eq(true) end it "returns true if flag contains feature requested" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("secret_feature") expect(subject.feature_enabled?("secret_feature")).to eq(true) end it "returns true if flag contains feature requested and the request is a symbol" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("secret_feature") expect(subject.feature_enabled?(:secret_feature)).to eq(true) end it "returns true if flag contains feature requested with other features 'enabled'" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("secret_feature,other_secret") expect(subject.feature_enabled?("secret_feature")).to eq(true) end it "returns false if flag is set but does not contain feature requested" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("fake_feature") expect(subject.feature_enabled?("secret_feature")).to eq(false) end it "returns false if flag set to 0" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("0") expect(subject.feature_enabled?("anything")).to eq(false) end it "returns false if flag is not set" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return(nil) expect(subject.feature_enabled?("anything")).to eq(false) end end describe "#features_requested" do it "returns an array of requested features" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("secret_feature,other_secret") expect(subject.features_requested).to eq(["secret_feature","other_secret"]) end end describe "#guard_with" do it "does not execute the block if the feature is not requested" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return(nil) expect{|b| subject.guard_with("secret_feature", &b) }.not_to yield_control end it "executes the block if the feature is valid and requested" do allow(ENV).to receive(:[]).with("VAGRANT_EXPERIMENTAL").and_return("secret_feature,other_secret") expect{|b| subject.guard_with("secret_feature", &b) }.to yield_control end end end ================================================ FILE: test/unit/vagrant/util/file_checksum_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'digest/md5' require 'digest/sha1' require 'vagrant/util/file_checksum' describe FileChecksum do include_context "unit" let(:environment) { isolated_environment } it "should return a valid checksum for a file" do file = environment.workdir.join("file") file.open("w+") { |f| f.write("HELLO!") } # Check multiple digests instance = described_class.new(file, Digest::MD5) expect(instance.checksum).to eq("9ac96c64417b5976a58839eceaa77956") instance = described_class.new(file, Digest::SHA1) expect(instance.checksum).to eq("264b207c7913e461c43d0f63d2512f4017af4755") end it "should support initialize with class or string" do file = environment.workdir.join("file") file.open("w+") { |f| f.write("HELLO!") } %w(md5 sha1 sha256 sha384 sha512).each do |type| klass = Digest.const_get(type.upcase) t_i = described_class.new(file, type) k_i = described_class.new(file, klass) expect(t_i.checksum).to eq(k_i.checksum) end end context "with an invalid digest" do let(:fake_digest) { :fake_digest } it "should raise an exception if the box has an invalid checksum type" do file = environment.workdir.join("file") file.open("w+") { |f| f.write("HELLO!") } expect{ described_class.new(file, fake_digest) }.to raise_error(Vagrant::Errors::BoxChecksumInvalidType) end end end ================================================ FILE: test/unit/vagrant/util/file_mutex_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'vagrant/util/file_mutex' describe Vagrant::Util::FileMutex do include_context "unit" let(:temp_dir) { Dir.mktmpdir("vagrant-test-util-mutex_test") } let(:mutex_path) { File.join(temp_dir, "test.lock") } let(:subject) { described_class.new(mutex_path) } after do FileUtils.rm_rf(temp_dir) end it "should create a lock file" do subject.lock expect(File).to exist(mutex_path) end it "should create and delete lock file" do subject.lock expect(File).to exist(mutex_path) subject.unlock expect(File).to_not exist(mutex_path) end it "should not raise an error if the lock file does not exist" do subject.unlock expect(File).to_not exist(mutex_path) end it "should run a function with a lock" do subject.with_lock { true } expect(File).to_not exist(mutex_path) end it "should fail running a function when locked" do # create a lock subject.lock # create a new lock that will run a function instance = described_class.new(mutex_path) # lock should persist for multiple runs expect {instance.with_lock { true }}. to raise_error(Vagrant::Errors::VagrantLocked) expect {instance.with_lock { true }}. to raise_error(Vagrant::Errors::VagrantLocked) # mutex should exist until its unlocked expect(File).to exist(mutex_path) subject.unlock expect(File).to_not exist(mutex_path) end it "should fail running a function within a locked" do # create a new lock that will run a function instance = described_class.new(mutex_path) expect { subject.with_lock { instance.with_lock{true} } }.to raise_error(Vagrant::Errors::VagrantLocked) expect(File).to_not exist(mutex_path) end it "should delete the lock even when the function fails" do expect { subject.with_lock { raise Vagrant::Errors::VagrantError.new } }.to raise_error(Vagrant::Errors::VagrantError) expect(File).to_not exist(mutex_path) end it "should unlock file before deletion" do lock_file = double(:lock_file) allow(subject).to receive(:lock_file).and_return(lock_file) allow(lock_file).to receive(:flock).and_return(true) expect(lock_file).to receive(:flock).with(File::LOCK_UN) expect(lock_file).to receive(:close) subject.with_lock { true } end end ================================================ FILE: test/unit/vagrant/util/guest_hosts_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'vagrant/util/guest_hosts' describe "Vagrant::Util::GuestHosts" do include_context "unit" let(:machine) { double("machine") } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } before do allow(machine).to receive(:communicate).and_return(comm) end describe "Linux" do subject{ Class.new { extend Vagrant::Util::GuestHosts::Linux } } it "can add replace hostname" do subject.replace_host(comm, "test.end", "192.186.4.2") expect(comm.received_commands[0]).to match(/sed -i '\/test.end\/d' \/etc\/hosts/) end it "can add hostname to loopback interface" do subject.add_hostname_to_loopback_interface(comm, "test.end", 4) expect(comm.received_commands[0]).to match(/for i in 1 2 3 4; do/) expect(comm.received_commands[0]).to match(/echo \"127.0.\${i}.1 test.end test\" >> \/etc\/hosts/) end end describe "BSD" do subject{ Class.new { extend Vagrant::Util::GuestHosts::BSD } } it "can add replace hostname" do subject.replace_host(comm, "test.end", "192.186.4.2") expect(comm.received_commands[0]).to match(/sed -i.bak '\/test.end\/d' \/etc\/hosts/) end it "can add hostname to loopback interface" do subject.add_hostname_to_loopback_interface(comm, "test.end", 4) expect(comm.received_commands[0]).to match(/for i in 1 2 3 4; do/) expect(comm.received_commands[0]).to match(/echo \"127.0.\${i}.1 test.end test\" >> \/etc\/hosts/) end end end ================================================ FILE: test/unit/vagrant/util/guest_inspection_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/guest_inspection" describe Vagrant::Util::GuestInspection::Linux do include_context "unit" let(:comm) { double("comm") } subject{ Class.new { extend Vagrant::Util::GuestInspection::Linux } } describe "#systemd?" do it "should execute the command with sudo" do expect(comm).to receive(:test).with(/ps/, {sudo: true}).and_return(true) expect(subject.systemd?(comm)).to be(true) end end end ================================================ FILE: test/unit/vagrant/util/guest_networks_spec.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/guest_networks" describe Vagrant::Util::GuestNetworks::Linux do include_context "unit" subject { Class.new { extend Vagrant::Util::GuestNetworks::Linux } } let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } let(:config) { double("config", vm: vm) } let(:guest) { double("guest") } let(:machine) { double("machine", guest: guest, config: config) } let(:networks){ [[:public_network, network_1], [:private_network, network_2]] } let(:vm){ double("vm", networks: networks) } let(:interfaces) { ["eth1", "eth2", "eth3"] } before do allow(machine).to receive(:communicate).and_return(comm) allow(guest).to receive(:capability).with(:network_interfaces).and_return(interfaces) end after do comm.verify_expectations! end let(:network_1) do { interface: 0, type: "dhcp", } end let(:network_2) do { interface: 1, type: "static", ip: "33.33.33.10", netmask: "255.255.0.0", gateway: "33.33.0.1", } end let(:network_3) do { interface: 2, type: "static", ip: "33.33.33.11", netmask: "255.255.0.0", gateway: "33.33.0.1", } end describe "#configure_network_manager" do it "should fetch mac address for devices" do subject.configure_network_manager(machine, [network_1, network_2, network_3]) expect(comm.received_commands).to include(%r{/net/eth1/address}) expect(comm.received_commands).to include(%r{/net/eth2/address}) expect(comm.received_commands).to include(%r{/net/eth3/address}) end it "should change ownership of files" do subject.configure_network_manager(machine, [network_1, network_2, network_3]) expect(comm.received_commands).to include(%r{chown root:root '/tmp/vagrant.*eth1.*'}) expect(comm.received_commands).to include(%r{chown root:root '/tmp/vagrant.*eth2.*'}) expect(comm.received_commands).to include(%r{chown root:root '/tmp/vagrant.*eth3.*'}) end it "should change mode of files" do subject.configure_network_manager(machine, [network_1, network_2, network_3]) expect(comm.received_commands).to include(%r{chmod 0600 '/tmp/vagrant.*eth1.*'}) expect(comm.received_commands).to include(%r{chmod 0600 '/tmp/vagrant.*eth2.*'}) expect(comm.received_commands).to include(%r{chmod 0600 '/tmp/vagrant.*eth3.*'}) end it "should move configuration files" do subject.configure_network_manager(machine, [network_1, network_2, network_3]) expect(comm.received_commands).to include(%r{mv '/tmp/vagrant.*eth1.*' '/etc/NetworkManager/system-connections/eth1.nmconnection'}) expect(comm.received_commands).to include(%r{mv '/tmp/vagrant.*eth2.*' '/etc/NetworkManager/system-connections/eth2.nmconnection'}) expect(comm.received_commands).to include(%r{mv '/tmp/vagrant.*eth3.*' '/etc/NetworkManager/system-connections/eth3.nmconnection'}) end it "should move configuration files" do subject.configure_network_manager(machine, [network_1, network_2, network_3]) expect(comm.received_commands).to include(%r{mv '/tmp/vagrant.*eth1.*' '/etc/NetworkManager/system-connections/eth1.nmconnection'}) expect(comm.received_commands).to include(%r{mv '/tmp/vagrant.*eth2.*' '/etc/NetworkManager/system-connections/eth2.nmconnection'}) expect(comm.received_commands).to include(%r{mv '/tmp/vagrant.*eth3.*' '/etc/NetworkManager/system-connections/eth3.nmconnection'}) end it "should move load new configuration files" do subject.configure_network_manager(machine, [network_1, network_2, network_3]) expect(comm.received_commands).to include("nmcli c load '/etc/NetworkManager/system-connections/eth1.nmconnection'") expect(comm.received_commands).to include("nmcli c load '/etc/NetworkManager/system-connections/eth2.nmconnection'") expect(comm.received_commands).to include("nmcli c load '/etc/NetworkManager/system-connections/eth3.nmconnection'") end it "should connect new devices" do subject.configure_network_manager(machine, [network_1, network_2, network_3]) expect(comm.received_commands).to include("nmcli d connect 'eth1'") expect(comm.received_commands).to include("nmcli d connect 'eth2'") expect(comm.received_commands).to include("nmcli d connect 'eth3'") end context "network configuration file" do let(:networks){ [[:public_network, network_1], [:private_network, network_2], [:private_network, network_3]] } let(:tempfile) { double("tempfile") } before do allow(tempfile).to receive(:binmode) allow(tempfile).to receive(:write) allow(tempfile).to receive(:fsync) allow(tempfile).to receive(:close) allow(tempfile).to receive(:path) allow(Tempfile).to receive(:open).and_yield(tempfile) end it "should generate two configuration files" do expect(Tempfile).to receive(:open).twice subject.configure_network_manager(machine, [network_1, network_2]) end it "should generate three configuration files" do expect(Tempfile).to receive(:open).thrice subject.configure_network_manager(machine, [network_1, network_2, network_3]) end it "should generate configuration with network_2 IP address" do expect(tempfile).to receive(:write).with(/#{Regexp.escape(network_2[:ip])}/) subject.configure_network_manager(machine, [network_1, network_2, network_3]) end it "should generate configuration with network_3 IP address" do expect(tempfile).to receive(:write).with(/#{Regexp.escape(network_3[:ip])}/) subject.configure_network_manager(machine, [network_1, network_2, network_3]) end end end describe "#get_current_devices" do it "should return a hash of current devices" do expect(comm).to receive(:execute).with("nmcli -t c show").and_yield(:stderr, "").and_yield(:stdout, "1:eth1:ethernet:eth1\n2:eth2:ethernet:eth2\n3:eth3:ethernet:eth3\n") result = subject.get_current_devices(comm) expect(result).to eq({"eth1" => "eth1", "eth2" => "eth2", "eth3" => "eth3"}) end it "should return an empty hash if no devices are found" do expect(comm).to receive(:execute).with("nmcli -t c show").and_yield(:stderr, "some error").and_yield(:stdout, "") result = subject.get_current_devices(comm) expect(result).to eq({}) end it "should ignore empty lines in the output" do expect(comm).to receive(:execute).with("nmcli -t c show").and_yield(:stderr, "").and_yield(:stdout, "1:eth1:ethernet:eth1\n\n2:eth2:ethernet:eth2\n\n3:eth3:ethernet:eth3\n") result = subject.get_current_devices(comm) expect(result).to eq({"eth1" => "eth1", "eth2" => "eth2", "eth3" => "eth3"}) end end end ================================================ FILE: test/unit/vagrant/util/hash_with_indifferent_access_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/hash_with_indifferent_access" describe Vagrant::Util::HashWithIndifferentAccess do let(:instance) { described_class.new } it "is a Hash" do expect(instance).to be_kind_of(Hash) end it "allows indifferent access when setting with a string" do instance["foo"] = "bar" expect(instance[:foo]).to eq("bar") end it "allows indifferent access when setting with a symbol" do instance[:foo] = "bar" expect(instance["foo"]).to eq("bar") end it "allows indifferent key lookup" do instance["foo"] = "bar" expect(instance.key?(:foo)).to be expect(instance.key?(:foo)).to be expect(instance.include?(:foo)).to be expect(instance.member?(:foo)).to be end it "allows for defaults to be passed in via an initializer block" do instance = described_class.new do |h,k| h[k] = "foo" end expect(instance[:foo]).to eq("foo") expect(instance["bar"]).to eq("foo") end end ================================================ FILE: test/unit/vagrant/util/install_cli_autocomplete_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'vagrant/util/install_cli_autocomplete' require 'fileutils' describe Vagrant::Util::InstallZSHShellConfig do let(:home) { "#{Dir.tmpdir}/not-home" } let(:target_file) { "#{home}/.zshrc" } subject { described_class.new() } describe "#shell_installed" do it "should return path to config file if exists" do allow(File).to receive(:exist?).with(target_file).and_return(true) expect(subject.shell_installed(home)).to eq(target_file) end it "should return nil if config file does not exists" do FileUtils.rm_rf(target_file) expect(subject.shell_installed(home)).to eq(nil) end end describe "#is_installed" do it "returns false if autocompletion not already installed" do allow(File).to receive(:foreach).with(target_file).and_yield("nothing") expect(subject.is_installed(target_file)).to eq(false) end it "returns true if autocompletion is already installed" do allow(File).to receive(:foreach).with(target_file).and_yield(subject.prepend_string) expect(subject.is_installed(target_file)).to eq(true) end end describe "#install" do it "installs autocomplete" do allow(File).to receive(:exist?).with(target_file).and_return(true) allow(File).to receive(:foreach).with(target_file).and_yield("nothing") expect(File).to receive(:open).with(target_file, "a") subject.install(home) end end end describe Vagrant::Util::InstallCLIAutocomplete do let(:zshrc_path) { "/path/to/.zshrc" } let(:bashrc_path) { "/path/to/.bash_profile" } subject { described_class } describe ".install" do it "installs requested shells" do allow_any_instance_of(Vagrant::Util::InstallZSHShellConfig).to receive(:install).and_return(zshrc_path) expect(subject.install(["zsh"])).to eq([zshrc_path]) end it "installs all shells by default" do allow_any_instance_of(Vagrant::Util::InstallZSHShellConfig).to receive(:install).and_return(zshrc_path) allow_any_instance_of(Vagrant::Util::InstallBashShellConfig).to receive(:install).and_return(bashrc_path) expect(subject.install()).to eq([zshrc_path, bashrc_path]) end it "does not install unsupported shells" do expect{ subject.install(["oops"]) }.to raise_error(ArgumentError) end end end ================================================ FILE: test/unit/vagrant/util/io_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 # -*- coding: utf-8 -*- require File.expand_path("../../../base", __FILE__) require 'vagrant/util/io' describe Vagrant::Util::IO do describe ".read_until_block" do let(:io) { double("io") } before do # Ensure that we don't get stuck in a loop allow(io).to receive(:read_nonblock).and_raise(EOFError) allow(io).to receive(:readpartial).and_raise(EOFError) end context "on non-Windows system" do before { allow(Vagrant::Util::Platform).to receive(:windows?). and_return(false) } it "should use a non-blocking read" do expect(io).to receive(:read_nonblock).and_return("") described_class.read_until_block(io) end it "should receive data until breakable event" do expect(io).to receive(:read_nonblock).and_return("one") expect(io).to receive(:read_nonblock).and_return("two") expect(io).to receive(:read_nonblock).and_return("three") data = described_class.read_until_block(io) expect(data).to eq("onetwothree") end context "with breakable errors" do [EOFError, Errno::EAGAIN, IO::EAGAINWaitReadable, IO::EINPROGRESSWaitReadable, IO::EWOULDBLOCKWaitReadable].each do |err_class| it "should break without error on #{err_class}" do expect(io).to receive(:read_nonblock).and_raise(err_class) expect(described_class.read_until_block(io)).to be_empty end end end context "with non-breakable errors" do it "should raise the error" do expect(io).to receive(:read_nonblock).and_raise(StandardError) expect { described_class.read_until_block(io) }.to raise_error(StandardError) end end end context "on Windows system" do before do allow(Vagrant::Util::Platform).to receive(:windows?). and_return(true) allow(IO).to receive(:select).with([io], any_args). and_return([io]) allow(io).to receive(:empty?).and_return(false) end it "should use select" do expect(IO).to receive(:select).with([io], any_args) described_class.read_until_block(io) end it "should receive data until breakable event" do expect(io).to receive(:readpartial).and_return("one") expect(io).to receive(:readpartial).and_return("two") expect(io).to receive(:readpartial).and_return("three") data = described_class.read_until_block(io) expect(data).to eq("onetwothree") end context "with breakable errors" do [EOFError, Errno::EAGAIN, IO::EAGAINWaitReadable, IO::EINPROGRESSWaitReadable, IO::EWOULDBLOCKWaitReadable].each do |err_class| it "should break without error on #{err_class}" do expect(io).to receive(:readpartial).and_raise(err_class) expect(described_class.read_until_block(io)).to be_empty end end end context "with non-breakable errors" do it "should raise the error" do expect(io).to receive(:readpartial).and_raise(StandardError) expect { described_class.read_until_block(io) }.to raise_error(StandardError) end end context "encoding" do let(:output) { "output".force_encoding("ASCII-8BIT") } before { expect(io).to receive(:readpartial).and_return(output) } it "should encode output to UTF-8" do expect(described_class.read_until_block(io).encoding.name).to eq("UTF-8") end context "when output includes characters with undefined conversion" do let(:output) { "output\xFF".force_encoding("ASCII-8BIT") } before { expect(Encoding).to receive(:default_external). and_return(Encoding.find("ASCII-8BIT")) } it "should return data with invalid characters replaced" do expect(described_class.read_until_block(io)).to include("�") end end end end end end ================================================ FILE: test/unit/vagrant/util/ipv4_interfaces_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/ipv4_interfaces" describe Vagrant::Util::IPv4Interfaces do subject { described_class } describe "#ipv4_interfaces" do let(:name) { double("name") } let(:address) { double("address") } let(:ipv4_ifaddr) do double("ipv4_ifaddr").tap do |ifaddr| allow(ifaddr).to receive(:name).and_return(name) allow(ifaddr).to receive_message_chain(:addr, :ipv4?).and_return(true) allow(ifaddr).to receive_message_chain(:addr, :ip_address).and_return(address) end end let(:ipv6_ifaddr) do double("ipv6_ifaddr").tap do |ifaddr| allow(ifaddr).to receive(:name) allow(ifaddr).to receive_message_chain(:addr, :ipv4?).and_return(false) end end let(:ifaddrs) { [ ipv4_ifaddr, ipv6_ifaddr ] } before do allow(Socket).to receive(:getifaddrs).and_return(ifaddrs) end it "returns a list of IPv4 interfaces with their names and addresses" do expect(subject.ipv4_interfaces).to eq([ [name, address] ]) end context "with nil interface address" do let(:nil_ifaddr) { double("nil_ifaddr", addr: nil ) } let(:ifaddrs) { [ ipv4_ifaddr, ipv6_ifaddr, nil_ifaddr ] } it "filters out nil addr info" do expect(subject.ipv4_interfaces).to eq([ [name, address] ]) end end end end ================================================ FILE: test/unit/vagrant/util/is_port_open_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "socket" require "vagrant/util/is_port_open" describe Vagrant::Util::IsPortOpen do subject { described_class } let(:open_port) { 52811 } let(:closed_port) { 52811 } it "should report open ports" do # Start a thread which listens on a port thr = Thread.new do server = TCPServer.new(open_port) Thread.current[:running] = true # Wait until we're told to die Thread.current[:die] = false while !Thread.current[:die] Thread.pass end # Die! server.close end # Wait until the server is running while !thr[:running] Thread.pass end # Verify that we report the port is open expect(subject.is_port_open?("127.0.0.1", open_port)).to be # Kill the thread thr[:die] = true thr.join end it "should report closed ports" do # This CAN fail, since port 52811 might actually be in use, but I'm # not sure what to do except choose some random port and hope for the # best, really. expect(subject.is_port_open?("127.0.0.1", closed_port)).not_to be end it "should handle connection refused" do expect(Socket).to receive(:tcp).with("0.0.0.0", closed_port, any_args).and_raise(Errno::ECONNREFUSED) expect(subject.is_port_open?("0.0.0.0", closed_port)).to be(false) end it "should raise an error if cannot assign requested address" do expect(Socket).to receive(:tcp).with("0.0.0.0", open_port, any_args).and_raise(Errno::EADDRNOTAVAIL) expect { subject.is_port_open?("0.0.0.0", open_port) }.to raise_error(Errno::EADDRNOTAVAIL) end it "should treat operation already in progress as unavailable" do expect(Socket).to receive(:tcp).with("0.0.0.0", closed_port, any_args).and_raise(Errno::EALREADY) expect(subject.is_port_open?("0.0.0.0", closed_port)).to be(false) end end ================================================ FILE: test/unit/vagrant/util/keypair_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require "openssl" require "ed25519" require "net/ssh" require File.expand_path("../../../base", __FILE__) require "vagrant/util/keypair" describe Vagrant::Util::Keypair do describe Vagrant::Util::Keypair::Rsa do describe ".create" do it "generates a usable keypair with no password" do # I don't know how to validate the final return value yet... pubkey, privkey, _ = described_class.create pubkey = OpenSSL::PKey::RSA.new(pubkey) privkey = OpenSSL::PKey::RSA.new(privkey) encrypted = pubkey.public_encrypt("foo") decrypted = privkey.private_decrypt(encrypted) expect(decrypted).to eq("foo") end it "generates a keypair that requires a password" do pubkey, privkey, _ = described_class.create("password") pubkey = OpenSSL::PKey::RSA.new(pubkey) privkey = OpenSSL::PKey::RSA.new(privkey, "password") encrypted = pubkey.public_encrypt("foo") decrypted = privkey.private_decrypt(encrypted) expect(decrypted).to eq("foo") end end end describe Vagrant::Util::Keypair::Ed25519 do describe ".create" do it "generates a usable keypair with no password" do pubkey, ossh_privkey, _ = described_class.create privkey = Net::SSH::Authentication::ED25519::PrivKey.read(ossh_privkey, "").sign_key pubkey = Ed25519::VerifyKey.new(pubkey) message = "vagrant test" signature = privkey.sign(message) expect(pubkey.verify(signature, message)).to be_truthy end it "does not generate a keypair that requires a password" do expect { described_class.create("my password") }.to raise_error(NotImplementedError) end end end end ================================================ FILE: test/unit/vagrant/util/line_buffer_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/line_buffer" describe Vagrant::Util::LineBuffer do it "should raise error when no callback is provided" do expect { subject }.to raise_error(ArgumentError) end context "with block defined" do let(:block) { proc{ |l| output << l } } let(:output) { [] } let(:partial) { "this is part of a line. " } let(:line) { "this is a full line\n" } subject { described_class.new(&block) } it "should not raise an error when callback is provided" do expect { subject }.not_to raise_error end describe "#<<" do it "should add line to the output" do subject << line expect(output).to eq([line.rstrip]) end it "should not add partial line to output" do subject << partial expect(output).to be_empty end it "should add partial line to output once full line is given" do subject << partial expect(output).to be_empty subject << line expect(output).to eq([partial + line.rstrip]) end it "should add line once it has surpassed max line length" do overflow = "a" * (described_class.const_get(:MAX_LINE_LENGTH) + 1) subject << overflow expect(output).to eq([overflow]) end end describe "#close" do it "should output any partial data left in buffer" do subject << partial expect(output).to be_empty subject.close expect(output).to eq([partial]) end it "should not be writable after closing" do subject.close expect { subject << partial }.to raise_error(FrozenError) end end end end ================================================ FILE: test/unit/vagrant/util/line_endings_helper_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/line_ending_helpers" describe Vagrant::Util::LineEndingHelpers do let(:klass) do Class.new do extend Vagrant::Util::LineEndingHelpers end end it "should convert DOS to unix-style line endings" do expect(klass.dos_to_unix("foo\r\nbar\r\n")).to eq("foo\nbar\n") end end ================================================ FILE: test/unit/vagrant/util/map_command_options_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'vagrant/util/map_command_options' describe Vagrant::Util::MapCommandOptions do subject { described_class } it "should convert a map to a list of command options" do [ [{a: "opt1", b: true, c: "opt3"}, "--", ["--a", "opt1", "--b", "--c", "opt3"]], [{a: "opt1", b: false, c: "opt3"}, "-", ["-a", "opt1", "-c", "opt3"]], [{a: "opt1", b: 1}, "--", ["--a", "opt1"]], [{a: 1, b: 1}, "--", []], [{}, "--", []], [nil, nil, []] ].each do |map, cmd_flag, expected_output| expect(subject.map_to_command_options(map, cmd_flag)).to eq(expected_output) end end end ================================================ FILE: test/unit/vagrant/util/mime_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'vagrant/util/mime' require 'mime/types' describe Vagrant::Util::Mime::Multipart do let(:mime) { subject } let(:time) { 603907018 } let(:secure_random) { "123qwe" } before do allow(Time).to receive(:now).and_return(time) allow(SecureRandom).to receive(:alphanumeric).and_return(secure_random) end it "can add headers" do mime.headers["Mime-Version"] = "1.0" expected_string = "Content-ID: <#{time}@#{secure_random}.local> Content-Type: multipart/mixed; boundary=Boundary_#{secure_random} Mime-Version: 1.0 --Boundary_#{secure_random} " expect(mime.to_s).to eq(expected_string) end it "can add content" do mime.add("something") expected_string = "Content-ID: <#{time}@#{secure_random}.local> Content-Type: multipart/mixed; boundary=Boundary_#{secure_random} --Boundary_#{secure_random} something --Boundary_#{secure_random} " expect(mime.to_s).to eq(expected_string) end it "can add Vagrant::Util::Mime::Entity content" do mime.add(Vagrant::Util::Mime::Entity.new("something", "text/cloud-config")) expected_string = "Content-ID: <#{time}@#{secure_random}.local> Content-Type: multipart/mixed; boundary=Boundary_#{secure_random} --Boundary_#{secure_random} Content-ID: <#{time}@#{secure_random}.local> Content-Type: text/cloud-config something --Boundary_#{secure_random} " expect(mime.to_s).to eq(expected_string) end end describe Vagrant::Util::Mime::Entity do let(:time) { 603907018 } let(:secure_random) { "123qwe" } before do allow(Time).to receive(:now).and_return(time) allow(SecureRandom).to receive(:alphanumeric).and_return(secure_random) end it "registers the content type" do described_class.new("something", "text/cloud-config") expect(MIME::Types).to include("text/cloud-config") end it "outputs as a string" do entity = described_class.new("something", "text/cloud-config") expected_string = "Content-ID: <#{time}@#{secure_random}.local> Content-Type: text/cloud-config something" expect(entity.to_s).to eq(expected_string) end it "can set disposition" do entity = described_class.new("something", "text/cloud-config") entity.disposition = "attachment; filename='path.sh'" expected_string = "Content-ID: <#{time}@#{secure_random}.local> Content-Type: text/cloud-config Content-Disposition: attachment; filename='path.sh' something" expect(entity.to_s).to eq(expected_string) end end ================================================ FILE: test/unit/vagrant/util/network_ip_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/network_ip" describe Vagrant::Util::NetworkIP do let(:klass) do Class.new do include Vagrant::Util::NetworkIP end end subject { klass.new } describe "#network_address" do it "calculates it properly" do expect(subject.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(subject.network_address("192.168.2.234", "24")).to eq("192.168.2.0") end it "calculates it properly with integer submask" do expect(subject.network_address("192.168.2.234", 24)).to eq("192.168.2.0") end it "calculates it properly for IPv6" do expect(subject.network_address("fde4:8dba:82e1::c4", "64")).to eq("fde4:8dba:82e1::") end it "calculates it properly for IPv6" do expect(subject.network_address("fde4:8dba:82e1::c4", 64)).to eq("fde4:8dba:82e1::") end it "calculates it properly for IPv6 for string mask" do expect(subject.network_address("fde4:8dba:82e1::c4", "ffff:ffff:ffff:ffff::")).to eq("fde4:8dba:82e1::") end it "recovers from invalid netmask" do # The mask function will produce an error for ruby >= 2.5 # If using a version of ruby that produces and error, then # test to ensure `subject.network_address` produces expected # results. begin IPAddr.new("192.168.2.234").mask("1.2.3.4") rescue IPAddr::InvalidPrefixError expect(subject.network_address("192.168.2.234", "1.2.3.4")).to eq("192.168.2.0") end end end end ================================================ FILE: test/unit/vagrant/util/numeric_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/numeric" describe Vagrant::Util::Numeric do include_context "unit" before(:each) { described_class.reset! } subject { described_class } describe "#string_to_bytes" do it "converts a string to the proper bytes" do bytes = subject.string_to_bytes("10KB") expect(bytes).to eq(10240) end it "returns nil if the given string is the wrong format" do bytes = subject.string_to_bytes("10 Kilobytes") expect(bytes).to eq(nil) end end describe "bytes to megabytes" do it "converts bytes to megabytes" do expect(subject.bytes_to_megabytes(1000000)).to eq(0.95) end end end ================================================ FILE: test/unit/vagrant/util/platform_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/platform" describe Vagrant::Util::Platform do include_context "unit" before(:all) { described_class.reset! } after { described_class.reset! } subject { described_class } describe "#architecture" do let(:cpu_string) { "unknown" } before do allow(RbConfig::CONFIG). to receive(:[]).with("target_cpu"). and_return(cpu_string) end context "when cpu is x86_64" do let(:cpu_string) { "x86_64" } it "should be mapped to amd64" do expect(described_class.architecture).to eq("amd64") end end context "when cpu is x64" do let(:cpu_string) { "x64" } it "should be mapped to amd64" do expect(described_class.architecture).to eq("amd64") end end context "when cpu is i386" do let(:cpu_string) { "i386" } it "should be mapped to i386" do expect(described_class.architecture).to eq("i386") end end context "when cpu is 386" do let(:cpu_string) { "386" } it "should be mapped to 386" do expect(described_class.architecture).to eq("i386") end end context "when cpu is arm64" do let(:cpu_string) { "arm64" } it "should be arm64" do expect(described_class.architecture).to eq("arm64") end end context "when cpu is aarch64" do let(:cpu_string) { "aarch64" } it "should be mapped to arm64" do expect(described_class.architecture).to eq("arm64") end end context "when cpu is unmapped value" do let(:cpu_string) { "custom-cpu" } it "should be returned as-is" do expect(described_class.architecture).to eq(cpu_string) end end context "when environment variable override is set" do let(:host_override) { "custom-host-override" } before do allow(ENV).to receive(:[]). with("VAGRANT_HOST_ARCHITECTURE"). and_return(host_override) end it "should return the custom override" do expect(subject.architecture).to eq(host_override) end end end describe "#cygwin_path" do let(:path) { "C:\\msys2\\home\\vagrant" } let(:updated_path) { "/home/vagrant" } let(:subprocess_result) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(0) allow(result).to receive(:stdout).and_return(updated_path) end end it "takes a windows path and returns a formatted path" do allow(Vagrant::Util::Which).to receive(:which).and_return("C:/msys2/cygpath") allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(subprocess_result) expect(Vagrant::Util::Subprocess).to receive(:execute).with("C:\\msys2\\cygpath", "-u", "-a", "C:\\msys2\\home\\vagrant") expect(subject.cygwin_path(path)).to eq("/home/vagrant") end end describe "#msys_path" do let(:updated_path) { "/home/vagrant" } let(:subprocess_result) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(0) allow(result).to receive(:stdout).and_return(updated_path) end end let(:old_path) { "/old/path/bin:/usr/local/bin:/usr/bin" } it "takes a windows path and returns a formatted path" do path = ENV["PATH"] allow(Vagrant::Util::Which).to receive(:which).and_return("C:/msys2/cygpath") allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(subprocess_result) allow(ENV).to receive(:[]).with("PATH").and_return(path) allow(ENV).to receive(:[]).with("VAGRANT_OLD_ENV_PATH").and_return(old_path) expect(Vagrant::Util::Subprocess).to receive(:execute).with("C:\\msys2\\cygpath", "-u", "-a", path) expect(subject.msys_path(path)).to eq("/home/vagrant") expect(ENV["PATH"]).to eq(path) end end describe "#cygwin?" do before do allow(subject).to receive(:platform).and_return("test") end around do |example| with_temp_env(VAGRANT_DETECTED_OS: "nope", PATH: "") do example.run end end it "returns true if VAGRANT_DETECTED_OS includes cygwin" do with_temp_env(VAGRANT_DETECTED_OS: "cygwin") do expect(subject).to be_cygwin end end it "returns true if OSTYPE includes cygwin" do with_temp_env(OSTYPE: "cygwin") do expect(subject).to be_cygwin end end it "returns true if platform has cygwin" do allow(subject).to receive(:platform).and_return("cygwin") expect(subject).to be_cygwin end it "returns false if the PATH contains cygwin" do with_temp_env(PATH: "C:/cygwin") do expect(subject).to_not be_cygwin end end it "returns false if nothing is available" do expect(subject).to_not be_cygwin end end describe "#msys?" do before do allow(subject).to receive(:platform).and_return("test") end around do |example| with_temp_env(VAGRANT_DETECTED_OS: "nope", PATH: "") do example.run end end it "returns true if VAGRANT_DETECTED_OS includes msys" do with_temp_env(VAGRANT_DETECTED_OS: "msys") do expect(subject).to be_msys end end it "returns true if OSTYPE includes msys" do with_temp_env(OSTYPE: "msys") do expect(subject).to be_msys end end it "returns true if platform has msys" do allow(subject).to receive(:platform).and_return("msys") expect(subject).to be_msys end it "returns false if the PATH contains msys" do with_temp_env(PATH: "C:/msys") do expect(subject).to_not be_msys end end it "returns false if nothing is available" do expect(subject).to_not be_msys end end describe "#fs_real_path" do it "fixes drive letters on Windows", :windows do expect(described_class.fs_real_path("c:/foo").to_s).to eql("C:/foo") end it "gracefully handles invalid input string errors" do bad_string = double("bad_string") allow(bad_string).to receive(:to_s).and_raise(ArgumentError) allow_any_instance_of(String).to receive(:encode).with("filesystem").and_return(bad_string) allow(subject).to receive(:fs_case_sensitive?).and_return(false) expect(described_class.fs_real_path("/dev/null").to_s).to eql("/dev/null") end end describe "#windows_unc_path" do it "correctly converts a path" do expect(described_class.windows_unc_path("c:/foo").to_s).to eql("\\\\?\\c:\\foo") end context "when given a UNC path" do let(:unc_path){ "\\\\srvname\\path" } it "should not modify the path" do expect(described_class.windows_unc_path(unc_path).to_s).to eql(unc_path) end end end describe ".systemd?" do before{ allow(subject).to receive(:windows?).and_return(false) } context "on windows" do before{ expect(subject).to receive(:windows?).and_return(true) } it "should return false" do expect(subject.systemd?).to be_falsey end end it "should return true if systemd is in use" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double(:result, stdout: "systemd")) expect(subject.systemd?).to be_truthy end it "should return false if systemd is not in use" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double(:result, stdout: "other")) expect(subject.systemd?).to be_falsey end end describe ".wsl_validate_matching_vagrant_versions!" do let(:exe_version){ Vagrant::VERSION.to_s } before do allow(Vagrant::Util::Which).to receive(:which).and_return(true) allow(Vagrant::Util::Subprocess).to receive(:execute).with("vagrant.exe", "--version"). and_return(double(exit_code: 0, stdout: "Vagrant #{exe_version}")) end it "should not raise an error" do Vagrant::Util::Platform.wsl_validate_matching_vagrant_versions! end context "when windows vagrant.exe is not installed" do before{ expect(Vagrant::Util::Which).to receive(:which).with("vagrant.exe").and_return(nil) } it "should not raise an error" do Vagrant::Util::Platform.wsl_validate_matching_vagrant_versions! end end context "when versions do not match" do let(:exe_version){ "1.9.9" } it "should raise an error" do expect { Vagrant::Util::Platform.wsl_validate_matching_vagrant_versions! }.to raise_error(Vagrant::Errors::WSLVagrantVersionMismatch) end end end describe ".windows_hyperv_admin?" do before { allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return(nil) } it "should return false when user is not in groups and cannot access Hyper-V" do expect(Vagrant::Util::Platform.windows_hyperv_admin?).to be_falsey end context "when VAGRANT_IS_HYPERV_ADMIN environment variable is set" do before { allow(ENV).to receive(:[]).with("VAGRANT_IS_HYPERV_ADMIN").and_return("1") } it "should return true" do expect(Vagrant::Util::Platform.windows_hyperv_admin?).to be_truthy end end context "when user is in the Hyper-V administators group" do it "should return true" do expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return(["Value" => "S-1-5-32-578"].to_json) expect(Vagrant::Util::Platform.windows_hyperv_admin?).to be_truthy end end context "when user is in the Domain Admins group" do it "should return true" do expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return(["Value" => "S-1-5-21-000-000-000-512"].to_json) expect(Vagrant::Util::Platform.windows_hyperv_admin?).to be_truthy end end context "when user has access to Hyper-V" do it "should return true" do expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/GetCurrent/).and_return(nil) expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/Get-VMHost/).and_return("true") expect(Vagrant::Util::Platform.windows_hyperv_admin?).to be_truthy end end end describe ".windows_hyperv_enabled?" do it "should return true if enabled" do allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return('Enabled') expect(Vagrant::Util::Platform.windows_hyperv_enabled?).to be_truthy end it "should return false if disabled" do allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return('Disabled') expect(Vagrant::Util::Platform.windows_hyperv_enabled?).to be_falsey end it "should return false if PowerShell cannot be validated" do allow_any_instance_of(Vagrant::Errors::PowerShellInvalidVersion).to receive(:translate_error) allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_raise(Vagrant::Errors::PowerShellInvalidVersion) expect(Vagrant::Util::Platform.windows_hyperv_enabled?).to be_falsey end end context "within the WSL" do before{ allow(subject).to receive(:wsl?).and_return(true) } describe ".wsl_path?" do it "should return true when path is not within /mnt" do expect(subject.wsl_path?("/tmp")).to be(true) end it "should return false when path is within /mnt" do expect(subject.wsl_path?("/mnt/c")).to be(false) end end describe ".wsl_rootfs" do let(:appdata_path){ "C:\\Custom\\Path" } let(:registry_paths){ nil } before do allow(subject).to receive(:wsl_windows_appdata_local).and_return(appdata_path) allow(Tempfile).to receive(:new).and_return(double("tempfile", path: "file.path", close!: true)) allow(Vagrant::Util::PowerShell).to receive(:execute_cmd).and_return(registry_paths) end context "when no instance information is in the registry" do before do expect(Dir).to receive(:open).with(/.*Custom.*Path.*/).and_yield(double("path", path: appdata_path)) expect(File).to receive(:exist?).and_return(true) end it "should only check the lxrun path" do expect(subject.wsl_rootfs).to include(appdata_path) end end context "with instance information in the registry" do let(:registry_paths) { ["C:\\Path1", "C:\\Path2"].join("\r\n") } before do allow(Dir).to receive(:open).and_yield(double("path", path: appdata_path)) allow(File).to receive(:exist?).and_return(false) end context "when no matches are detected" do it "should check all paths given" do expect(Dir).to receive(:open).and_yield(double("path", path: appdata_path)).exactly(3).times expect(File).to receive(:exist?).and_return(false).exactly(3).times expect{ subject.wsl_rootfs }.to raise_error(Vagrant::Errors::WSLRootFsNotFoundError) end it "should raise not found error" do expect{ subject.wsl_rootfs }.to raise_error(Vagrant::Errors::WSLRootFsNotFoundError) end end context "when file marker match found" do let(:matching_path){ registry_paths.split("\r\n").last } let(:matching_part){ matching_path.split("\\").last } before do allow(File).to receive(:exist?).with(/#{matching_part}/).and_return(true) end it "should return the matching path" do expect(Dir).to receive(:open).with(/#{matching_part}/).and_yield(double("path", path: matching_part)) expect(subject.wsl_rootfs).to start_with(matching_path) end it "should return matching path when access error encountered" do expect(Dir).to receive(:open).with(/#{matching_part}/).and_raise(Errno::EACCES) expect(subject.wsl_rootfs).to start_with(matching_path) end end end context "when wslpath command success" do it "should check path returned by command" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double("process", exit_code: 0, stdout: "/c/Custom/Path")) expect(Dir).to receive(:open).with(/^\/c\/Custom\/Path\//).and_yield(double("path", path: appdata_path)) expect(File).to receive(:exist?).and_return(true) expect(subject.wsl_rootfs).to include(appdata_path) end end context "when wslpath command failed" do it "should check fallback path" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double("process", exit_code: 1)) expect(Dir).to receive(:open).with(/\/mnt\//).and_yield(double("path", path: appdata_path)) expect(File).to receive(:exist?).and_return(true) expect(subject.wsl_rootfs).to include(appdata_path) end end context "when wslpath command raise error" do it "should check fallback path" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_raise(Vagrant::Errors::CommandUnavailable, file: "wslpath") expect(Dir).to receive(:open).with(/\/mnt\//).and_yield(double("path", path: appdata_path)) expect(File).to receive(:exist?).and_return(true) expect(subject.wsl_rootfs).to include(appdata_path) end end end describe ".wsl_to_windows_path" do let(:path){ "/home/vagrant/test" } context "when not within WSL" do before{ allow(subject).to receive(:wsl?).and_return(false) } it "should return the path unmodified" do expect(subject.wsl_to_windows_path(path)).to eq(path) end end context "when within WSL" do before{ allow(subject).to receive(:wsl?).and_return(true) } context "when windows access is not enabled" do before{ allow(subject).to receive(:wsl_windows_access?).and_return(false) } it "should return the path unmodified" do expect(subject.wsl_to_windows_path(path)).to eq(path) end end context "when windows access is enabled" do let(:rootfs_path){ "C:\\WSL\\rootfs" } before do allow(subject).to receive(:wsl_windows_access?).and_return(true) allow(subject).to receive(:wsl_rootfs).and_return(rootfs_path) end it "should generate expanded path when within WSL" do expect(subject.wsl_to_windows_path(path)).to eq("#{rootfs_path}#{path.gsub("/", "\\")}") end it "should generate direct path when outside the WSL" do expect(subject.wsl_to_windows_path("/mnt/c/vagrant")).to eq("c:\\vagrant") end it "should not modify path when already in windows format" do expect(subject.wsl_to_windows_path("C:\\vagrant")).to eq("C:\\vagrant") end context "when within lxrun generated WSL instance" do let(:rootfs_path){ "C:\\WSL\\lxss" } it "should not include rootfs when accessing home" do expect(subject.wsl_to_windows_path("/home/vagrant")).not_to include("rootfs") end it "should include rootfs when accessing non-home path" do expect(subject.wsl_to_windows_path("/tmp/test")).to include("rootfs") end it "should properly handle Pathname" do expect(subject.wsl_to_windows_path(Pathname.new("/tmp/test"))).to include("rootfs") end end context "when wslpath command success" do it "should return path returned by command" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double("process", exit_code: 0, stdout: "C:\\Custom\\Path")) expect(subject.wsl_to_windows_path(path)).to eq("C:\\Custom\\Path") end end context "when wslpath command failed" do it "should return path by fallback" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double("process", exit_code: 1)) expect(subject.wsl_to_windows_path(path)).to eq("#{rootfs_path}#{path.gsub("/", "\\")}") end end context "when wslpath command raise error" do it "should return path by fallback" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_raise(Vagrant::Errors::CommandUnavailable, file: "wslpath") expect(subject.wsl_to_windows_path(path)).to eq("#{rootfs_path}#{path.gsub("/", "\\")}") end end end end end describe ".wsl_windows_accessible_path" do context "when within WSL" do before do allow(subject).to receive(:wsl?).and_return(true) allow(subject).to receive(:wsl_windows_home).and_return("C:\\Users\\vagrant") allow(ENV).to receive(:[]).with("VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH").and_return(nil) end context "when wslpath command success" do it "should return path returned by command" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double("process", exit_code: 0, stdout: "/d/vagrant")) expect(subject.wsl_windows_accessible_path.to_s).to eq("/d/vagrant") end end context "when wslpath command failed" do it "should return path by fallback" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(double("process", exit_code: 1)) expect(subject.wsl_windows_accessible_path.to_s).to eq("/mnt/c/Users/vagrant") end end context "when wslpath command raise error" do it "should return path by fallback" do expect(Vagrant::Util::Subprocess).to receive(:execute).and_raise(Vagrant::Errors::CommandUnavailable, file: "wslpath") expect(subject.wsl_windows_accessible_path.to_s).to eq("/mnt/c/Users/vagrant") end end end end describe ".wsl_drvfs_mounts" do let(:mount_output) { <<-EOF rootfs on / type lxfs (rw,noatime) sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,noatime) proc on /proc type proc (rw,nosuid,nodev,noexec,noatime) none on /dev type tmpfs (rw,noatime,mode=755) devpts on /dev/pts type devpts (rw,nosuid,noexec,noatime) none on /run type tmpfs (rw,nosuid,noexec,noatime,mode=755) none on /run/lock type tmpfs (rw,nosuid,nodev,noexec,noatime) none on /run/shm type tmpfs (rw,nosuid,nodev,noatime) none on /run/user type tmpfs (rw,nosuid,nodev,noexec,noatime,mode=755) binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,noatime) C: on /mnt/c type drvfs (rw,noatime) EOF } before do expect(Vagrant::Util::Subprocess).to receive(:execute).with("mount"). and_return(Vagrant::Util::Subprocess::Result.new(0, mount_output, "")) end it "should locate DrvFs mount path" do expect(subject.wsl_drvfs_mounts).to eq(["/mnt/c"]) end context "when no DrvFs mounts exist" do let(:mount_output){ "" } it "should locate no paths" do expect(subject.wsl_drvfs_mounts).to eq([]) end end end describe ".wsl_drvfs_path?" do before do expect(subject).to receive(:wsl_drvfs_mounts).and_return(["/mnt/c"]) end it "should return true when path prefix is found" do expect(subject.wsl_drvfs_path?("/mnt/c/some/path")).to be_truthy end it "should return false when path prefix is not found" do expect(subject.wsl_drvfs_path?("/home/vagrant/some/path")).to be_falsey end end end describe ".unix_windows_path" do it "takes a windows path and returns a POSIX-like path" do expect(subject.unix_windows_path("C:\\Temp\\Windows")).to eq("C:/Temp/Windows") end end end ================================================ FILE: test/unit/vagrant/util/powershell_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'vagrant/util/powershell' describe Vagrant::Util::PowerShell do include_context "unit" after{ described_class.reset! } describe ".version" do before do allow(described_class).to receive(:executable) .and_return("powershell") allow(Vagrant::Util::Subprocess).to receive(:execute) end after do described_class.version end it "should execute powershell command" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("powershell", any_args) end it "should use the default timeout" do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args, hash_including( timeout: Vagrant::Util::PowerShell::DEFAULT_VERSION_DETECTION_TIMEOUT)) end it "should use environment variable provided timeout" do with_temp_env("VAGRANT_POWERSHELL_VERSION_DETECTION_TIMEOUT" => "1") do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args, hash_including( timeout: 1)) described_class.version end end it "should use default timeout when environment variable value is invalid" do with_temp_env("VAGRANT_POWERSHELL_VERSION_DETECTION_TIMEOUT" => "invalid value") do expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args, hash_including( timeout: Vagrant::Util::PowerShell::DEFAULT_VERSION_DETECTION_TIMEOUT)) described_class.version end end end describe ".executable" do before do allow(ENV).to receive(:[]).with("VAGRANT_PREFERRED_POWERSHELL").and_return(nil) allow(Vagrant::Util::Which).to receive(:which).and_return(nil) allow(Vagrant::Util::Subprocess).to receive(:execute) do |*args| Vagrant::Util::Subprocess::Result.new(0, args.last.sub("echo ", ""), "") end end context "when powershell found in PATH" do before{ expect(Vagrant::Util::Which).to receive(:which). with("powershell").and_return("powershell") } it "should return powershell string" do expect(described_class.executable).to eq("powershell") end end context "when pwsh found in PATH" do before { expect(Vagrant::Util::Which).to receive(:which). with("pwsh").and_return("pwsh") } it "should return pwsh string" do expect(described_class.executable).to eq("pwsh") end end context "when not found in PATH" do before { allow(File).to receive(:executable?) } it "should return nil" do expect(described_class.executable).to be_nil end it "should check PATH with .exe extension" do expect(Vagrant::Util::Which).to receive(:which).with("powershell.exe") described_class.executable end it "should return powershell.exe when found" do expect(Vagrant::Util::Which).to receive(:which). with("powershell.exe").and_return("powershell.exe") expect(described_class.executable).to eq("powershell.exe") end it "should check for powershell with full path" do expect(File).to receive(:executable?).with(/WindowsPowerShell\/v1.0\/powershell.exe/) described_class.executable end end context "powershell preference" do before do allow(Vagrant::Util::Which).to receive(:which) allow(File).to receive(:executable?) end it "should prefer pwsh found on in the PATH" do expect(Vagrant::Util::Which).to receive(:which).with("pwsh.exe").and_return("pwsh.exe") expect(described_class.executable).to eq("pwsh.exe") end it "should use powershell.exe when found on PATH and pwsh.exe is not" do expect(Vagrant::Util::Which).to receive(:which).with("pwsh.exe").and_return("powershell.exe") expect(described_class.executable).to eq("powershell.exe") end it "should prefer powershell.exe when env var is set and powershell.exe and pwsh.exe are on PATH" do expect(ENV).to receive(:[]).with("VAGRANT_PREFERRED_POWERSHELL").and_return("powershell") expect(Vagrant::Util::Which).to receive(:which).with("pwsh.exe").and_return("pwsh.exe") expect(Vagrant::Util::Which).to receive(:which).with("powershell.exe").and_return("powershell.exe") expect(described_class.executable).to eq("powershell.exe") end it "should use pwsh.exe when env var is set to powershell but only pwsh.exe is avaialble" do expect(ENV).to receive(:[]).with("VAGRANT_PREFERRED_POWERSHELL").and_return("powershell") expect(Vagrant::Util::Which).to receive(:which).with("pwsh.exe").and_return("pwsh.exe") expect(Vagrant::Util::Which).to receive(:which).with("powershell.exe").and_return(nil) expect(described_class.executable).to eq("pwsh.exe") end end end describe ".available?" do context "when powershell executable is available" do before{ expect(described_class).to receive(:executable).and_return("powershell") } it "should be true" do expect(described_class.available?).to be(true) end end context "when powershell executable is not available" do before{ expect(described_class).to receive(:executable).and_return(nil) } it "should be false" do expect(described_class.available?).to be(false) end end end describe ".execute" do before do allow(described_class).to receive(:validate_install!) allow(described_class).to receive(:executable) .and_return("powershell") allow(Vagrant::Util::Subprocess).to receive(:execute) end it "should validate installation before use" do expect(described_class).to receive(:validate_install!) described_class.execute("command") end it "should include command to execute" do expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| comm = args.detect{|s| s.to_s.include?("custom-command") } expect(comm.to_s).to include("custom-command") end described_class.execute("custom-command") end it "should accept custom environment" do expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| comm = args.detect{|s| s.to_s.include?("custom-command") } expect(comm.to_s).to include("$env:TEST_KEY=test-value") end described_class.execute("custom-command", env: {"TEST_KEY" => "test-value"}) end it "should define a custom module path" do expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| comm = args.detect{|s| s.to_s.include?("custom-command") } expect(comm.to_s).to include("$env:PSModulePath+';C:\\My-Path'") end described_class.execute("custom-command", module_path: "C:\\My-Path") end end describe ".execute_cmd" do let(:result) do Vagrant::Util::Subprocess::Result.new( exit_code, stdout, stderr) end let(:exit_code){ 0 } let(:stdout){ "" } let(:stderr){ "" } before do allow(described_class).to receive(:validate_install!) allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(result) allow(described_class).to receive(:executable).and_return("powershell") end it "should validate installation before use" do expect(described_class).to receive(:validate_install!) described_class.execute_cmd("command") end it "should include command to execute" do expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| comm = args.detect{|s| s.to_s.include?("custom-command") } expect(comm.to_s).to include("custom-command") result end described_class.execute_cmd("custom-command") end it "should accept custom environment" do expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| comm = args.detect{|s| s.to_s.include?("custom-command") } expect(comm.to_s).to include("$env:TEST_KEY=test-value") result end described_class.execute_cmd("custom-command", env: {"TEST_KEY" => "test-value"}) end it "should define a custom module path" do expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| comm = args.detect{|s| s.to_s.include?("custom-command") } expect(comm.to_s).to include("$env:PSModulePath+';C:\\My-Path'") result end described_class.execute_cmd("custom-command", module_path: "C:\\My-Path") end context "with command output" do let(:stdout){ "custom-output" } it "should return stdout" do expect(described_class.execute_cmd("cmd")).to eq(stdout) end end context "with failed command" do let(:exit_code){ 1 } it "should return nil" do expect(described_class.execute_cmd("cmd")).to be_nil end end end describe ".execute_inline" do let(:result) do Vagrant::Util::Subprocess::Result.new( exit_code, stdout, stderr) end let(:exit_code){ 0 } let(:stdout){ "" } let(:stderr){ "" } let(:command) { ["run", "--this", "custom-command"] } before do allow(described_class).to receive(:validate_install!) allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(result) allow(described_class).to receive(:executable).and_return("powershell") end it "should validate installation before use" do expect(described_class).to receive(:validate_install!) described_class.execute_inline(command) end it "should include command to execute" do expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| comm = args.detect{|s| s.to_s.include?("custom-command") } expect(comm.to_s).to include("custom-command") result end described_class.execute_inline(command) end it "should accept custom environment" do expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| comm = args.detect{|s| s.to_s.include?("custom-command") } expect(comm.to_s).to include("$env:TEST_KEY=test-value") result end described_class.execute_inline(command, env: {"TEST_KEY" => "test-value"}) end it "should define a custom module path" do expect(Vagrant::Util::Subprocess).to receive(:execute) do |*args| comm = args.detect{|s| s.to_s.include?("custom-command") } expect(comm.to_s).to include("$env:PSModulePath+';C:\\My-Path'") result end described_class.execute_inline(command, module_path: "C:\\My-Path") end it "should return a result instance" do expect(described_class.execute_inline(command)).to eq(result) end end describe ".validate_install!" do before do allow(described_class).to receive(:available?).and_return(true) end context "with version under minimum required" do before{ expect(described_class).to receive(:version).and_return("2.1").at_least(:once) } it "should raise an error" do expect{ described_class.validate_install! }.to raise_error(Vagrant::Errors::PowerShellInvalidVersion) end end context "with version above minimum required" do before{ expect(described_class).to receive(:version).and_return("3.1").at_least(:once) } it "should return true" do expect(described_class.validate_install!).to be(true) end end end describe ".powerup_command" do let(:result) do Vagrant::Util::Subprocess::Result.new( exit_code, stdout, stderr) end let(:exit_code){ 0 } let(:stdout){ "" } let(:stderr){ "" } context "when the powershell executable is 'powershell'" do before do allow(described_class).to receive(:executable).and_return("powershell") end it "should use the 'powershell' executable" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("powershell", any_args).and_return(result) described_class.powerup_command("run", [], []) end end context "when the powershell executable is 'powershell.exe'" do before do allow(described_class).to receive(:executable).and_return("powershell.exe") end it "should use the 'powershell.exe' executable" do expect(Vagrant::Util::Subprocess).to receive(:execute).with("powershell.exe", any_args).and_return(result) described_class.powerup_command("run", [], []) end end end end ================================================ FILE: test/unit/vagrant/util/presence_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/presence" describe Vagrant::Util::Presence do subject { described_class } describe "#presence" do it "returns false for nil" do expect(subject.presence(nil)).to be(false) end it "returns false for false" do expect(subject.presence(false)).to be(false) end it "returns false for an empty string" do expect(subject.presence("")).to be(false) end it "returns false for a string with null bytes" do expect(subject.presence("\u0000")).to be(false) end it "returns false for an empty array" do expect(subject.presence([])).to be(false) end it "returns false for an array with nil values" do expect(subject.presence([nil, nil])).to be(false) end it "returns false for an empty hash" do expect(subject.presence({})).to be(false) end it "returns true for true" do expect(subject.presence(true)).to be(true) end it "returns the object for an object" do obj = Object.new expect(subject.presence(obj)).to be(obj) end it "returns the class for a class" do expect(subject.presence(String)).to be(String) end end end ================================================ FILE: test/unit/vagrant/util/retryable_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/retryable" describe Vagrant::Util::Retryable do let(:klass) do Class.new do extend Vagrant::Util::Retryable end end it "doesn't retry by default" do tries = 0 block = lambda do tries += 1 raise RuntimeError, "Try" end # It should re-raise the error expect { klass.retryable(&block) }. to raise_error(RuntimeError) # It should've tried once expect(tries).to eq(1) end it "retries the set number of times" do tries = 0 block = lambda do tries += 1 raise RuntimeError, "Try" end # It should re-raise the error expect { klass.retryable(tries: 5, &block) }. to raise_error(RuntimeError) # It should've tried all specified times expect(tries).to eq(5) end it "only retries on the given exception" do tries = 0 block = lambda do tries += 1 raise StandardError, "Try" end # It should re-raise the error expect { klass.retryable(tries: 5, on: RuntimeError, &block) }. to raise_error(StandardError) # It should've never tried since it was a different kind of error expect(tries).to eq(1) end it "can retry on multiple types of errors" do tries = 0 foo_error = Class.new(StandardError) bar_error = Class.new(StandardError) block = lambda do tries += 1 raise foo_error, "Try" if tries == 1 raise bar_error, "Try" if tries == 2 raise RuntimeError, "YAY" end # It should re-raise the error expect { klass.retryable(tries: 5, on: [foo_error, bar_error], &block) }. to raise_error(RuntimeError) # It should've never tried since it was a different kind of error expect(tries).to eq(3) end it "doesn't sleep between tries by default" do block = lambda do raise RuntimeError, "Try" end # Sleep should never be called expect(klass).not_to receive(:sleep) # Run it. expect { klass.retryable(tries: 5, &block) }. to raise_error(RuntimeError) end it "sleeps specified amount between retries" do block = lambda do raise RuntimeError, "Try" end # Sleep should be called between each retry expect(klass).to receive(:sleep).with(10).exactly(4).times # Run it. expect { klass.retryable(tries: 5, sleep: 10, &block) }. to raise_error(RuntimeError) end end ================================================ FILE: test/unit/vagrant/util/safe_chdir_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require 'tmpdir' require File.expand_path("../../../base", __FILE__) require 'vagrant/util/safe_chdir' describe Vagrant::Util::SafeChdir do let(:temp_dir) { Dir.mktmpdir("vagrant-test-util-safe-chdir") } let(:temp_dir2) { Dir.mktmpdir("vagrant-test-util-safe-chdir-2") } after do FileUtils.rm_rf(temp_dir) FileUtils.rm_rf(temp_dir2) end it "should change directories" do expected = nil result = nil Dir.chdir(temp_dir) do expected = Dir.pwd end described_class.safe_chdir(temp_dir) do result = Dir.pwd end expect(result).to eq(expected) end it "should allow recursive chdir" do expected = nil result = nil Dir.chdir(temp_dir) do expected = Dir.pwd end expect do described_class.safe_chdir(temp_dir2) do described_class.safe_chdir(temp_dir) do result = Dir.pwd end end end.to_not raise_error expect(result).to eq(expected) end end ================================================ FILE: test/unit/vagrant/util/scoped_hash_override_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/scoped_hash_override" describe Vagrant::Util::ScopedHashOverride do let(:klass) do Class.new do extend Vagrant::Util::ScopedHashOverride end end it "should not mess with non-overrides" do original = { key: "value", another_value: "foo" } expect(klass.scoped_hash_override(original, "foo")).to eq(original) end it "should override if the scope matches" do original = { key: "value", scope__key: "replaced" } expected = { key: "replaced", scope__key: "replaced" } expect(klass.scoped_hash_override(original, "scope")).to eq(expected) end it "should ignore non-matching scopes" do original = { key: "value", scope__key: "replaced", another__key: "value" } expected = { key: "replaced", scope__key: "replaced", another__key: "value" } expect(klass.scoped_hash_override(original, "scope")).to eq(expected) end end ================================================ FILE: test/unit/vagrant/util/shell_quote_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/shell_quote" describe Vagrant::Util::ShellQuote do subject { described_class } it "quotes properly" do expected = "foo '\\''bar'\\''" expect(subject.escape("foo 'bar'", "'")).to eql(expected) end end ================================================ FILE: test/unit/vagrant/util/ssh_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/platform" require "vagrant/util/ssh" describe Vagrant::Util::SSH do include_context "unit" let(:private_key_path) { temporary_file.to_s } describe "checking key permissions" do let(:key_path) { temporary_file } it "should do nothing on Windows" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) key_path.chmod(0700) # Get the mode now and verify that it is untouched afterwards mode = key_path.stat.mode described_class.check_key_permissions(key_path) expect(key_path.stat.mode).to eq(mode) end it "should fix the permissions", :skip_windows do key_path.chmod(0644) described_class.check_key_permissions(key_path) expect(key_path.stat.mode).to eq(0100600) end end describe "#exec" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], compression: true, dsa_authentication: true }} let(:ssh_path) { /.*ssh/ } before { allow(Vagrant::Util::Which).to receive(:which).with("ssh", any_args).and_return(ssh_path) } it "searches original PATH for executable" do expect(Vagrant::Util::Which).to receive(:which).with("ssh", original_path: true).and_return("valid-return") allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) described_class.exec(ssh_info) end it "searches current PATH if original PATH did not result in valid executable" do expect(Vagrant::Util::Which).to receive(:which).with("ssh", original_path: true).and_return(nil) expect(Vagrant::Util::Which).to receive(:which).with("ssh").and_return("valid-return") allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) described_class.exec(ssh_info) end it "raises an exception if there is no ssh" do allow(Vagrant::Util::Which).to receive(:which).and_return(nil) expect { described_class.exec(ssh_info) }. to raise_error Vagrant::Errors::SSHUnavailable end it "raises an exception if there is no ssh and platform is windows" do allow(Vagrant::Util::Which).to receive(:which).and_return(nil) allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) expect { described_class.exec(ssh_info) }. to raise_error Vagrant::Errors::SSHUnavailableWindows end it "raises an exception if the platform is windows and uses PuTTY Link" do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) allow(Vagrant::Util::Subprocess).to receive(:execute). and_return(double("output", stdout: 'PuTTY Link')) expect { described_class.exec(ssh_info) }. to raise_error Vagrant::Errors::SSHIsPuttyLink end it "invokes SSH with options if subprocess is not allowed" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) .with( ssh_path, "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "Compression=yes", "-o", "DSAAuthentication=yes", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", "-i", ssh_info[:private_key_path][0], ) end context "when deprecated algorithms are disabled" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", disable_deprecated_algorithms: true, }} it "does not include options to enable ssh-rsa key types and host key algorithms" do expect(Vagrant::Util::SafeExec).to receive(:exec) do |*args| args.each do |arg| expect(arg).not_to eq("PubkeyAcceptedKeyTypes=+ssh-rsa") expect(arg).not_to eq("HostKeyAlgorithms=+ssh-rsa") end end described_class.exec(ssh_info) end end context "when deprecated algorithms are not disabled" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", }} it "includes options to enable ssh-rsa key types and host key algorithms" do expect(Vagrant::Util::SafeExec).to receive(:exec) do |*args| expect( args.any? { |arg| arg == "PubkeyAcceptedKeyTypes=+ssh-rsa" } ).to be_truthy expect( args.any? { |arg| arg == "HostKeyAlgorithms=+ssh-rsa" } ).to be_truthy end described_class.exec(ssh_info) end end context "when using '%' in a private_key_path" do let(:private_key_path) { "/tmp/percent%folder" } let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], compression: true, dsa_authentication: true }} it "uses the IdentityFile argument and escapes the '%' character" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) described_class.exec(ssh_info) expect(Vagrant::Util::SafeExec).to have_received(:exec) .with( ssh_path, "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "Compression=yes", "-o", "DSAAuthentication=yes", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", "-o", "IdentityFile=\"/tmp/percent%%folder\"", ) end end context "when disabling compression or dsa_authentication flags" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], compression: false, dsa_authentication: false }} it "does not include compression or dsa_authentication flags if disabled" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) .with( ssh_path, "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", "-i", ssh_info[:private_key_path][0], ) end end context "when verify_host_key is true" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], verify_host_key: true }} it "does not disable StrictHostKeyChecking or set UserKnownHostsFile" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) .with( ssh_path, "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", "-i", ssh_info[:private_key_path][0], ) end end context "when not on solaris not using plain mode or with keys_only enabled" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], keys_only: true }} it "adds IdentitiesOnly as an option for ssh" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) allow(Vagrant::Util::Platform).to receive(:solaris?).and_return(false) expect(described_class.exec(ssh_info, {plain_mode: true})).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) .with( ssh_path, "localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", ) end end context "when forward_x11 is enabled" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], forward_x11: true }} it "enables ForwardX11 options" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) .with( ssh_path, "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", "-i", ssh_info[:private_key_path][0], "-o", "ForwardX11=yes", "-o", "ForwardX11Trusted=yes", ) end end context "when forward_agent is enabled" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], forward_agent: true }} it "enables agent forwarding options" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) .with( ssh_path, "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", "-i", ssh_info[:private_key_path][0], "-o", "ForwardAgent=yes", ) end end context "when extra_args is provided as an array" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], extra_args: ["-L", "8008:localhost:80"] }} it "enables agent forwarding options" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) .with( ssh_path, "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", "-i", ssh_info[:private_key_path][0], "-L", "8008:localhost:80", ) end end context "when extra_args is provided as a string" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], extra_args: "-6" }} it "enables agent forwarding options" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) expect(described_class.exec(ssh_info)).to eq(nil) expect(Vagrant::Util::SafeExec).to have_received(:exec) .with( ssh_path, "vagrant@localhost", "-p", "2222", "-o", "LogLevel=FATAL", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAcceptedKeyTypes=+ssh-rsa", "-o", "HostKeyAlgorithms=+ssh-rsa", "-i", ssh_info[:private_key_path][0], "-6", ) end end context "when config is provided" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", config: "/path/to/config" }} it "enables ssh config loading" do allow(Vagrant::Util::SafeExec).to receive(:exec).and_return(nil) expect(Vagrant::Util::SafeExec).to receive(:exec) do |exe_path, *args| expect(exe_path).to match(ssh_path) config_options = ["-F", "/path/to/config"] expect(args & config_options).to eq(config_options) end expect(described_class.exec(ssh_info)).to eq(nil) end end context "with subprocess enabled" do let(:ssh_info) {{ host: "localhost", port: 2222, username: "vagrant", private_key_path: [private_key_path], }} it "executes SSH in a subprocess with options and returns an exit code Fixnum" do # mock out ChildProcess process = double() allow(ChildProcess).to receive(:build).and_return(process) allow(process).to receive(:io).and_return(double("process-io")) allow(process.io).to receive(:inherit!).and_return(true) allow(process).to receive(:start).and_return(true) allow(process).to receive(:wait).and_return(true) allow(process).to receive(:exit_code).and_return(0) expect(described_class.exec(ssh_info, {subprocess: true})).to eq(0) end end end end ================================================ FILE: test/unit/vagrant/util/string_block_editor_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/string_block_editor" describe Vagrant::Util::StringBlockEditor do describe "#keys" do it "should return all the keys" do data = < true}) ]} let(:sleep_test_commands) {[ described_class.new("sleep", "5"), described_class.new("sleep", "5", {:detach => true}) ]} describe '#execute' do before do # ensure we have `cat` and `echo` in our PATH so that we can run these # tests successfully. ['cat', 'echo'].each do |cmd| if !Vagrant::Util::Which.which(cmd) pending("cannot run subprocess tests without command #{cmd.inspect}") end end end let (:cat) { described_class.new('cat', :notify => [:stdin]) } it 'yields the STDIN stream for the process if we set :notify => :stdin' do echo = described_class.new('echo', 'hello world', :notify => [:stdin]) echo.execute do |type, data| expect(type).to eq(:stdin) expect(data).to be_a(::IO) end end it 'can close STDIN' do result = cat.execute do |type, stdin| # We should be able to close STDIN without raising an exception stdin.close end # we should exit successfully. expect(result.exit_code).to eq(0) end it 'can write to STDIN correctly' do data = "hello world\n" result = cat.execute do |type, stdin| stdin.write(data) stdin.close end # we should exit successfully. expect(result.exit_code).to eq(0) # we should see our data as the output from `cat` expect(result.stdout).to eq(data) end it 'does not wait for process if detach option specified' do cat = described_class.new('cat', {:detach => true}) expect(cat.execute).to eq(nil) end context "running within AppImage" do let(:appimage_ld_path) { nil } let(:exec_path) { "/exec/path" } let(:appimage_path) { "/appimage" } let(:process) { double("process", io: process_io, environment: process_env) } let(:process_io) { double("process_io") } let(:process_env) { double("process_env") } let(:subject) { described_class.new(exec_path) } before do allow(process).to receive(:start) allow(process).to receive(:duplex=) allow(process).to receive(:alive?).and_return(false) allow(process).to receive(:exited?).and_return(true) allow(process).to receive(:poll_for_exit).and_return(0) allow(process).to receive(:exit_code).and_return(0) allow(process_io).to receive(:stdout=) allow(process_io).to receive(:stderr=) allow(process_io).to receive(:stdin).and_return(double("io_stdin", "sync=" => true)) allow(process_env).to receive(:[]=) allow(ENV).to receive(:[]).with("VAGRANT_INSTALLER_ENV").and_return("1") allow(ENV).to receive(:[]).with("VAGRANT_APPIMAGE").and_return("1") allow(ENV).to receive(:[]).with("VAGRANT_APPIMAGE_HOST_LD_LIBRARY_PATH").and_return(appimage_ld_path) allow(File).to receive(:file?).with(exec_path).and_return(true) allow(ChildProcess).to receive(:build).and_return(process) allow(Vagrant).to receive(:installer_embedded_dir).and_return(appimage_path) allow(Vagrant).to receive(:user_data_path).and_return("") allow(Vagrant::Util::Platform).to receive(:darwin?).and_return(false) end after { subject.execute } it "should not update LD_LIBRARY_PATH when environment variable is not set" do expect(process_env).not_to receive(:[]=).with("LD_LIBRARY_PATH", anything) end context "when APPIMAGE_LD_LIBRARY_PATH environment variable is set" do let(:appimage_ld_path) { "APPIMAGE_SYSTEM_LIBS" } it "should set LD_LIBRARY_PATH when executable is not within appimage" do expect(process_env).to receive(:[]=).with("LD_LIBRARY_PATH", appimage_ld_path) end context "when executable is located within AppImage" do let(:exec_path) { "#{appimage_path}/exec/path" } it "should not set LD_LIBRARY_PATH" do expect(process_env).not_to receive(:[]=).with("LD_LIBRARY_PATH", anything) end end end end end describe "#running?" do it "should return false when subprocess has not been started" do ls_test_commands.each do |sp| expect(sp.running?).to be(false) end end it "should return false when subprocess has completed" do ls_test_commands.each do |sp| sp.execute sleep(0.1) expect(sp.running?).to be(false) end end it "should return true when subprocess is running" do sleep_test_commands.each do |sp| thread = Thread.new{ sp.execute } sleep(0.3) expect(sp.running?).to be(true) sp.stop thread.join end end end describe "#stop" do context "when subprocess has not been started" do it "should return false" do ls_test_commands.each do |sp| expect(sp.stop).to be(false) end end end context "when subprocess has already completed" do it "should return false" do ls_test_commands.each do |sp| sp.execute sleep(0.1) expect(sp.stop).to be(false) end end end context "when subprocess is running" do it "should return true" do sleep_test_commands.each do |sp| thread = Thread.new{ sp.execute } sleep(0.1) expect(sp.stop).to be(true) thread.join end end it "should stop the process" do sleep_test_commands.each do |sp| thread = Thread.new{ sp.execute } sleep(0.1) sp.stop expect(sp.running?).to be(false) thread.join end end end end end ================================================ FILE: test/unit/vagrant/util/uploader_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require "vagrant/util/uploader" describe Vagrant::Util::Uploader do let(:destination) { "fake" } let(:file) { "my/file.box" } let(:curl_options) { [destination, "--request", "PUT", "--upload-file", file, "--fail", {notify: :stderr}] } let(:subprocess_result) do double("subprocess_result").tap do |result| allow(result).to receive(:exit_code).and_return(exit_code) allow(result).to receive(:stderr).and_return("") end end subject { described_class.new(destination, file, options) } before :each do allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(subprocess_result) end describe "#upload!" do context "with a good exit status" do let(:options) { {} } let(:exit_code) { 0 } it "uploads the file and returns true" do expect(Vagrant::Util::Subprocess).to receive(:execute). with("curl", *curl_options). and_return(subprocess_result) expect(subject.upload!).to be end end context "with a bad exit status" do let(:options) { {} } let(:exit_code) { 1 } it "raises an exception" do expect(Vagrant::Util::Subprocess).to receive(:execute). with("curl", *curl_options). and_return(subprocess_result) expect { subject.upload! }. to raise_error(Vagrant::Errors::UploaderError) end end end end ================================================ FILE: test/unit/vagrant/util/which_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../../base", __FILE__) require 'vagrant/util/platform' require 'vagrant/util/which' describe Vagrant::Util::Which do def tester (file_extension, test_extension, mode, &block) # create file in temp directory filename = '__vagrant_unit_test__' dir = Dir.tmpdir file = Pathname(dir) + (filename + file_extension) file.open("w") { |f| f.write("#") } file.chmod(mode) # set the path to the directory where the file is located allow(ENV).to receive(:[]).with("PATH").and_return(dir.to_s) block.call filename + test_extension file.unlink end it "should return a path for an executable file" do tester '.bat', '.bat', 0755 do |name| expect(described_class.which(name)).not_to be_nil end end if Vagrant::Util::Platform.windows? it "should return a path for a Windows executable file" do tester '.bat', '', 0755 do |name| expect(described_class.which(name)).not_to be_nil end end end it "should return nil for a non-executable file" do tester '.txt', '.txt', 0644 do |name| expect(described_class.which(name)).to be_nil end end context "original_path option" do before{ allow(ENV).to receive(:[]).with("PATH").and_return("") } it "should use the original path when instructed" do expect(ENV).to receive(:fetch).with("VAGRANT_OLD_ENV_PATH", any_args).and_return("") described_class.which("file", original_path: true) end end end ================================================ FILE: test/unit/vagrant/vagrantfile_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../../base", __FILE__) require "pathname" require "tmpdir" require "vagrant/vagrantfile" describe Vagrant::Vagrantfile do include_context "unit" let(:keys) { [] } let(:loader) { Vagrant::Config::Loader.new( Vagrant::Config::VERSIONS, Vagrant::Config::VERSIONS_ORDER) } subject { described_class.new(loader, keys) } before do keys << :test end def configure(&block) loader.set(:test, [["2", block]]) end # A helper to register a provider for use in tests. def register_provider(name, config_class=nil, options=nil) provider_cls = Class.new(VagrantTests::DummyProvider) do if options && options[:unusable] def self.usable?(raise_error=false) raise Vagrant::Errors::VagrantError if raise_error false end end end register_plugin("2") do |p| p.provider(name, options) { provider_cls } if config_class p.config(name, :provider) { config_class } end end provider_cls end describe "#config" do it "exposes the global configuration" do configure do |config| config.vm.box = "what" end expect(subject.config.vm.box).to eq("what") end end describe "#machine" do let(:boxes) { Vagrant::BoxCollection.new(iso_env.boxes_dir) } let(:data_path) { Pathname.new(Dir.mktmpdir("vagrant-machine-data-path")) } let(:env) { iso_env.create_vagrant_env } let(:iso_env) { isolated_environment } let(:vagrantfile) { described_class.new(loader, keys) } subject { vagrantfile.machine(:default, :foo, boxes, data_path, env) } before do @foo_config_cls = Class.new(Vagrant.plugin("2", "config")) do attr_accessor :value end @provider_cls = register_provider("foo", @foo_config_cls) configure do |config| config.vm.box = "foo" config.vm.provider "foo" do |p| p.value = "rawr" end end iso_env.box3("foo", "1.0", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 123 end VF end after do FileUtils.rm_rf(data_path.to_s) end describe '#data_dir' do subject { super().data_dir } it { should eq(data_path) } end describe '#env' do subject { super().env } it { should equal(env) } end describe '#name' do subject { super().name } it { should eq(:default) } end describe '#provider' do subject { super().provider } it { should be_kind_of(@provider_cls) } end describe '#provider_name' do subject { super().provider_name } it { should eq(:foo) } end describe '#vagrantfile' do subject { super().vagrantfile } it { should equal(vagrantfile) } end it "has the proper box" do expect(subject.box.name).to eq("foo") end it "has the valid configuration" do expect(subject.config.vm.box).to eq("foo") end it "loads the provider-specific configuration" do expect(subject.provider_config).to be_kind_of(@foo_config_cls) expect(subject.provider_config.value).to eq("rawr") end end describe "#machine_config" do let(:iso_env) { isolated_environment } let(:boxes) { Vagrant::BoxCollection.new(iso_env.boxes_dir) } it "should return a basic configured machine" do provider_cls = register_provider("foo") configure do |config| config.vm.box = "foo" end results = subject.machine_config(:default, :foo, boxes) box = results[:box] config = results[:config] expect(config.vm.box).to eq("foo") expect(box).to be_nil expect(results[:provider_cls]).to equal(provider_cls) end it "configures without a provider or boxes" do register_provider("foo") configure do |config| config.vm.box = "foo" end results = subject.machine_config(:default, nil, nil) box = results[:box] config = results[:config] expect(config.vm.box).to eq("foo") expect(box).to be_nil expect(results[:provider_cls]).to be_nil end it "configures with sub-machine config" do register_provider("foo") configure do |config| config.ssh.port = "1" config.vm.box = "base" config.vm.define "foo" do |f| f.ssh.port = 100 end end results = subject.machine_config(:foo, :foo, boxes) config = results[:config] expect(config.vm.box).to eq("base") expect(config.ssh.port).to eq(100) end it "configures with box configuration if it exists" do register_provider("foo") configure do |config| config.vm.box = "base" end iso_env.box3("base", "1.0", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 123 end VF results = subject.machine_config(:default, :foo, boxes) box = results[:box] config = results[:config] expect(config.vm.box).to eq("base") expect(config.ssh.port).to eq(123) expect(box).to_not be_nil expect(box.name).to eq("base") end it "does not configure box configuration if set to ignore" do register_provider("foo") configure do |config| config.vm.box = "base" config.vm.ignore_box_vagrantfile = true end iso_env.box3("base", "1.0", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 123 config.vm.hostname = "hello" end VF results = subject.machine_config(:default, :foo, boxes) box = results[:box] config = results[:config] expect(config.vm.box).to eq("base") expect(config.ssh.port).to eq(nil) expect(config.vm.hostname).to eq(nil) expect(box).to_not be_nil expect(box.name).to eq("base") end it "configures with the proper box version" do register_provider("foo") configure do |config| config.vm.box = "base" config.vm.box_version = "~> 1.2" end iso_env.box3("base", "1.0", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 123 end VF iso_env.box3("base", "1.3", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 245 end VF results = subject.machine_config(:default, :foo, boxes) box = results[:box] config = results[:config] expect(config.vm.box).to eq("base") expect(config.ssh.port).to eq(245) expect(box).to_not be_nil expect(box.name).to eq("base") expect(box.version).to eq("1.3") end it "configures with the custom box architecture" do register_provider("foo") configure do |config| config.vm.box = "base" config.vm.box_architecture = "custom-arch" end results = subject.machine_config(:default, :foo, boxes) config = results[:config] expect(config.vm.box).to eq("base") expect(config.vm.box_architecture).to eq("custom-arch") end it "configures with the default box architecture" do register_provider("foo") configure do |config| config.vm.box = "base" end results = subject.machine_config(:default, :foo, boxes) config = results[:config] expect(config.vm.box).to eq("base") expect(config.vm.box_architecture).to eq(:auto) end it "configures box architecture to nil" do register_provider("foo") configure do |config| config.vm.box = "base" config.vm.box_architecture = nil end results = subject.machine_config(:default, :foo, boxes) config = results[:config] expect(config.vm.box).to eq("base") expect(config.vm.box_architecture).to be_nil end it "configures with box config of other supported formats" do register_provider("foo", nil, box_format: "bar") configure do |config| config.vm.box = "base" end iso_env.box3("base", "1.0", :bar, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 123 end VF results = subject.machine_config(:default, :foo, boxes) config = results[:config] expect(config.vm.box).to eq("base") expect(config.ssh.port).to eq(123) end it "loads provider overrides if set" do register_provider("foo") register_provider("bar") configure do |config| config.ssh.port = 1 config.vm.box = "base" config.vm.provider "foo" do |_, c| c.ssh.port = 100 end end # Test with the override results = subject.machine_config(:default, :foo, boxes) config = results[:config] expect(config.vm.box).to eq("base") expect(config.ssh.port).to eq(100) # Test without the override results = subject.machine_config(:default, :bar, boxes) config = results[:config] expect(config.vm.box).to eq("base") expect(config.ssh.port).to eq(1) end it "loads the proper box if in a provider override" do register_provider("foo") configure do |config| config.vm.box = "base" config.vm.box_version = "1.0" config.vm.provider "foo" do |_, c| c.vm.box = "foobox" c.vm.box_version = "2.0" end end iso_env.box3("base", "1.0", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 123 end VF iso_env.box3("foobox", "2.0", :foo, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.ssh.port = 234 end VF results = subject.machine_config(:default, :foo, boxes) config = results[:config] box = results[:box] expect(config.vm.box).to eq("foobox") expect(config.ssh.port).to eq(234) expect(config.vm.box_version).to eq("2.0") expect(box).to_not be_nil expect(box.name).to eq("foobox") expect(box.version).to eq("2.0") end it "raises an error if the machine is not found" do expect { subject.machine_config(:foo, :foo, boxes) }. to raise_error(Vagrant::Errors::MachineNotFound) end it "raises an error if the provider is not found" do expect { subject.machine_config(:default, :foo, boxes) }. to raise_error(Vagrant::Errors::ProviderNotFound) end it "raises an error if the provider is not found but gives suggestion" do register_provider("foo") expect { subject.machine_config(:default, :Foo, boxes) }. to raise_error(Vagrant::Errors::ProviderNotFoundSuggestion) end it "raises an error if the provider is not usable" do register_provider("foo", nil, unusable: true) expect { subject.machine_config(:default, :foo, boxes) }. to raise_error(Vagrant::Errors::ProviderNotUsable) end it "does not try to load the box if the box is empty" do provider_cls = register_provider("foo") configure do |config| config.vm.box = "" end expect(boxes).not_to receive(:find) results = subject.machine_config(:default, :foo, boxes) end context "when provider validation is ignored" do before do configure do |config| config.vm.box = "base" config.vm.box_version = "1.0" config.vm.define :guest1 config.vm.define :guest2 config.vm.provider "custom" do |_, c| c.ssh.port = 123 end end iso_env.box3("base", "1.0", :custom, vagrantfile: <<-VF) Vagrant.configure("2") do |config| config.vagrant.plugins = "vagrant-custom" end VF end it "should not raise an error if provider is not found" do expect { subject.machine_config(:guest1, :custom, boxes, nil, false) }. not_to raise_error end it "should return configuration from box Vagrantfile" do config = subject.machine_config(:guest1, :custom, boxes, nil, false)[:config] expect(config.vagrant.plugins).to be_a(Hash) expect(config.vagrant.plugins.keys).to include("vagrant-custom") end end context "local box metadata file" do let(:data_path) { double(:data_path) } let(:meta_file) { double(:meta_file) } let(:box_version) { "2.0" } before do register_provider("foo") iso_env.box3("base", "1.0", :foo) allow(data_path).to receive(:join).with("box_meta"). and_return(meta_file) allow(meta_file).to receive(:file?).and_return(false) configure do |config| config.vm.box = "base" config.vm.box_version = box_version end end it "checks for local box metadata file" do expect(meta_file).to receive(:file?).and_return(false) subject.machine_config(:default, :foo, boxes, data_path) end context "file exists" do let(:meta_file_content) { '{"name":"base","version":"1.0"}' } before do allow(meta_file).to receive(:file?).and_return(true) allow(meta_file).to receive(:read).and_return(meta_file_content) end it "reads the local box metadata file" do expect(meta_file).to receive(:read).and_return(meta_file_content) subject.machine_config(:default, :foo, boxes, data_path) end it "properly loads the box defined in metadata" do result = subject.machine_config(:default, :foo, boxes, data_path) expect(result[:box]).not_to be_nil end context "with invalid box version" do let(:box_version) { "1.0" } let(:meta_file_content) { '{"name":"base","version":"2.0"}' } it "loads box base on Vagrantfile information" do result = subject.machine_config(:default, :foo, boxes, data_path) expect(result[:box]).not_to be_nil end end end end end describe "#machine_names" do it "returns the default name when single-VM" do configure { |config| } expect(subject.machine_names).to eq([:default]) end it "returns all of the names in a multi-VM" do configure do |config| config.vm.define "foo" config.vm.define "bar" end expect(subject.machine_names).to eq( [:foo, :bar]) end end describe "#machine_names_and_options" do it "returns the default name" do configure { |config| } expect(subject.machine_names_and_options).to eq({ default: { config_version: "2" }, }) end it "returns all the machines" do configure do |config| config.vm.define "foo" config.vm.define "bar", autostart: false config.vm.define "baz", autostart: true end expect(subject.machine_names_and_options).to eq({ foo: { config_version: "2" }, bar: { config_version: "2", autostart: false }, baz: { config_version: "2", autostart: true }, }) end end describe "#primary_machine_name" do it "returns the default name when single-VM" do configure { |config| } expect(subject.primary_machine_name).to eq(:default) end it "returns the designated machine in multi-VM" do configure do |config| config.vm.define "foo" config.vm.define "bar", primary: true config.vm.define "baz" end expect(subject.primary_machine_name).to eq(:bar) end it "returns nil if no designation in multi-VM" do configure do |config| config.vm.define "foo" config.vm.define "baz" end expect(subject.primary_machine_name).to be_nil end end end ================================================ FILE: test/unit/vagrant_test.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require File.expand_path("../base", __FILE__) describe Vagrant do include_context "unit" it "has the path to the source root" do expect(described_class.source_root).to eq(Pathname.new(File.expand_path("../../../", __FILE__))) end describe "plugin superclass" do describe "v1" do it "returns the proper class for version 1" do expect(described_class.plugin("1")).to eq(Vagrant::Plugin::V1::Plugin) end it "returns the proper components for version 1" do expect(described_class.plugin("1", :command)).to eq(Vagrant::Plugin::V1::Command) expect(described_class.plugin("1", :communicator)).to eq(Vagrant::Plugin::V1::Communicator) expect(described_class.plugin("1", :config)).to eq(Vagrant::Plugin::V1::Config) expect(described_class.plugin("1", :guest)).to eq(Vagrant::Plugin::V1::Guest) expect(described_class.plugin("1", :host)).to eq(Vagrant::Plugin::V1::Host) expect(described_class.plugin("1", :provider)).to eq(Vagrant::Plugin::V1::Provider) expect(described_class.plugin("1", :provisioner)).to eq(Vagrant::Plugin::V1::Provisioner) end end describe "v2" do it "returns the proper class for version 2" do expect(described_class.plugin("2")).to eq(Vagrant::Plugin::V2::Plugin) end it "returns the proper components for version 2" do expect(described_class.plugin("2", :command)).to eq(Vagrant::Plugin::V2::Command) expect(described_class.plugin("2", :communicator)).to eq(Vagrant::Plugin::V2::Communicator) expect(described_class.plugin("2", :config)).to eq(Vagrant::Plugin::V2::Config) expect(described_class.plugin("2", :guest)).to eq(Vagrant::Plugin::V2::Guest) expect(described_class.plugin("2", :host)).to eq(Vagrant::Plugin::V2::Host) expect(described_class.plugin("2", :provider)).to eq(Vagrant::Plugin::V2::Provider) expect(described_class.plugin("2", :provisioner)).to eq(Vagrant::Plugin::V2::Provisioner) expect(described_class.plugin("2", :synced_folder)).to eq(Vagrant::Plugin::V2::SyncedFolder) end end it "raises an exception if an unsupported version is given" do expect { described_class.plugin("88") }. to raise_error(ArgumentError) end end describe "has_plugin?" do it "should find the installed plugin by the registered name" do Class.new(described_class.plugin(Vagrant::Config::CURRENT_VERSION)) do name "i_am_installed" end expect(described_class.has_plugin?("i_am_installed")).to be(true) end it "should return false if the plugin is not installed" do expect(described_class.has_plugin?("i_dont_exist")).to be(false) end it "finds plugins by gem name" do specs = [Gem::Specification.new] specs[0].name = "foo" allow(Vagrant::Plugin::Manager.instance).to receive(:installed_specs).and_return(specs) allow(Vagrant::Plugin::Manager.instance).to receive(:ready?).and_return(true) expect(described_class.has_plugin?("foo")).to be(true) expect(described_class.has_plugin?("bar")).to be(false) end it "finds plugins by gem name and version" do specs = [Gem::Specification.new] specs[0].name = "foo" specs[0].version = "1.2.3" allow(Vagrant::Plugin::Manager.instance).to receive(:ready?).and_return(true) allow(Vagrant::Plugin::Manager.instance).to receive(:installed_specs).and_return(specs) expect(described_class.has_plugin?("foo", "~> 1.2.0")).to be(true) expect(described_class.has_plugin?("foo", "~> 1.0.0")).to be(false) expect(described_class.has_plugin?("bar", "~> 1.2.0")).to be(false) end end describe "require_version" do it "should succeed if valid range" do expect { described_class.require_version(Vagrant::VERSION) }. to_not raise_error end it "should not succeed if bad range" do expect { described_class.require_version("> #{Vagrant::VERSION}") }. to raise_error(Vagrant::Errors::VagrantVersionBad) end end describe "version?" do it "should succeed if valid range" do expect(described_class.version?(Vagrant::VERSION)).to be(true) end it "should not succeed if bad range" do expect(described_class.version?("> #{Vagrant::VERSION}")).to be(false) end end describe "original_env" do before do ENV["VAGRANT_OLD_ENV_foo"] = "test" ENV["VAGRANT_OLD_ENV_bar"] = "test" end after do ENV["VAGRANT_OLD_ENV_foo"] = "test" ENV["VAGRANT_OLD_ENV_bar"] = "test" end it "should return the original environment" do expect(Vagrant.original_env).to eq( "foo" => "test", "bar" => "test", ) end end end ================================================ FILE: test/vagrant-spec/.runner-vmware.sh ================================================ #!/usr/bin/env bash # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 function cleanup { vagrant destroy --force } trap cleanup EXIT GEM_PATH=$(ls vagrant-spec*.gem) set -ex if [ -f "${GEM_PATH}" ] then mv "${GEM_PATH}" vagrant-spec.gem fi vagrant box update vagrant box prune guests=$(vagrant status | grep vmware | awk '{print $1}') vagrant up --no-provision declare -A pids for guest in ${guests} do vagrant provision ${guest} & pids[$guest]=$! sleep 60 done result=0 set +e for guest in ${guests} do wait ${pids[$guest]} if [ $? -ne 0 ] then echo "Provision failure for: ${guest}" result=1 fi done exit $result ================================================ FILE: test/vagrant-spec/Vagrantfile.spec ================================================ # -*- mode: ruby -*- # vi: set ft=ruby : # Guest boxes to use for vagrant-spec GUEST_BOXES = { 'hashicorp/bionic64' => '1.0.282', 'hashicorp-vagrant/ubuntu-16.04' => '1.0.1', 'hashicorp-vagrant/centos-7.4' => '1.0.2', # 'hashicorp-vagrant/windows-10' => '1.0.0', 'spox/osx-10.12' => '0.0.1' } DOCKER_IMAGES = { 'nginx' => 'latest' } # Host boxes to run vagrant-spec HOST_BOXES = { 'hashicorp/bionic64' => '1.0.282', 'hashicorp-vagrant/ubuntu-16.04' => '1.0.1', 'hashicorp-vagrant/centos-7.4' => '1.0.2', # 'hashicorp-vagrant/windows-10' => '1.0.0', 'spox/osx-10.12' => '0.0.1' } # Not all boxes are named by their specific "platform" # so this allows Vagrant to use the right provision script PLATFORM_SCRIPT_MAPPING = { "ubuntu" => "ubuntu", "bionic" => "ubuntu", "centos" => "centos", "windows" => "windows" } # Determine what providers to test enabled_providers = ENV.fetch("VAGRANT_SPEC_PROVIDERS", "virtualbox").split(",") # Set what boxes should be used enabled_guests = ENV["VAGRANT_GUEST_BOXES"] ? ENV["VAGRANT_GUEST_BOXES"].split(",") : GUEST_BOXES.keys enabled_docker_images = ENV["VAGRANT_DOCKER_IMAGES"] ? ENV["VAGRANT_DOCKER_IMAGES"].split(",") : DOCKER_IMAGES.keys enabled_hosts = ENV["VAGRANT_HOST_BOXES"] ? ENV["VAGRANT_HOST_BOXES"].split(",") : HOST_BOXES.keys guest_boxes = Hash[GUEST_BOXES.find_all{|name, version| enabled_guests.include?(name)}.compact] docker_images = Hash[DOCKER_IMAGES.find_all{|name, version| enabled_docker_images.include?(name)}.compact] host_boxes = Hash[HOST_BOXES.find_all{|name, version| enabled_hosts.include?(name)}.compact] # Grab vagrantcloud token, if available vagrantcloud_token = ENV["VAGRANT_CLOUD_TOKEN"] # Download copies of the guest boxes for testing if missing enabled_providers.each do |provider_name| next if provider_name == "docker" guest_boxes.each do |guest_box, box_version| box_owner, box_name = guest_box.split('/') box_path = File.join(File.dirname(__FILE__), "./boxes/#{guest_box.sub('/', '_')}.#{provider_name}.#{box_version}.box") if !File.exist?(box_path) $stderr.puts "Downloading guest box #{guest_box}" cmd = "curl -Lf -o #{box_path} https://app.vagrantup.com/#{box_owner}/boxes/#{box_name}/versions/#{box_version}/providers/#{provider_name}.box" if vagrantcloud_token cmd += "?access_token=#{vagrantcloud_token}" end result = system(cmd) if !result $stderr.puts $stderr.puts "ERROR: Failed to download guest box #{guest_box} for #{provider_name}!" exit 1 end end end end Vagrant.configure(2) do |global_config| host_boxes.each do |box_name, box_version| platform = box_name.split('/').last.sub(/[^a-z]+$/, '') enabled_providers.each do |provider_name| global_config.vm.define("#{box_name.split('/').last}-#{provider_name}") do |config| config.vm.box = box_name config.vm.box_version = box_version config.vm.synced_folder '.', '/vagrant', disable: true config.vm.synced_folder '../../', '/vagrant' config.vm.provider :vmware_desktop do |vmware| vmware.vmx["memsize"] = ENV.fetch("VAGRANT_HOST_MEMORY", "5000") vmware.vmx['vhv.enable'] = 'TRUE' vmware.vmx['vhv.allow'] = 'TRUE' end if platform == "windows" config.vm.provision :shell, path: "./scripts/#{PLATFORM_SCRIPT_MAPPING[platform]}-setup.#{provider_name}.ps1", run: "once" else config.vm.provision :shell, path: "./scripts/#{PLATFORM_SCRIPT_MAPPING[platform]}-setup.#{provider_name}.sh", run: "once", env: { "HASHIBOT_USERNAME" => ENV["HASHIBOT_USERNAME"], "HASHIBOT_TOKEN" => ENV["HASHIBOT_TOKEN"] } end if provider_name == "docker" docker_images.each_with_index do |image_info, idx| docker_image, _ = image_info spec_cmd_args = ENV["VAGRANT_SPEC_ARGS"] if idx != 0 spec_cmd_args = "#{spec_cmd_args} --without-component cli/*".strip end if platform == "windows" config.vm.provision( :shell, path: "./scripts/#{platform}-run.#{provider_name}.ps1", keep_color: true, env: { "VAGRANT_SPEC_ARGS" => "test --components provider/docker/docker/* #{spec_cmd_args}".strip, "VAGRANT_SPEC_DOCKER_IMAGE" => docker_image } ) else config.vm.provision( :shell, path: "./scripts/#{PLATFORM_SCRIPT_MAPPING[platform]}-run.#{provider_name}.sh", keep_color: true, env: { "VAGRANT_SPEC_ARGS" => "test --components provider/docker/docker/* #{spec_cmd_args}".strip, "VAGRANT_SPEC_DOCKER_IMAGE" => docker_image, "VAGRANT_LOG" => "trace", "VAGRANT_LOG_LEVEL" => "trace", "VAGRANT_SPEC_LOG_PATH" => "/tmp/vagrant-spec.log", } ) end end else guest_boxes.each_with_index do |box_info, idx| guest_box, box_version = box_info guest_platform = guest_box.split('/').last.sub(/[^a-z]+$/, '') guest_platform = PLATFORM_SCRIPT_MAPPING[guest_platform] spec_cmd_args = ENV["VAGRANT_SPEC_ARGS"] if idx != 0 spec_cmd_args = "#{spec_cmd_args} --without-component cli/*".strip end if platform == "windows" config.vm.provision( :shell, path: "./scripts/#{platform}-run.#{provider_name}.ps1", keep_color: true, env: { "VAGRANT_SPEC_ARGS" => "#{spec_cmd_args}".strip, "VAGRANT_SPEC_BOX" => "c:/vagrant/#{guest_box.sub('/', '_')}.#{provider_name}.#{box_version}.box", "VAGRANT_SPEC_GUEST_PLATFORM" => guest_platform, } ) else components = [ "provider/#{provider_name}/basic", "provider/#{provider_name}/provisioner/shell", ] config.vm.provision( :shell, path: "./scripts/#{PLATFORM_SCRIPT_MAPPING[platform]}-run.#{provider_name}.sh", keep_color: true, env: { # "VAGRANT_SPEC_ARGS" => "test #{spec_cmd_args}".strip, # TEMP: Forcing just the basic component of the provider suite as not all tests are passing yet. # Hoping to widen this out over time to be unscoped with everything passing. "VAGRANT_SPEC_ARGS" => "test --components #{components.join(" ")}", "VAGRANT_SPEC_BOX" => "/vagrant/test/vagrant-spec/boxes/#{guest_box.sub('/', '_')}.#{provider_name}.#{box_version}.box", "VAGRANT_SPEC_GUEST_PLATFORM" => guest_platform, "VAGRANT_LOG" => "trace", "VAGRANT_LOG_LEVEL" => "trace", "VAGRANT_SPEC_LOG_PATH" => "/tmp/vagrant-spec.log", } ) end end end end end end end ================================================ FILE: test/vagrant-spec/boxes/.keep ================================================ ================================================ FILE: test/vagrant-spec/configs/vagrant-spec.config.docker.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../acceptance/base" Vagrant::Spec::Acceptance.configure do |c| c.component_paths << File.expand_path("../../../acceptance", __FILE__) c.skeleton_paths << File.expand_path("../../../acceptance/skeletons", __FILE__) # Allow for slow setup to still pass c.assert_retries = 15 c.vagrant_path = ENV.fetch("VAGRANT_PATH", "vagrant") c.provider "docker", image: ENV["VAGRANT_SPEC_DOCKER_IMAGE"], box: "placeholder" end ================================================ FILE: test/vagrant-spec/configs/vagrant-spec.config.virtualbox.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "../../acceptance/base" Vagrant::Spec::Acceptance.configure do |c| c.component_paths << File.expand_path("../test/acceptance", __FILE__) c.skeleton_paths << File.expand_path("../test/acceptance/skeletons", __FILE__) # Allow for slow setup to still pass c.assert_retries = 15 c.vagrant_path = ENV.fetch("VAGRANT_PATH", "vagrant") c.provider "virtualbox", box: ENV["VAGRANT_SPEC_BOX"], contexts: ["provider-context/virtualbox"] end ================================================ FILE: test/vagrant-spec/readme.md ================================================ # Running vagrant-spec The vagrant-spec project is where Vagrant acceptance tests live. __NOTE:__ You must use a hypervisor that allows for nested virtualization to run these tests. So for the _vagrant_ project, it uses the vagrant vmware plugin as a host. If you want to test this locally, please keep in mind that you will need this hypervisor to properly run the tests. ## Requirements - vagrant installed (from source, or from packages) - vagrant vmware plugin - ![vagrant](https://github.com/hashicorp/vagrant) repo - ![vagrant-spec](https://github.com/hashicorp/vagrant-spec) repo ## Relevant environment variables: Below are some environment variables used for running vagrant-spec. Many of these are required for defining which hosts and guests to run the tests on. - VAGRANT_CLOUD_TOKEN + Token to use if fetching a private box (like windows). This does not have to be explicitly set if you log into Vagrant cloud with `vagrant cloud login`. - VAGRANT_HOST_BOXES - Vagrant box to use as a host for installing VirtualBox and bringing up Vagrant guests to test - VAGRANT_GUEST_BOXES - Vagrant box to use as a guest to run tests on - VAGRANT_CWD - Directory location of vagrant-spec Vagrantfile inside of the Vagrant source repo - VAGRANT_VAGRANTFILE - Vagrantfile to use for running vagrant-spec. Unless changed, this should be set as `Vagrantfile.spec`. - VAGRANT_HOST_MEMORY - Set how much memory your host will use (defaults to 2048) - VAGRANT_SPEC_ARGS - Specific arguments to pass along to the vagrant-spec gem, such as running specific tests instead of the whole suite - Example: `--component cli` ## How to run First, we need to build vagrant-spec and copy the built gem into the Vagrant source repo: ``` cd vagrant-spec gem build *.gemspec cp vagrant-spec-0.0.1.gem /path/to/vagrant/vagrant-spec.gem ``` Next, make a decision as to which host and guest boxes will be used to run the tests. A list of valid hosts and guests can be found in the `Vagrantfile.spec` adjacent to this readme. From the root dir of the `vagrant` project, run the following command: ```shell VAGRANT_CLOUD_TOKEN=REAL_TOKEN_HERE VAGRANT_HOST_BOXES=hashicorp-vagrant/centos-7.4 VAGRANT_GUEST_BOXES=hashicorp-vagrant/windows-10 VAGRANT_CWD=test/vagrant-spec/ VAGRANT_VAGRANTFILE=Vagrantfile.spec vagrant up --provider vmware_desktop ``` If you are running windows, you must give your host box more memory than the default. That can be done through the environment variable `VAGRANT_HOST_MEMORY` ```shell VAGRANT_HOST_MEMORY=10000 VAGRANT_CLOUD_TOKEN=REAL_TOKEN_HERE VAGRANT_HOST_BOXES=hashicorp-vagrant/centos-7.4 VAGRANT_GUEST_BOXES=hashicorp-vagrant/windows-10 VAGRANT_CWD=test/vagrant-spec/ VAGRANT_VAGRANTFILE=Vagrantfile.spec vagrant up --provider vmware_desktop ``` __Note:__ It is not required that you invoke Vagrant directly in the source repo, so if you wish to run it else where, be sure to properly set the `VAGRANT_CWD` environment variable to point to the proper test directory inside of the Vagrant source. ### How to run specific tests Sometimes when debugging, it's useful to only run a small subset of tests, instead of waiting for everything to run. This can be achieved by passing along arguments using the `VAGRANT_SPEC_ARGS` environment variable: For example, here is what you could set to only run cli tests ```shell VAGRANT_SPEC_ARGS="--component cli" ``` Or with the full command.... ```shell VAGRANT_SPEC_ARGS="--component cli" VAGRANT_CLOUD_TOKEN=REAL_TOKEN_HERE VAGRANT_HOST_BOXES=hashicorp-vagrant/centos-7.4 VAGRANT_GUEST_BOXES=hashicorp-vagrant/windows-10 VAGRANT_CWD=test/vagrant-spec/ VAGRANT_VAGRANTFILE=Vagrantfile.spec vagrant up --provider vmware_desktop ``` ### About Vagrantfile.spec This Vagrantfile expects the box used to end in a specific "platform", so that it can associate a provision script with the correct plaform. Because some boxes might not end in their platform (like `hashicorp-vagrant/ubuntu-16.04` versus `hashicorp/bionic64`), there is a hash defined called `PLATFORM_SCRIPT_MAPPING` that will tell vagrant which platform script to provision with rather than relying on the box ending with the name of the platform. ================================================ FILE: test/vagrant-spec/scripts/centos-run.virtualbox.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 set -x export VAGRANT_EXPERIMENTAL="${VAGRANT_EXPERIMENTAL:-1}" export VAGRANT_SPEC_BOX="${VAGRANT_SPEC_BOX}" vagrant-spec ${VAGRANT_SPEC_ARGS} --config /vagrant/test/vagrant-spec/configs/vagrant-spec.config.virtualbox.rb result=$? exit $result ================================================ FILE: test/vagrant-spec/scripts/centos-setup.virtualbox.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 set -xe curl -Lo /etc/yum.repos.d/virtualbox.repo http://download.virtualbox.org/virtualbox/rpm/rhel/virtualbox.repo yum groupinstall -y "Development Tools" yum install -y kernel-devel-$(uname -r) yum install -y VirtualBox-${VAGRANT_CENTOS_VIRTUALBOX_VERSION:-5.1} # Install Go wget -qO go.tar.gz https://go.dev/dl/go1.17.6.linux-amd64.tar.gz tar -xzf go.tar.gz --directory /usr/local export PATH=$PATH:/usr/local/go/bin go version # Install Ruby curl -sSL https://rvm.io/pkuczynski.asc | sudo gpg --import - curl -sSL https://get.rvm.io | bash -s stable --ruby source .bashrc pushd /vagrant # Get vagrant-plugin-sdk repo git config --global url."https://${HASHIBOT_USERNAME}:${HASHIBOT_TOKEN}@github.com".insteadOf "https://github.com" # Build Vagrant artifacts gem install bundler -v "$(grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1)" make bundle install ln -s /vagrant/vagrant /bin/vagrant popd # Install vagrant-spec git clone https://github.com/hashicorp/vagrant-spec.git pushd vagrant-spec gem build vagrant-spec.gemspec gem install vagrant-spec*.gem vagrant-spec -h popd ================================================ FILE: test/vagrant-spec/scripts/ubuntu-install-vagrant.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 set -e # Install Go wget -qO go.tar.gz https://go.dev/dl/go1.17.6.linux-amd64.tar.gz tar -xzf go.tar.gz --directory /usr/local export PATH=$PATH:/usr/local/go/bin go version # Install Ruby curl -sSL https://rvm.io/pkuczynski.asc | sudo gpg --import - curl -sSL https://get.rvm.io | bash -s stable source /usr/local/rvm/scripts/rvm rvm install ruby-2.7.2 rvm --default use ruby-2.7.2 # Remove RVM's automatically installed bundler integration, which messes w/ # Vagrant's ruby binary invocation gem uninstall -i /usr/local/rvm/rubies/ruby-2.7.2/lib/ruby/gems/2.7.0 rubygems-bundler pushd /vagrant # Get vagrant-plugin-sdk repo git config --global url."https://${HASHIBOT_USERNAME}:${HASHIBOT_TOKEN}@github.com".insteadOf "https://github.com" # Build Vagrant artifacts gem install bundler -v "$(grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1)" make bundle install gem build -o /tmp/vagrant.gem vagrant.gemspec gem install /tmp/vagrant.gem popd # Install vagrant-spec git clone https://github.com/hashicorp/vagrant-spec.git pushd vagrant-spec # TEMP: We are using a branch of vagrant-spec while we stabilize the changes # necessary. Once this branch lands we can remove this line and build from main. git checkout vagrant-ruby gem build -o /tmp/vagrant-spec.gem vagrant-spec.gemspec gem install /tmp/vagrant-spec.gem popd ================================================ FILE: test/vagrant-spec/scripts/ubuntu-run.docker.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 export VAGRANT_SPEC_DOCKER_IMAGE="${VAGRANT_SPEC_DOCKER_IMAGE}" # Explicitly use Go binary export VAGRANT_PATH=/vagrant/vagrant # Explicitly set high open file limits... vagrant-ruby tends to run into the # default 1024 limit during some operations. ulimit -n 65535 vagrant-spec ${VAGRANT_SPEC_ARGS} --config /vagrant/test/vagrant-spec/configs/vagrant-spec.config.docker.rb result=$? exit $result ================================================ FILE: test/vagrant-spec/scripts/ubuntu-run.virtualbox.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 export VAGRANT_EXPERIMENTAL="${VAGRANT_EXPERIMENTAL:-1}" export VAGRANT_SPEC_BOX="${VAGRANT_SPEC_BOX}" # Explicitly use Go binary export VAGRANT_PATH=/vagrant/vagrant # Explicitly set high open file limits... vagrant-ruby tends to run into the # default 1024 limit during some operations. ulimit -n 65535 vagrant-spec ${VAGRANT_SPEC_ARGS} --config /vagrant/test/vagrant-spec/configs/vagrant-spec.config.virtualbox.rb result=$? exit $result ================================================ FILE: test/vagrant-spec/scripts/ubuntu-setup.docker.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 set -e apt-get update -q apt-get install -qq -y --force-yes curl apt-transport-https apt-get purge -qq -y lxc-docker* || true curl -sSL https://get.docker.com/ | sh /bin/bash /vagrant/test/vagrant-spec/scripts/ubuntu-install-vagrant.sh ================================================ FILE: test/vagrant-spec/scripts/ubuntu-setup.virtualbox.sh ================================================ #!/bin/bash # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 set -e export DEBIAN_FRONTEND=noninteractive apt-get update -q apt-get install -qqy linux-headers-$(uname -r) apt-get install -qqy virtualbox apt-get install -qqy nfs-kernel-server /bin/bash /vagrant/test/vagrant-spec/scripts/ubuntu-install-vagrant.sh ================================================ FILE: test/vagrant-spec/scripts/windows-run.virtualbox.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 cd /vagrant vagrant plugin install ./vagrant-spec.gem if ( $env:VAGRANT_EXPERIMENTAL -eq "" ) { $env:VAGRANT_EXPERIMENTAL="1" } vagrant vagrant-spec $Env:VAGRANT_SPEC_ARGS /vagrant/test/vagrant-spec/configs/vagrant-spec.config.virtualbox.rb ================================================ FILE: test/vagrant-spec/scripts/windows-setup.virtualbox.ps1 ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 Write-Output "Downloading virtualbox guest additions" $vboxadd_url = "http://download.virtualbox.org/virtualbox/5.2.2/VBoxGuestAdditions_5.2.2.iso" $vboxadd_output = "C:/Windows/Temp/vboxguestadditions.iso" (New-Object System.Net.WebClient).DownloadFile($vboxadd_url, $vboxadd_output) Write-Output "Mounting virtualbox guest additions" Mount-DiskImage -ImagePath $vboxadd_output Write-Output "Installing Virtualbox Guest Additions" Write-Output "Checking for Certificates in vBox ISO" if(test-path E:\ -Filter *.cer) { Get-ChildItem E:\cert -Filter *.cer | ForEach-Object { certutil -addstore -f "TrustedPublisher" $_.FullName } } Start-Process -FilePath "E:\VBoxWindowsAdditions.exe" -ArgumentList "/S" -Wait Write-Output "Downloading virtualbox" $vbox_url = "http://download.virtualbox.org/virtualbox/5.2.2/VirtualBox-5.2.2-119230-Win.exe" $vbox_output = "C:/Windows/Temp/virtualbox.exe" (New-Object System.Net.WebClient).DownloadFile($vbox_url, $vbox_output) Write-Output "Installing virtualbox" # Extract the contents of the installer Start-Process -FilePath $vbox_output ` -ArgumentList ('--extract','--silent','--path','C:\Windows\Temp') ` -Wait ` -NoNewWindow # Find the installer # 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" } cd "C:\Windows\Temp" $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)\vbox_install.log`"" ` -Verb RunAs ` -Wait ` -WorkingDirectory "$pwd" cd "C:\vagrant\pkg\dist" $vagrant_matches = Get-ChildItem | Where-Object { $_.Name -match "vagrant.*_x86_64.msi" } if ($vagrant_matches.Count -ne 1) { Write-Host "Could not find vagrant installer" exit 1 } $vagrant_installerPath = Resolve-Path $vagrant_matches[0] Write-Output $vagrant_installerPath Write-Output "Installing vagrant" Start-Process -FilePath "$($env:systemroot)\System32\msiexec.exe" ` -ArgumentList "/i `"$vagrant_installerPath`" /qn /norestart /l*v `"$($pwd)\vagrant_install.log`"" ` -Verb RunAs ` -Wait ` -WorkingDirectory "$pwd" ================================================ FILE: vagrant-spec.config.example.rb ================================================ # Copyright IBM Corp. 2010, 2025 # SPDX-License-Identifier: BUSL-1.1 require_relative "test/acceptance/base" Vagrant::Spec::Acceptance.configure do |c| c.component_paths << File.expand_path("../test/acceptance", __FILE__) c.skeleton_paths << File.expand_path("../test/acceptance/skeletons", __FILE__) c.provider "virtualbox", box: "", contexts: ["provider-context/virtualbox"] end ================================================ FILE: vagrant.gemspec ================================================ $:.unshift File.expand_path("../lib", __FILE__) require "vagrant/version" Gem::Specification.new do |s| s.name = "vagrant" s.version = Vagrant::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Mitchell Hashimoto", "John Bender"] s.email = ["mitchell.hashimoto@gmail.com", "john.m.bender@gmail.com"] s.homepage = "https://www.vagrantup.com" s.license = 'BUSL-1.1' s.summary = "Build and distribute virtualized development environments." s.description = "Vagrant is a tool for building and distributing virtualized development environments." s.required_ruby_version = ">= 3.0", "< 3.5" s.required_rubygems_version = ">= 1.3.6" s.add_dependency "base64", "~> 0.2.0" s.add_dependency "bcrypt_pbkdf", "~> 1.1" s.add_dependency "childprocess", "~> 5.1" s.add_dependency "csv", "~> 3.3" s.add_dependency "ed25519", "~> 1.3.0" s.add_dependency "erubi" s.add_dependency "hashicorp-checkpoint", "~> 0.1.5" s.add_dependency "i18n", "~> 1.12" s.add_dependency "listen", "~> 3.7" s.add_dependency "log4r", "~> 1.1.9", "< 1.1.11" s.add_dependency "logger", "~> 1.0" s.add_dependency "mime-types", "~> 3.3" s.add_dependency "net-ftp", "~> 0.2" s.add_dependency "net-ssh", "~> 7.0" s.add_dependency "net-sftp", "~> 4.0" s.add_dependency "net-scp", "~> 4.0" s.add_dependency "ostruct", "~> 0.6.0" s.add_dependency "rb-kqueue", "~> 0.2.0" s.add_dependency "rexml", "~> 3.2" s.add_dependency "rubyzip", "~> 2.3.2" s.add_dependency "vagrant_cloud", "~> 3.1.2" s.add_dependency "wdm", "~> 0.2.0" s.add_dependency "winrm", ">= 2.3.9", "< 3.0" s.add_dependency "winrm-elevated", ">= 1.2.3", "< 2.0" s.add_dependency "winrm-fs", ">= 1.3.5", "< 2.0" # Needed for go generate to use grpc_tools_ruby_protoc s.add_development_dependency "grpc-tools", "~> 1.41" # required to include https://github.com/ruby/ipaddr/issues/35 s.add_dependency "ipaddr", ">= 1.2.4" # Constraint rake to properly handle deprecated method usage # from within rspec s.add_development_dependency "rake", "~> 13.0" s.add_development_dependency "rake-compiler" s.add_development_dependency "rspec", "~> 3.11" s.add_development_dependency "rspec-its", "~> 1.3.0" s.add_development_dependency "fake_ftp", "~> 0.3.0" s.add_development_dependency "webrick", "~> 1.7" # The following block of code determines the files that should be included # in the gem. It does this by reading all the files in the directory where # this gemspec is, and parsing out the ignored files from the gitignore. # Note that the entire gitignore(5) syntax is not supported, specifically # the "!" syntax, but it should mostly work correctly. root_path = File.dirname(__FILE__) all_files = Dir.chdir(root_path) { Dir.glob("**/{*,.*}") } all_files.reject! { |file| [".", ".."].include?(File.basename(file)) } all_files.reject! { |file| file.start_with?("website/") } all_files.reject! { |file| file.start_with?("test/") } all_files.reject! { |file| file.start_with?("cmd/") } all_files.reject! { |file| file.start_with?("builtin/") } all_files.reject! { |file| file.start_with?("internal/") } all_files.reject! { |file| file.start_with?("vendor/") } gitignore_path = File.join(root_path, ".gitignore") gitignore = File.readlines(gitignore_path) gitignore.map! { |line| line.chomp.strip } gitignore.reject! { |line| line.empty? || line =~ /^(#|!)/ } gitmodules_path = File.join(root_path, ".gitmodules") gitmodules = File.readlines(gitmodules_path) gitmodules.map! { |line| line.chomp.strip } gitmodules.reject! { |line| line.empty? || line =~ /^(#|!)/ } unignored_files = all_files.reject do |file| # Ignore any directories, the gemspec only cares about files next true if File.directory?(file) # Ignore any paths that match anything in the gitignore. We do # two tests here: # # - First, test to see if the entire path matches the gitignore. # - Second, match if the basename does, this makes it so that things # like '.DS_Store' will match sub-directories too (same behavior # as git). # gitignore.any? do |ignore| File.fnmatch(ignore, file, File::FNM_PATHNAME) || File.fnmatch(ignore, File.basename(file), File::FNM_PATHNAME) end gitmodules.any? do |ignore| File.fnmatch(ignore, file, File::FNM_PATHNAME) || File.fnmatch(ignore, File.basename(file), File::FNM_PATHNAME) end end s.files = unignored_files s.executables = unignored_files.map { |f| f[/^bin\/(.*)/, 1] }.compact s.extensions = ["ext/vagrant/vagrant_ssl/extconf.rb"] s.require_path = 'lib' end ================================================ FILE: version.txt ================================================ 2.4.10.dev ================================================ FILE: website/.editorconfig ================================================ # This file is for unifying the coding style for different editors and IDEs # editorconfig.org root = true [*] end_of_line = lf charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 2 [Makefile] indent_style = tab [{*.md,*.json}] max_line_length = null ================================================ FILE: website/.env ================================================ NEXT_PUBLIC_ALGOLIA_APP_ID=YY0FFNI7MF NEXT_PUBLIC_ALGOLIA_INDEX=product_VAGRANT NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY=c4c3e690f46940fa3ba9624da4d7fc0c ================================================ FILE: website/.eslintrc.js ================================================ /** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ module.exports = { ...require('@hashicorp/platform-cli/config/.eslintrc'), ignorePatterns: ['public/'], } ================================================ FILE: website/.gitignore ================================================ node_modules .DS_Store .next out .mdx-data # As per Next.js conventions (https://nextjs.org/docs/basic-features/environment-variables#default-environment-variables) !.env .env*.local website-preview ================================================ FILE: website/.nvmrc ================================================ v22 ================================================ FILE: website/.stylelintrc.js ================================================ /** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ module.exports = { ...require('@hashicorp/platform-cli/config/stylelint.config'), } ================================================ FILE: website/LICENSE.md ================================================ # Proprietary License This license is temporary while a more official one is drafted. However, this should make it clear: The text contents of this website are MPL 2.0 licensed. The design contents of this website are proprietary and may not be reproduced or reused in any way other than to run the website locally. The license for the design is owned solely by HashiCorp, Inc. ================================================ FILE: website/Makefile ================================================ ###################################################### # NOTE: This file is managed by the Digital Team's # # Terraform configuration @ hashicorp/mktg-terraform # ###################################################### .DEFAULT_GOAL := website # Set the preview mode for the website shell to "developer" or "io" PREVIEW_MODE ?= developer REPO ?= vagrant # Enable setting alternate docker tool, e.g. 'make DOCKER_CMD=podman' DOCKER_CMD ?= docker CURRENT_GIT_BRANCH=$$(git rev-parse --abbrev-ref HEAD) LOCAL_CONTENT_DIR= PWD=$$(pwd) DOCKER_IMAGE="hashicorp/dev-portal" DOCKER_IMAGE_LOCAL="dev-portal-local" DOCKER_RUN_FLAGS=-it \ --publish "3000:3000" \ --rm \ --tty \ --volume "$(PWD)/content:/app/content" \ --volume "$(PWD)/public:/app/public" \ --volume "$(PWD)/data:/app/data" \ --volume "$(PWD)/redirects.js:/app/redirects.js" \ --volume "next-dir:/app/website-preview/.next" \ --volume "$(PWD)/.env:/app/.env" \ --volume "$(PWD)/.env.development:/app/website-preview/.env.development" \ --volume "$(PWD)/.env.local:/app/website-preview/.env.local" \ -e "REPO=$(REPO)" \ -e "PREVIEW_FROM_REPO=$(REPO)" \ -e "IS_CONTENT_PREVIEW=true" \ -e "LOCAL_CONTENT_DIR=$(LOCAL_CONTENT_DIR)" \ -e "CURRENT_GIT_BRANCH=$(CURRENT_GIT_BRANCH)" \ -e "PREVIEW_MODE=$(PREVIEW_MODE)" # Default: run this if working on the website locally to run in watch mode. .PHONY: website website: @echo "==> Downloading latest Docker image..." @$(DOCKER_CMD) pull $(DOCKER_IMAGE) @echo "==> Starting website..." @$(DOCKER_CMD) run $(DOCKER_RUN_FLAGS) $(DOCKER_IMAGE) # Use this if you have run `website/build-local` to use the locally built image. .PHONY: website/local website/local: @echo "==> Starting website from local image..." @$(DOCKER_CMD) run $(DOCKER_RUN_FLAGS) $(DOCKER_IMAGE_LOCAL) # Run this to generate a new local Docker image. .PHONY: website/build-local website/build-local: @echo "==> Building local Docker image" @$(DOCKER_CMD) build https://github.com/hashicorp/dev-portal.git\#main \ -t $(DOCKER_IMAGE_LOCAL) ================================================ FILE: website/README.md ================================================ ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Vagrant Website This subdirectory contains the content for the [Vagrant Website](https://vagrantup.com/). ## Table of Contents - [Contributions](#contributions-welcome) - [Running the Site Locally](#running-the-site-locally) - [Editing Markdown Content](#editing-markdown-content) - [Editing Navigation Sidebars](#editing-navigation-sidebars) - [Changing the Release Version](#changing-the-release-version) - [Redirects](#redirects) - [Browser Support](#browser-support) - [Deployment](#deployment) ## Contributions Welcome! If you find a typo or you feel like you can improve the HTML, CSS, or JavaScript, we welcome contributions. Feel free to open issues or pull requests like any normal GitHub project, and we'll merge it in 🚀 ## Running the Site Locally The website can be run locally through node.js or [Docker](https://www.docker.com/get-started). If you choose to run through Docker, everything will be a little bit slower due to the additional overhead, so for frequent contributors it may be worth it to use node. > **Note:** If you are using a text editor that uses a "safe write" save style such as **vim** or **goland**, this can cause issues with the live reload in development. If you turn off safe write, this should solve the problem. In vim, this can be done by running `:set backupcopy=yes`. In goland, search the settings for "safe write" and turn that setting off. ### With Docker Running the site locally is simple. Provided you have Docker installed, clone this repo, run `make`, and then visit `http://localhost:3000`. The docker image is pre-built with all the website dependencies installed, which is what makes it so quick and simple, but also means if you need to change dependencies and test the changes within Docker, you'll need a new image. If this is something you need to do, you can run `make build-image` to generate a local Docker image with updated dependencies, then `make website-local` to use that image and preview. ### With Node If your local development environment has a supported version (v22+) of [node installed](https://nodejs.org/en/) you can run: - `npm install` - `npm start` ...and then visit `http://localhost:3000`. If you pull down new code from github, you should run `npm install` again. Otherwise, there's no need to re-run `npm install` each time the site is run, you can just run `npm start` to get it going. ## Editing Markdown Content Documentation content is written in [Markdown](https://www.markdownguide.org/cheat-sheet/) and you'll find all files listed under the `/content` directory. To create a new page with Markdown, create a file ending in `.mdx` in a `content/`. The path in the content directory will be the URL route. For example, `content/docs/hello.mdx` will be served from the `/docs/hello` URL. > **Important**: Files and directories will only be rendered and published to the website if they are [included in sidebar data](#editing-navigation-sidebars). Any file not included in sidebar data will not be rendered or published. This file can be standard Markdown and also supports [YAML frontmatter](https://middlemanapp.com/basics/frontmatter/). YAML frontmatter is optional, there are defaults for all keys. ```yaml --- title: 'My Title' description: "A thorough, yet succinct description of the page's contents" --- ``` The significant keys in the YAML frontmatter are: - `title` `(string)` - This is the title of the page that will be set in the HTML title. - `description` `(string)` - This is a description of the page that will be set in the HTML description. > ⚠️ If there is a need for a `/api/*` url on this website, the url will be changed to `/api-docs/*`, as the `api` folder is reserved by next.js. ### Validating Content Content changes are automatically validated against a set of rules as part of the pull request process. If you want to run these checks locally to validate your content before comitting your changes, you can run the following command: ``` npm run content-check ``` If the validation fails, actionable error messages will be displayed to help you address detected issues. ### Creating New Pages There is currently a small bug with new page creation - if you create a new page and link it up via subnav data while the server is running, it will report an error saying the page was not found. This can be resolved by restarting the server. ### Markdown Enhancements There are several custom markdown plugins that are available by default that enhance [standard markdown](https://commonmark.org/) to fit our use cases. This set of plugins introduces a couple instances of custom syntax, and a couple specific pitfalls that are not present by default with markdown, detailed below: - > **Warning**: We are deprecating the current [paragraph alerts](https://github.com/hashicorp/remark-plugins/tree/master/plugins/paragraph-custom-alerts#paragraph-custom-alerts), in favor of the newer [MDX Inline Alert](#inline-alerts) components. The legacy paragraph alerts are represented by the symbols `~>`, `->`, `=>`, or `!>`. - If you see `@include '/some/path.mdx'`, this is a [markdown include](https://github.com/hashicorp/remark-plugins/tree/master/plugins/include-markdown#include-markdown-plugin). It's worth noting as well that all includes resolve from `website/content/partials` by default, and that changes to partials will not live-reload the website. - If you see `# Headline ((#slug))`, this is an example of an [anchor link alias](https://github.com/hashicorp/remark-plugins/tree/je.anchor-link-adjustments/plugins/anchor-links#anchor-link-aliases). It adds an extra permalink to a headline for compatibility and is removed from the output. - Due to [automatically generated permalinks](https://github.com/hashicorp/remark-plugins/tree/je.anchor-link-adjustments/plugins/anchor-links#anchor-links), any text changes to _headlines_ or _list items that begin with inline code_ can and will break existing permalinks. Be very cautious when changing either of these two text items. Headlines are fairly self-explanatory, but here's an example of how to list items that begin with inline code look. ```markdown - this is a normal list item - `this` is a list item that begins with inline code ``` Its worth noting that _only the inline code at the beginning of the list item_ will cause problems if changed. So if you changed the above markup to... ```markdown - lsdhfhksdjf - `this` jsdhfkdsjhkdsfjh ``` ...while it perhaps would not be an improved user experience, no links would break because of it. The best approach is to **avoid changing headlines and inline code at the start of a list item**. If you must change one of these items, make sure to tag someone from the digital marketing development team on your pull request, they will help to ensure as much compatibility as possible. ### Custom Components A number of custom [mdx components](https://mdxjs.com/) are available for use within any `.mdx` file. Each one is documented below: #### Inline Alerts There are custom MDX components available to author alert data. [See the full documentation here](https://developer.hashicorp.com/swingset/components/mdxinlinealert). They render as colored boxes to draw the user's attention to some type of aside. ```mdx ## Alert types ### Tip To provide general information to the user regarding the current context or relevant actions. ### Highlight To provide general or promotional information to the user prominently. ### Note To help users avoid an issue. Provide guidance and actions if possible. ### Warning To indicate critical issues that need immediate action or help users understand something critical. ## Title override prop To provide general information. ``` #### Tabs The `Tabs` component creates tabbed content of any type, but is often used for code examples given in different languages. Here's an example of how it looks from the Vagrant documentation website: ![Tabs Component](https://p176.p0.n0.cdn.getcloudapp.com/items/WnubALZ4/Screen%20Recording%202020-06-11%20at%2006.03%20PM.gif?v=1de81ea720a8cc8ade83ca64fb0b9edd) > Please refer to the [Swingset](https://react-components.vercel.app/?component=Tabs) documentation for the latest examples and API reference. It can be used as such within a markdown file: ````mdx Normal **markdown** content. ```shell-session $ command ... ``` ```shell-session $ curl ... ``` Continued normal markdown content ```` The intentionally skipped line is a limitation of the mdx parser which is being actively worked on. All tabs must have a heading, and there is no limit to the number of tabs, though it is recommended to go for a maximum of three or four. #### Enterprise Alert This component provides a standard way to call out functionality as being present only in the enterprise version of the software. It can be presented in two contexts, inline or standalone. Here's an example of standalone usage from the Consul docs website: ![Enterprise Alert Component - Standalone](https://p176.p0.n0.cdn.getcloudapp.com/items/WnubALp8/Screen%20Shot%202020-06-11%20at%206.06.03%20PM.png?v=d1505b90bdcbde6ed664831a885ea5fb) The standalone component can be used as such in markdown files: ```mdx # Page Headline Continued markdown content... ``` It can also receive custom text contents if you need to change the messaging but wish to retain the style. This will replace the text `This feature is available in all versions of Consul Enterprise.` with whatever you add. For example: ```mdx # Page Headline My custom text here, and a link! Continued markdown content... ``` It's important to note that once you are adding custom content, it must be html and can not be markdown, as demonstrated above with the link. Now let's look at inline usage, here's an example: ![Enterprise Alert Component - Inline](https://p176.p0.n0.cdn.getcloudapp.com/items/L1upYLEJ/Screen%20Shot%202020-06-11%20at%206.07.50%20PM.png?v=013ba439263de8292befbc851d31dd78) And here's how it could be used in your markdown document: ```mdx ### Some Enterprise Feature Continued markdown content... ``` It's also worth noting that this component will automatically adjust to the correct product colors depending on the context. #### Other Components Other custom components can be made available on a per-site basis, the above are the standards. If you have questions about custom components that are not documented here, or have a request for a new custom component, please reach out to @hashicorp/digital-marketing. ### Syntax Highlighting When using fenced code blocks, the recommendation is to tag the code block with a language so that it can be syntax highlighted. For example: ```` ``` // BAD: Code block with no language tag ``` ```javascript // GOOD: Code block with a language tag ``` ```` Check out the [supported languages list](https://prismjs.com/#supported-languages) for the syntax highlighter we use if you want to double check the language name. It is also worth noting specifically that if you are using a code block that is an example of a terminal command, the correct language tag is `shell-session`. For example: 🚫**BAD**: Using `shell`, `sh`, `bash`, or `plaintext` to represent a terminal command ```` ```shell $ terraform apply ``` ```` ✅**GOOD**: Using `shell-session` to represent a terminal command ```` ```shell-session $ terraform apply ``` ```` ## Editing Navigation Sidebars The structure of the sidebars are controlled by files in the [`/data` directory](data). For example, [data/docs-nav-data.json](data/docs-nav-data.json) controls the **docs** sidebar. Within the `data` folder, any file with `-nav-data` after it controls the navigation for the given section. The sidebar uses a simple recursive data structure to represent _files_ and _directories_. The sidebar is meant to reflect the structure of the docs within the filesystem while also allowing custom ordering. Let's look at an example. First, here's our example folder structure: ```text . ├── docs │   └── directory │   ├── index.mdx │   ├── file.mdx │   ├── another-file.mdx │   └── nested-directory │   ├── index.mdx │   └── nested-file.mdx ``` Here's how this folder structure could be represented as a sidebar navigation, in this example it would be the file `website/data/docs-nav-data.json`: ```json [ { "title": "Directory", "routes": [ { "title": "Overview", "path": "directory" }, { "title": "File", "path": "directory/file" }, { "title": "Another File", "path": "directory/another-file" }, { "title": "Nested Directory", "routes": [ { "title": "Overview", "path": "directory/nested-directory" }, { "title": "Nested File", "path": "directory/nested-directory/nested-file" } ] } ] } ] ``` A couple more important notes: - Within this data structure, ordering is flexible, but hierarchy is not. The structure of the sidebar must correspond to the structure of the content directory. So while you could put `file` and `another-file` in any order in the sidebar, or even leave one or both of them out, you could not decide to un-nest the `nested-directory` object without also un-nesting it in the filesystem. - The `title` property on each node in the `nav-data` tree is the human-readable name in the navigation. - The `path` property on each leaf node in the `nav-data` tree is the URL path where the `.mdx` document will be rendered, and the - Note that "index" files must be explicitly added. These will be automatically resolved, so the `path` value should be, as above, `directory` rather than `directory/index`. A common convention is to set the `title` of an "index" node to be `"Overview"`. Below we will discuss a couple of more unusual but still helpful patterns. ### Index-less Categories Sometimes you may want to include a category but not have a need for an index page for the category. This can be accomplished, but as with other branch and leaf nodes, a human-readable `title` needs to be set manually. Here's an example of how an index-less category might look: ```text . ├── docs │   └── indexless-category │   └── file.mdx ``` ```json // website/data/docs-nav-data.json [ { "title": "Indexless Category", "routes": [ { "title": "File", "path": "indexless-category/file" } ] } ] ``` ### Custom or External Links Sometimes you may have a need to include a link that is not directly to a file within the docs hierarchy. This can also be supported using a different pattern. For example: ```json [ { "name": "Directory", "routes": [ { "title": "File", "path": "directory/file" }, { "title": "Another File", "path": "directory/another-file" }, { "title": "Tao of HashiCorp", "href": "https://www.hashicorp.com/tao-of-hashicorp" } ] } ] ``` If the link provided in the `href` property is external, it will display a small icon indicating this. If it's internal, it will appear the same way as any other direct file link. ## Changing the Release Version To change the version displayed for download on the website, head over to `data/version.js` and change the number there. It's important to note that the version number must match a version that has been released and is live on `releases.hashicorp.com` -- if it does not, the website will be unable to fetch links to the binaries and will not compile. So this version number should be changed _only after a release_. ### Displaying a Prerelease If there is a prerelease of any type that should be displayed on the downloads page, this can be done by editing `pages/downloads/index.jsx`. By default, the download component might look something like this: ```jsx ``` To add a prerelease, an extra `prerelease` property can be added to the component as such: ```jsx ``` This configuration would display something like the following text on the website, emphasis added to the configurable parameters: ``` A {{ release candidate }} for {{ v1.0.0 }} is available! The release can be downloaded here. ``` You may customize the parameters in any way you'd like. To remove a prerelease from the website, simply delete the `prerelease` parameter from the above component. ## Redirects This website structures URLs based on the filesystem layout. This means that if a file is moved, removed, or a folder is re-organized, links will break. If a path change is necessary, it can be mitigated using redirects. It's important to note that redirects should only be used to cover for external links -- if you are moving a path which internal links point to, the internal links should also be adjusted to point to the correct page, rather than relying on a redirect. To add a redirect, head over to the `redirects.js` file - the format is fairly simple - there's a `source` and a `destination` - fill them both in, indicate that it's a permanent redirect or not using the `permanent` key, and that's it. Let's look at an example: ``` { source: '/foo', destination: '/bar', permanent: true } ``` This redirect rule will send all incoming links to `/foo` to `/bar`. For more details on the redirects file format, [check out the docs on vercel](https://vercel.com/docs/configuration#project/redirects). All redirects will work both locally and in production exactly the same way, so feel free to test and verify your redirects locally. In the past testing redirects has required a preview deployment -- this is no longer the case. Please note however that if you add a redirect while the local server is running, you will need to restart it in order to see the effects of the redirect. There is still one caveat though: redirects do not apply to client-side navigation. By default, all links in the navigation and docs sidebar will navigate purely on the client side, which makes navigation through the docs significantly faster, especially for those with low-end devices and/or weak internet connections. In the future, we plan to convert all internal links within docs pages to behave this way as well. This means that if there is a link on this website to a given piece of content that has changed locations in some way, we need to also _directly change existing links to the content_. This way, if a user clicks a link that navigates on the client side, or if they hit the url directly and the page renders from the server side, either one will work perfectly. Let's look at an example. Say you have a page called `/docs/foo` which needs to be moved to `/docs/nested/foo`. Additionally, this is a page that has been around for a while and we know there are links into `/docs/foo.html` left over from our previous website structure. First, we move the page, then adjust the docs sidenav, in `data/docs-navigation.js`. Find the category the page is in, and move it into the appropriate subcategory. Next, we add to `_redirects` as such. The `.html` version is covered automatically. ```js { source: '/foo', destination: '/nested/foo', permanent: true } ``` Next, we run a global search for internal links to `/foo`, and make sure to adjust them to be `/nested/foo` - this is to ensure that client-side navigation still works correctly. _Adding a redirect alone is not enough_. One more example - let's say that content is being moved to an external website. A common example is guides moving to `learn.hashicorp.com`. In this case, we take all the same steps, except that we need to make a different type of change to the `docs-navigation` file. If previously the structure looked like: ```js { category: 'docs', content: [ 'foo' ] } ``` If we no longer want the link to be in the side nav, we can simply remove it. If we do still want the link in the side nav, but pointing to an external destination, we need to slightly change the structure as such: ```js { category: 'docs', content: [ { title: 'Foo Title', href: 'https://learn.hashicorp.com//foo' } ] } ``` As the majority of items in the side nav are internal links, the structure makes it as easy as possible to represent these links. This alternate syntax is the most concise manner than an external link can be represented. External links can be used anywhere within the docs sidenav. It's also worth noting that it is possible to do glob-based redirects, for example matching `/docs/*`, and you may see this pattern in the redirects file. This type of redirect is much higher risk and the behavior is a bit more nuanced, so if you need to add a glob redirect, please reach out to the website maintainers and ask about it first. ## Browser Support We support the following browsers targeting roughly the versions specified. | ![Chrome](https://raw.githubusercontent.com/alrra/browser-logos/main/src/chrome/chrome.svg) | ![Edge](https://raw.githubusercontent.com/alrra/browser-logos/main/src/edge/edge.svg) | ![Opera](https://raw.githubusercontent.com/alrra/browser-logos/main/src/opera/opera.svg) | ![Firefox](https://raw.githubusercontent.com/alrra/browser-logos/main/src/firefox/firefox.svg) | ![Safari](https://raw.githubusercontent.com/alrra/browser-logos/main/src/safari/safari.svg) | | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | **Latest** | **Latest** | **Latest** | **Latest** | **Latest** | ## Deployment This website is hosted on Vercel and configured to automatically deploy anytime you push code to the `stable-website` branch. Any time a pull request is submitted that changes files within the `website` folder, a deployment preview will appear in the github checks which can be used to validate the way docs changes will look live. Deployments from `stable-website` will look and behave the same way as deployment previews. ================================================ FILE: website/content/docs/boxes/base.mdx ================================================ --- layout: docs page_title: Creating a Base Box description: |- There are a special category of boxes known as "base boxes." These boxes contain the bare minimum required for Vagrant to function, are generally not made by repackaging an existing Vagrant environment (hence the "base" in the "base box"). --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Creating a Base Box There are a special category of boxes known as "base boxes." These boxes contain the bare minimum required for Vagrant to function, are generally not made by repackaging an existing Vagrant environment (hence the "base" in the "base box"). For example, the Ubuntu boxes provided by the Vagrant project (such as "bionic64") are base boxes. They were created from a minimal Ubuntu install from an ISO, rather than repackaging an existing environment. Base boxes are extremely useful for having a clean slate starting point from which to build future development environments. The Vagrant project hopes in the future to be able to provide base boxes for many more operating systems. Until then, this page documents how you can create your own base box. ~> **Advanced topic!** Creating a base box can be a time consuming and tedious process, and is not recommended for new Vagrant users. If you are just getting started with Vagrant, we recommend trying to find existing base boxes to use first. ## What's in a Base Box? A base box typically consists of only a bare minimum set of software for Vagrant to function. As an example, a Linux box may contain only the following: - Package manager - SSH - SSH user so Vagrant can connect - Perhaps Chef, Puppet, etc. but not strictly required. In addition to this, each [provider](/vagrant/docs/providers/) may require additional software. For example, if you are making a base box for VirtualBox, you will want to include the VirtualBox guest additions so that shared folders work properly. But if you are making an AWS base box, this is not required. ## Creating a Base Box Creating a base box is actually provider-specific. This means that depending on if you are using VirtualBox, VMware, AWS, etc. the process for creating a base box is different. Because of this, this one document cannot be a full guide to creating a base box. This page will document some general guidelines for creating base boxes, however, and will link to provider-specific guides for creating base boxes. Provider-specific guides for creating base boxes are linked below: - [Docker Base Boxes](/vagrant/docs/providers/docker/boxes) - [Hyper-V Base Boxes](/vagrant/docs/providers/hyperv/boxes) - [VMware Base Boxes](/vagrant/docs/providers/vmware/boxes) - [VirtualBox Base Boxes](/vagrant/docs/providers/virtualbox/boxes) ### Packer and Vagrant Cloud We strongly recommend using [Packer](https://www.packer.io/) to create reproducible builds for your base boxes, as well as automating the builds. Read more about [automating Vagrant box creation with Packer](/packer/guides/packer-on-cicd/build-image-in-cicd) in the Packer documentation. ### Disk Space When creating a base box, make sure the user will have enough disk space to do interesting things, without being annoying. For example, in VirtualBox, you should create a dynamically resizing drive with a large maximum size. This causes the actual footprint of the drive to be small initially, but to dynamically grow towards the max size as disk space is needed, providing the most flexibility for the end user. If you are creating an AWS base box, do not force the AMI to allocate terabytes of EBS storage, for example, since the user can do that on their own. But you should default to mounting ephemeral drives, because they're free and provide a lot of disk space. ### Memory Like disk space, finding the right balance of the default amount of memory is important. For most providers, the user can modify the memory with the Vagrantfile, so do not use too much by default. It would be a poor user experience (and mildly shocking) if a `vagrant up` from a base box instantly required many gigabytes of RAM. Instead, choose a value such as 512MB, which is usually enough to play around and do interesting things with a Vagrant machine, but can easily be increased when needed. ### Peripherals (Audio, USB, etc.) Disable any non-necessary hardware in a base box such as audio and USB controllers. These are generally unnecessary for Vagrant usage and, again, can be easily added via the Vagrantfile in most cases. ## Default User Settings Just about every aspect of Vagrant can be modified. However, Vagrant does expect some defaults which will cause your base box to "just work" out of the box. You should create these as defaults if you intend to publicly distribute your box. If you are creating a base box for private use, you should try _not_ to follow these, as they open up your base box to security risks (known users, passwords, private keys, etc.). ### "vagrant" User By default, Vagrant expects a "vagrant" user to SSH into the machine as. This user should be setup with the [insecure keypairs](https://github.com/hashicorp/vagrant/tree/main/keys) that Vagrant uses as a default to attempt to SSH. It should belong to a group named "vagrant". Also, even though Vagrant uses key-based authentication by default, it is a general convention to set the password for the "vagrant" user to "vagrant". This lets people login as that user manually if they need to. To configure SSH access with the insecure keypair, place the [public keys](https://github.com/hashicorp/vagrant/tree/main/keys/vagrant.pub) into the `~/.ssh/authorized_keys` file for the "vagrant" user. Note that OpenSSH is very picky about file permissions. Therefore, make sure that `~/.ssh` has `0700` permissions and the authorized keys file has `0600` permissions. When Vagrant boots a box and detects the insecure keypair, it will automatically replace it with a randomly generated keypair for additional security while the box is running. ### Root Password: "vagrant" Vagrant does not actually use or expect any root password. However, having a generally well known root password makes it easier for the general public to modify the machine if needed. Publicly available base boxes usually use a root password of "vagrant" to keep things easy. ### Password-less Sudo This is **important!**. Many aspects of Vagrant expect the default SSH user to have passwordless sudo configured. This lets Vagrant configure networks, mount synced folders, install software, and more. To begin, some minimal installations of operating systems do not even include `sudo` by default. Verify that you install `sudo` in some way. After installing sudo, configure it (usually using `visudo`) to allow passwordless sudo for the "vagrant" user. This can be done with the following line at the end of the configuration file: ``` vagrant ALL=(ALL) NOPASSWD: ALL ``` Additionally, Vagrant does not use a pty or tty by default when connected via SSH. You will need to make sure there is no line that has `requiretty` in it. Remove that if it exists. This allows sudo to work properly without a tty. Note that you _can_ configure Vagrant to request a pty, which lets you keep this configuration. But Vagrant by default does not do this. ### SSH Tweaks In order to keep SSH speedy even when your machine or the Vagrant machine is not connected to the internet, set the `UseDNS` configuration to `no` in the SSH server configuration. This avoids a reverse DNS lookup on the connecting SSH client which can take many seconds. ## Windows Boxes Supported Windows guest operating systems: - Windows 7 - Windows 8 - Windows Server 2008 - Windows Server 2008 R2 - Windows Server 2012 - Windows Server 2012 R2 Windows Server 2003 and Windows XP are _not_ supported, but if you are a die hard XP fan [this](https://stackoverflow.com/a/18593425/18475) may help you. ### Base Windows Configuration - Turn off UAC - Disable complex passwords - Disable "Shutdown Tracker" - Disable "Server Manager" starting at login (for non-Core) In addition to disabling UAC in the control panel, you also must disable UAC in the registry. This may vary from Windows version to Windows version, but Windows 8/8.1 use the command below. This will allow some things like automated Puppet installs to work within Vagrant Windows base boxes. ``` reg add HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /d 0 /t REG_DWORD /f /reg:64 ``` ### Base WinRM Configuration To enable and configure WinRM you will need to set the WinRM service to auto-start and allow unencrypted basic auth (obviously this is not secure). Run the following commands from a regular Windows command prompt: ``` winrm quickconfig -q winrm set winrm/config/winrs @{MaxMemoryPerShellMB="512"} winrm set winrm/config @{MaxTimeoutms="1800000"} winrm set winrm/config/service @{AllowUnencrypted="true"} winrm set winrm/config/service/auth @{Basic="true"} sc config WinRM start= auto ``` ### Additional WinRM 1.1 Configuration These additional configuration steps are specific to Windows Server 2008 (WinRM 1.1). For Windows Server 2008 R2, Windows 7 and later versions of Windows you can ignore this section. 1. Ensure the Windows PowerShell feature is installed 2. Change the WinRM port to 5985 or upgrade to WinRM 2.0 The following commands will change the WinRM 1.1 port to what's expected by Vagrant: ``` netsh firewall add portopening TCP 5985 "Port 5985" winrm set winrm/config/listener?Address=*+Transport=HTTP @{Port="5985"} ``` ### Optional WinSSH Configuration When using the WinSSH communicator, you may run into an issue where a PowerShell command can't display a progress bar. A typical error message might look like: ``` Win32 internal error "Access is denied" 0x5 occurred while reading the console output buffer. ``` In order to prevent this, we recommend setting `$ProgressPreference = "SilentlyContinue"` in your box's PowerShell profile: ``` if (!(Test-Path -Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force } Add-Content $PROFILE '$ProgressPreference = "SilentlyContinue"' ``` ## Other Software At this point, you have all the common software you absolutely _need_ for your base box to work with Vagrant. However, there is some additional software you can install if you wish. While we plan on it in the future, Vagrant still does not install Chef or Puppet automatically when using those provisioners. Users can use a shell provisioner to do this, but if you want Chef/Puppet to just work out of the box, you will have to install them in the base box. Installing this is outside the scope of this page, but should be fairly straightforward. In addition to this, feel free to install and configure any other software you want available by default for this base box. ## Packaging the Box Packaging the box into a `box` file is provider-specific. Please refer to the provider-specific documentation for creating a base box. Some provider-specific guides are linked to towards the top of this page. ## Distributing the Box You can distribute the box file however you would like. However, if you want to support versioning, putting multiple providers at a single URL, pushing updates, analytics, and more, we recommend you add the box to [HashiCorp's Vagrant Cloud](/vagrant/vagrant-cloud). You can upload both public and private boxes to this service. ## Testing the Box To test the box, pretend you are a new user of Vagrant and give it a shot: ```shell-session $ vagrant box add --name my-box /path/to/the/new.box ... $ vagrant init my-box ... $ vagrant up ... ``` If you made a box for some other provider, be sure to specify the `--provider` option to `vagrant up`. If the up succeeded, then your box worked! ================================================ FILE: website/content/docs/boxes/box_repository.mdx ================================================ --- layout: docs page_title: Box Repository description: |- Vagrant can download boxes from a Box Repository. [Vagrantcloud](https://vagrantcloud.com/) is the HashiCorp maintained Box Repository. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Box Repository A Vagrant Box Repository provides Vagrant with some boxes and information on how to get the boxes. This can exist on a local filesystem or a service like Vagrantcloud. There are two components that make up a Box Repository: - Vagrant Boxes - These are Vagrant `.box` files. See the [box documentation](/vagrant/docs/boxes) for more information on Vagrant boxes. - Box Catalog Metadata - This is a JSON document (typically exchanged during interactions with [HashiCorp's Vagrant Cloud](/vagrant/vagrant-cloud)) that specifies the name of the box, a description, available versions, available providers, and URLs to the actual box files (next component) for each provider and version. If this catalog metadata does not exist, a box file can still be added directly, but it will not support versioning and updating. ## Box Catalog Metadata The metadata is an optional component for a box (but highly recommended) that enables [versioning](/vagrant//docs/boxes/versioning), updating, multiple providers from a single file, and more. -> **You do not need to manually make the metadata.** If you have an account with [HashiCorp's Vagrant Cloud](/vagrant/vagrant-cloud), you can create boxes there, and HashiCorp's Vagrant Cloud automatically creates the metadata for you. The format is still documented here. It is a JSON document, structured in the following way: ```json { "name": "hashicorp/bionic64", "description": "This box contains Ubuntu 18.04 LTS 64-bit.", "versions": [ { "version": "0.1.0", "providers": [ { "name": "virtualbox", "url": "http://example.com/bionic64_010_virtualbox.box", "checksum_type": "sha1", "checksum": "foo", "architecture": "amd64", "default_architecture": true } ] } ] } ``` As you can see, the JSON document can describe multiple versions of a box, multiple providers, and can add/remove providers/architectures in different versions. This JSON file can be passed directly to `vagrant box add` from the local filesystem using a file path or via a URL, and Vagrant will install the proper version of the box. In this case, the value for the `url` key in the JSON can also be a file path. If multiple providers are available, Vagrant will ask what provider you want to use. ================================================ FILE: website/content/docs/boxes/format.mdx ================================================ --- layout: docs page_title: Box File Format description: |- The box file format for Vagrant supports a number different providers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Box File Format A Vagrant `.box` file is a [tarball]() (`tar`, `tar.gz`, `zip`) that contains all the information for a provider to launch a Vagrant machine. There are four different components that make up a box: - VM artifacts (required) - This is the VM image and other artifacts in the format accepted by the provider the box is intended for. For example, a box targeting the VirtualBox provider might have a `.ofv` file and some `.vmdk` files. - metadata.json (required) - Contains a map with information about the box. Most importantly the target provider. - info.json - This is a JSON document that can provide additional information about the box that displays when a user runs `vagrant box list -i`. More information is provided [here](/vagrant/docs/boxes/info). - Vagrantfile - The Vagrantfile embedded in the Vagrant box will provide some defaults for users of the box. For more information on how Vagrant merges Vagrantfiles including ones sourced within the box file see the [Vagrantfile docs](/vagrant/vagrant-cloud) So, if you extract a box and look at it's contents it will look like: ``` # contents of the hashicorp/bionic64 box # ref: https://app.vagrantup.com/hashicorp/boxes/bionic64 $ ls hashicorp_bionic_box Vagrantfile metadata.json box.ovf ubuntu-18.04-amd64-disk001.vmdk ``` ## Box metadata.json Within the archive, Vagrant does expect a single file: `metadata.json`. There is only one `metadata.json` per box file. `metadata.json` must contain at least the "provider" key with the provider the box is for. Vagrant uses this to verify the provider of the box. For example, if your box was for VirtualBox, the `metadata.json` would look like this: ```json { "provider": "virtualbox" } ``` If there is no `metadata.json` file or the file does not contain valid JSON with at least a "provider" key, then Vagrant will error when adding the box, because it cannot verify the provider. Other keys/values may be added to the metadata without issue. The value of the metadata file is passed opaquely into Vagrant and plugins can make use of it. Values currently used by Vagrant core: * `provider` - (string) Provider for the box * `architecture` - (string) Architecture of the box ================================================ FILE: website/content/docs/boxes/index.mdx ================================================ --- layout: docs page_title: Vagrant Boxes description: |- Boxes are the package format for Vagrant environments. A box can be used by anyone on any platform that Vagrant supports to bring up an identical working environment. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Vagrant boxes Boxes are the package format for Vagrant environments. You specify a box environment and operating configurations in your [Vagrantfile](/vagrant/docs/vagrantfile). You can use a box on any [supported platform](/vagrant/downloads) to bring up identical working environments. To enable teams to use and manage the same boxes, [versions are supported](/vagrant/docs/boxes/versioning). ~> **Note**: Boxes require a provider, a virtualization product, to operate. Before you can use a box, ensure that you have properly installed a supported [provider](/vagrant/docs/providers). The quickest way to get started is to select a pre-defined box environment from the [publicly available catalog on Vagrant Cloud](https://vagrantcloud.com/boxes/search). You can also add and share your own customized boxes on Vagrant Cloud. The `vagrant box` CLI utility provides all the functionality for box management. You can read the documentation on the [vagrant box](/vagrant/docs/cli/box) command for more information. ## Discover boxes To find boxes, explore the [public Vagrant box catalog](https://vagrantcloud.com/boxes/search) for a box that matches your use case. The catalog contains most major operating systems as bases, as well as specialized boxes to get you started with common configurations such as LAMP stacks, Ruby, and Python. The boxes on the public catalog work with many different [providers](/vagrant/docs/providers/). The list of supported providers is located in the box descriptions. ### Add a box You can add a box from the public catalog at any time. The box's description includes instructions on how to add it to an existing Vagrantfile or initiate it as a new environment on the command-line. A common misconception is that a namespace like "ubuntu" represents the official space for Ubuntu boxes. This is untrue. Namespaces on Vagrant Cloud behave very similarly to namespaces on GitHub. Just as GitHub's support team is unable to assist with issues in someone's repository, HashiCorp's support team is unable to assist with third-party published boxes. ## Official boxes There are only two officially-recommended box sets. HashiCorp publishes a basic Ubuntu 18.04 64-bit box that is available for minimal use cases. It is highly optimized, small in size, and includes support for VirtualBox, Hyper-V, and VMware. To get started, use the `init` command to initialize your environment. ```shell-session $ vagrant init hashicorp/bionic64 ``` If you have an existing Vagrantfile, add `hashicorp/bionic64`. ```ruby Vagrant.configure("2") do |config| config.vm.box = "hashicorp/bionic64" end ``` For other base operating system environments, we recommend the [Bento boxes](https://vagrantcloud.com/bento). The Bento boxes are [open source](https://github.com/chef/bento) and built for a number of providers including VMware, VirtualBox, and Parallels. There are a variety of operating systems and versions available. Special thanks to the Bento project for providing a solid base template for the `hashicorp/bionic64` box. It is often a point of confusion but Canonical, the company that makes the Ubuntu operating system, publishes boxes under the "ubuntu" namespace on Vagrant Cloud. These boxes only support VirtualBox. ## Create a box If you are unable to find a box that meets your specific use case, you can create one. We recommend that you first create a [base box](/vagrant/docs/boxes/base) to have a clean slate to start from when you build future development environments. Learn more about [box formats](/vagrant/docs/boxes/format) to get started. ================================================ FILE: website/content/docs/boxes/info.mdx ================================================ --- layout: docs page_title: Box Info Format description: |- A box can provide additional information to the user by supplying an info.json file within the box. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Additional Box Information When creating a Vagrant box, you can supply additional information that might be relevant to the user when running `vagrant box list -i`. For example, you could package your box to include information about the author of the box and a website for users to learn more: ``` brian@localghost % vagrant box list -i hashicorp/bionic64 (virtualbox, 1.0.0) - author: brian - homepage: https://www.vagrantup.com ``` ## Box Info To accomplish this, you simply need to include a file named `info.json` when creating a [base box](/vagrant/docs/boxes/base) which is a JSON document containing any and all relevant information that will be displayed to the user when the `-i` option is used with `vagrant box list`. ```json { "author": "brian", "homepage": "https://example.com" } ``` There are no special keys or values in `info.json`, and Vagrant will print each key and value on its own line. The [Box File Format](/vagrant/docs/boxes/format) provides more information about what else goes into a Vagrant box. ================================================ FILE: website/content/docs/boxes/versioning.mdx ================================================ --- layout: docs page_title: Box Versioning description: |- Since Vagrant 1.5, boxes support versioning. This allows the people who make boxes to push updates to the box, and the people who use the box have a simple workflow for checking for updates, updating their boxes, and seeing what has changed. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Box Versioning Since Vagrant 1.5, boxes support versioning. This allows the people who make boxes to push updates to the box, and the people who use the box have a simple workflow for checking for updates, updating their boxes, and seeing what has changed. If you are just getting started with Vagrant, box versioning is not too important, and we recommend learning about some other topics first. But if you are using Vagrant on a team or plan on creating your own boxes, versioning is very important. Luckily, having versioning built right in to Vagrant makes it easy to use and fit nicely into the Vagrant workflow. This page will cover how to use versioned boxes. It does _not_ cover how to update your own custom boxes with versions. That is covered in [creating a base box](/vagrant/docs/boxes/base). ## Viewing Versions and Updating `vagrant box list` only shows _installed_ versions of boxes. If you want to see all available versions of a box, you will have to find the box on [HashiCorp's Vagrant Cloud](/vagrant/vagrant-cloud). An easy way to find a box is to use the url `https://vagrantcloud.com/$USER/$BOX`. For example, for the `hashicorp/bionic64` box, you can find information about it at `https://vagrantcloud.com/hashicorp/bionic64`. You can check if the box you are using is outdated with `vagrant box outdated`. This can check if the box in your current Vagrant environment is outdated as well as any other box installed on the system. Finally, you can update boxes with `vagrant box update`. This will download and install the new box. This _will not_ magically update running Vagrant environments. If a Vagrant environment is already running, you will have to destroy and recreate it to acquire the new updates in the box. The update command just downloads these updates locally. ## Version Constraints You can constrain a Vagrant environment to a specific version or versions of a box using the [Vagrantfile](/vagrant/docs/vagrantfile/) by specifying the `config.vm.box_version` option. If this option is not specified, the latest version is always used. This is equivalent to specifying a constraint of ">= 0". The box version configuration can be a specific version or a constraint of versions. Constraints can be any combination of the following: `= X`, `> X`, `< X`, `>= X`, `<= X`, `~> X`. You can combine multiple constraints by separating them with commas. All the constraints should be self explanatory except perhaps for `~>`, known as the "pessimistic constraint". Examples explain it best: `~> 1.0` is equivalent to `>= 1.0, < 2.0`. And `~> 1.1.5` is equivalent to `>= 1.1.5, < 1.2.0`. You can choose to handle versions however you see fit. However, many boxes in the public catalog follow [semantic versioning](http://semver.org/). Basically, only the first number (the "major version") breaks backwards compatibility. In terms of Vagrant boxes, this means that any software that runs in version "1.1.5" of a box should work in "1.2" and "1.4.5" and so on, but "2.0" might introduce big changes that break your software. By following this convention, the best constraint is `~> 1.0` because you know it is safe no matter what version is in that range. Please note that, while the semantic versioning specification allows for more than three points and pre-release or beta versions, Vagrant boxes must be of the format `X.Y.Z` where `X`, `Y`, and `Z` are all positive integers. ## Automatic Update Checking Using the [Vagrantfile](/vagrant/docs/vagrantfile/), you can also configure Vagrant to automatically check for updates during any `vagrant up`. This is enabled by default, but can easily be disabled with `config.vm.box_check_update = false` in your Vagrantfile. When this is enabled, Vagrant will check for updates on every `vagrant up`, not just when the machine is being created from scratch, but also when it is resuming, starting after being halted, etc. If an update is found, Vagrant will output a warning to the user letting them know an update is available. That user can choose to ignore the warning for now, or can update the box by running `vagrant box update`. Vagrant can not and does not automatically download the updated box and update the machine because boxes can be relatively large and updating the machine requires destroying it and recreating it, which can cause important data to be lost. Therefore, this process is manual to the extent that the user has to manually enter a command to do it. ## Pruning Old Versions Vagrant does not automatically prune old versions because it does not know if they might be in use by other Vagrant environments. Because boxes can be large, you may want to actively prune them once in a while using `vagrant box remove`. You can see all the boxes that are installed using `vagrant box list`. Another option is to use `vagrant box prune` command to remove all installed boxes that are outdated and not currently in use. ================================================ FILE: website/content/docs/cli/aliases.mdx ================================================ --- layout: docs page_title: Aliases - Command-Line Interface description: |- Custom Vagrant commands can be defined using aliases, allowing for a simpler, easier, and more familiar command line interface. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Aliases Inspired in part by Git's own [alias functionality](https://git-scm.com/book/en/v2/Git-Basics-Git-Aliases), aliases make your Vagrant experience simpler, easier, and more familiar by allowing you to create your own custom Vagrant commands. Aliases can be defined within `VAGRANT_HOME/aliases` file, or in a custom file defined using the `VAGRANT_ALIAS_FILE` environment variable, in the following format: ```shell # basic command-level aliases start = up stop = halt # advanced command-line aliases eradicate = !vagrant destroy && rm -rf .vagrant ``` In a nutshell, aliases are defined using a standard `key = value` format, where the `key` is the new Vagrant command, and the `value` is the aliased command. Using this format, there are two types of aliases that can be defined: internal and external aliases. ## Internal Aliases Internal command aliases call the CLI class directly, allowing you to alias one Vagrant command to another Vagrant command. This technique can be very useful for creating commands that you think _should_ exist. For example, if `vagrant stop` feels more intuitive than `vagrant halt`, the following alias definitions would make that change possible: ```shell stop = halt ``` This makes the following commands equivalent: ```shell vagrant stop vagrant halt ``` ## External Aliases While internal aliases can be used to define more intuitive Vagrant commands, external command aliases are used to define Vagrant commands with brand new functionality. These aliases are prefixed with the `!` character, which indicates to the interpreter that the alias should be executed as a shell command. For example, let's say that you want to be able to view the processor and memory utilization of the active project's virtual machine. To do this, you could define a `vagrant metrics` command that returns the required information in an easy-to-read format, like so: ```shell metrics = !ps aux | grep "[V]BoxHeadless" | grep $(cat .vagrant/machines/default/virtualbox/id) | awk '{ printf("CPU: %.02f%%, Memory: %.02f%%", $3, $4) }' ``` The above alias, from within the context of an active Vagrant project, would print the CPU and memory utilization directly to the console: ```text CPU: 4.20%, Memory: 11.00% ``` ================================================ FILE: website/content/docs/cli/box.mdx ================================================ --- layout: docs page_title: vagrant box - Command-Line Interface description: |- The "vagrant box" command is used to manage "vagrant box add", "vagrant box remove", and other box-related commands such as "outdated", "list", and "update". --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Box **Command: `vagrant box`** This is the command used to manage (add, remove, etc.) [boxes](/vagrant/docs/boxes). The main functionality of this command is exposed via even more subcommands: - [`add`](#box-add) - [`list`](#box-list) - [`outdated`](#box-outdated) - [`prune`](#box-prune) - [`remove`](#box-remove) - [`repackage`](#box-repackage) - [`update`](#box-update) # Box Add **Command: `vagrant box add ADDRESS`** This adds a box with the given address to Vagrant. The address can be one of three things: - A shorthand name from the [public catalog of available Vagrant images](https://vagrantcloud.com/boxes/search), such as "hashicorp/bionic64". - File path or HTTP URL to a box in a [catalog](https://vagrantcloud.com/boxes/search). For HTTP, basic authentication is supported and `http_proxy` environmental variables are respected. HTTPS is also supported. - URL directly a box file. In this case, you must specify a `--name` flag (see below) and versioning/updates will not work. If an error occurs during the download or the download is interrupted with a Ctrl-C, then Vagrant will attempt to resume the download the next time it is requested. Vagrant will only attempt to resume a download for 24 hours after the initial download. ## Options - `--box-version VALUE` - The version of the box you want to add. By default, the latest version will be added. The value of this can be an exact version number such as "1.2.3" or it can be a set of version constraints. A version constraint looks like ">= 1.0, < 2.0". - `--cacert CERTFILE` - The certificate for the CA used to verify the peer. This should be used if the remote end does not use a standard root CA. - `--capath CERTDIR` - The certificate directory for the CA used to verify the peer. This should be used if the remote end does not use a standard root CA. - `--cert CERTFILE` - A client certificate to use when downloading the box, if necessary. - `--clean` - If given, Vagrant will remove any old temporary files from prior downloads of the same URL. This is useful if you do not want Vagrant to resume a download from a previous point, perhaps because the contents changed. - `--force` - When present, the box will be downloaded and overwrite any existing box with this name. - `--insecure` - When present, SSL certificates will not be verified if the URL is an HTTPS URL. - `--provider PROVIDER` - If given, Vagrant will verify the box you are adding is for the given provider. By default, Vagrant automatically detects the proper provider to use. ## Options for direct box files The options below only apply if you are adding a box file directly (when you are not using a catalog). - `--checksum VALUE` - A checksum for the box that is downloaded. If specified, Vagrant will compare this checksum to what is actually downloaded and will error if the checksums do not match. This is highly recommended since box files are so large. If this is specified, `--checksum-type` must also be specified. If you are downloading from a catalog, the checksum is included within the catalog entry. - `--checksum-type TYPE` - The type of checksum that `--checksum` is if it is specified. Supported values are currently "md5", "sha1", "sha256", "sha384", and "sha512". - `--name VALUE` - Logical name for the box. This is the value that you would put into `config.vm.box` in your Vagrantfile. When adding a box from a catalog, the name is included in the catalog entry and does not have to be specified. ~> **Checksums for versioned boxes or boxes from HashiCorp's Vagrant Cloud:** For boxes from HashiCorp's Vagrant Cloud, the checksums are embedded in the metadata of the box. The metadata itself is served over TLS and its format is validated. # Box List **Command: `vagrant box list`** This command lists all the boxes that are installed into Vagrant. # Box Outdated **Command: `vagrant box outdated`** This command tells you whether or not the box you are using in your current Vagrant environment is outdated. If the `--global` flag is present, every installed box will be checked for updates. This will show the latest version available for the specific provider type, which may be different than the absolute latest version available. Checking for updates involves refreshing the metadata associated with a box. This generally requires an internet connection. By default, if Vagrant has recently checked for a box that's out of date, it will cache that answer and not look up another update for one hour. This cached value can be ignored if the `--force` flag is used. ## Options - `--force` - Check for updates for all installed boxes and ignore cache interval. - `--global` - Check for updates for all installed boxes, not just the boxes for the current Vagrant environment. # Box Prune **Command: `vagrant box prune`** This command removes old versions of installed boxes. If the box is currently in use vagrant will ask for confirmation. ## Options - `--provider PROVIDER` - The specific provider type for the boxes to destroy. - `--dry-run` - Only print the boxes that would be removed. - `--name NAME` - The specific box name to check for outdated versions. - `--force` - Destroy without confirmation even when box is in use. - `--keep-active-boxes` - When combined with `--force`, will keep boxes still actively in use. # Box Remove **Command: `vagrant box remove NAME`** 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 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. - `--provider VALUE` - The provider-specific box to remove with the given name. This is only required if a box is backed by multiple providers. If there is only a single provider, Vagrant will default to removing it. # Box Repackage **Command: `vagrant box repackage NAME PROVIDER VERSION`** This command repackages the given box and puts it in the current directory so you can redistribute it. The name, provider, and version of the box can be retrieved using `vagrant box list`. When you add a box, Vagrant unpacks it and stores it internally. The original `*.box` file is not preserved. This command is useful for reclaiming a `*.box` file from an installed Vagrant box. # Box Update **Command: `vagrant box update`** This command updates the box for the current Vagrant environment if there are updates available. The command can also update a specific box (outside of an active Vagrant environment), by specifying the `--box` flag. -> Note that updating the box will not update an already-running Vagrant machine. To reflect the changes in the box, you will have to destroy and bring back up the Vagrant machine. If you just want to check if there are updates available, use the `vagrant box outdated` command. ## Options - `--box VALUE` - Name of a specific box to update. If this flag is not specified, Vagrant will update the boxes for the active Vagrant environment. - `--provider VALUE` - When `--box` is present, this controls what provider-specific box to update. This is not required unless the box has multiple providers. Without the `--box` flag, this has no effect. ================================================ FILE: website/content/docs/cli/cloud.mdx ================================================ --- layout: docs page_title: vagrant cloud - Command-Line Interface description: |- The "vagrant cloud" command can be used for taking actions against Vagrant Cloud like searching or uploading a Vagrant Box --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Cloud **Command: `vagrant cloud`** This is the command used to manage anything related to [Vagrant Cloud](https://vagrantcloud.com). The main functionality of this command is exposed via subcommands: - [`auth`](#cloud-auth) - [`box`](#cloud-box) - [`provider`](#cloud-provider) - [`publish`](#cloud-publish) - [`search`](#cloud-search) - [`version`](#cloud-version) # Cloud Auth **Command: `vagrant cloud auth`** The `cloud auth` command is for handling all things related to authorization with Vagrant Cloud. - [`login`](#cloud-auth-login) - [`logout`](#cloud-auth-logout) - [`whoami`](#cloud-auth-whoami) ## Cloud Auth Login **Command: `vagrant cloud auth login`** The login command is used to authenticate with [HashiCorp's Vagrant Cloud](/vagrant/vagrant-cloud) server. Logging in is only necessary if you are accessing protected boxes. **Logging in is not a requirement to use Vagrant.** The vast majority of Vagrant does _not_ require a login. Only certain features such as protected boxes. The reference of available command-line flags to this command is available below. ### Options - `--check` - This will check if you are logged in. In addition to outputting whether you are logged in or not, the command exit status will be 0 if you are logged in, or 1 if you are not. - `--logout` - This will log you out if you are logged in. If you are already logged out, this command will do nothing. It is not an error to call this command if you are already logged out. - `--token` - This will set the Vagrant Cloud login token manually to the provided string. It is assumed this token is a valid Vagrant Cloud access token. ### Examples Securely authenticate to Vagrant Cloud using a username and password: ```shell-session $ vagrant cloud auth login # ... Vagrant Cloud username: Vagrant Cloud password: ``` Check if the current user is authenticated: ```shell-session $ vagrant cloud auth login --check You are already logged in. ``` Securely authenticate with Vagrant Cloud using a token: ```shell-session $ vagrant cloud auth login --token ABCD1234 The token was successfully saved. ``` ## Cloud Auth Logout **Command: `vagrant cloud auth logout`** This will log you out if you are logged in. If you are already logged out, this command will do nothing. It is not an error to call this command if you are already logged out. ## Cloud Auth Whoami **Command: `vagrant cloud auth whoami [TOKEN]`** This command will validate your Vagrant Cloud token and will print the user who it belongs to. If a token is passed in, it will attempt to validate it instead of the token stored stored on disk. # Cloud Box **Command: `vagrant cloud box`** The `cloud box` command is used to manage life cycle operations for all `box` entities on Vagrant Cloud. - [`create`](#cloud-box-create) - [`delete`](#cloud-box-delete) - [`show`](#cloud-box-show) - [`update`](#cloud-box-update) ## Cloud Box Create **Command: `vagrant cloud box create ORGANIZATION/BOX-NAME`** The box create command is used to create a new box entry on Vagrant Cloud. ### Options - `--description DESCRIPTION` - A full description of the box. Can be formatted with Markdown. - `--short-description DESCRIPTION` - A short summary of the box. - `--private` - Will make the new box private (Public by default) ## Cloud Box Delete **Command: `vagrant cloud box delete ORGANIZATION/BOX-NAME`** The box delete command will _permanently_ delete the given box entry on Vagrant Cloud. Before making the request, it will ask if you are sure you want to delete the box. ## Cloud Box Show **Command: `vagrant cloud box show ORGANIZATION/BOX-NAME`** The box show command will display information about the latest version for the given Vagrant box. ## Cloud Box Update **Command: `vagrant cloud box update ORGANIZATION/BOX-NAME`** The box update command will update an already created box on Vagrant Cloud with the given options. ### Options - `--description DESCRIPTION` - A full description of the box. Can be formatted with Markdown. - `--short-description DESCRIPTION` - A short summary of the box. - `--private` - Will make the new box private (Public by default) # Cloud Provider **Command: `vagrant cloud provider`** The `cloud provider` command is used to manage the life cycle operations for all `provider` entities on Vagrant Cloud. - [`create`](#cloud-provider-create) - [`delete`](#cloud-provider-delete) - [`update`](#cloud-provider-update) - [`upload`](#cloud-provider-upload) ## Cloud Provider Create **Command: `vagrant cloud provider create ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION [URL]`** The provider create command is used to create a new provider entry on Vagrant Cloud. The `url` argument is expected to be a remote URL that Vagrant Cloud can use to download the provider. If no `url` is specified, the provider entry can be updated later with a url or the [upload](#cloud-provider-upload) command can be used to upload a Vagrant [box file](/vagrant/docs/boxes). ## Cloud Provider Delete **Command: `vagrant cloud provider delete ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION`** The provider delete command is used to delete a provider entry on Vagrant Cloud. Before making the request, it will ask if you are sure you want to delete the provider. ## Cloud Provider Update **Command: `vagrant cloud provider update ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION [URL]`** The provider update command will update an already created provider for a box on Vagrant Cloud with the given options. ## Cloud Provider Upload **Command: `vagrant cloud provider upload ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION BOX-FILE`** The provider upload command will upload a Vagrant [box file](/vagrant/docs/boxes) to Vagrant Cloud for the specified version and provider. # Cloud Publish **Command: `vagrant cloud publish ORGANIZATION/BOX-NAME VERSION PROVIDER-NAME [PROVIDER-FILE]`** The publish command is a complete solution for creating and updating a Vagrant box on Vagrant Cloud. Instead of having to create each attribute of a Vagrant box with separate commands, the publish command instead asks you to provide all the information required before creating or updating a new box. ## Options - `--box-version VERSION` - Version to create for the box - `--description DESCRIPTION` - A full description of the box. Can be formatted with Markdown. - `--force` - Disables confirmation when creating or updating a box. - `--short-description DESCRIPTION` - A short summary of the box. - `--private` - Will make the new box private (Public by default) - `--release` - Automatically releases the box after creation (Unreleased by default) - `--url` - Valid remote URL to download the box file - `--version-description DESCRIPTION` - Description of the version that will be created. ## Examples Creating a new box on Vagrant Cloud: ```shell-session $ vagrant cloud publish briancain/supertest 1.0.0 virtualbox boxes/my/virtualbox.box -d "A really cool box to download and use" --version-description "A cool version" --release --short-description "Download me!" You are about to create a box on Vagrant Cloud with the following options: briancain/supertest (1.0.0) for virtualbox Automatic Release: true Box Description: A really cool box to download and use Box Short Description: Download me! Version Description: A cool version Do you wish to continue? [y/N] y Creating a box entry... Creating a version entry... Creating a provider entry... Uploading provider with file /Users/vagrant/boxes/my/virtualbox.box Releasing box... Complete! Published briancain/supertest tag: briancain/supertest username: briancain name: supertest private: false downloads: 0 created_at: 2018-07-25T17:53:04.340Z updated_at: 2018-07-25T18:01:10.665Z short_description: Download me! description_markdown: A really cool box to download and use current_version: 1.0.0 providers: virtualbox ``` # Cloud Search **Command: `vagrant cloud search QUERY`** The cloud search command will take a query and search Vagrant Cloud for any matching Vagrant boxes. Various filters can be applied to the results. ## Options - `--json` - Format search results in JSON. - `--page PAGE` - The page to display. Defaults to the first page of results. - `--short` - Shows a simple list of box names for the results. - `--order ORDER` - Order to display results. Can either be `desc` or `asc`. Defaults to `desc`. - `--limit LIMIT` - Max number of search results to display. Defaults to 25. - `--provider PROVIDER` - Filter search results to a single provider. - `--sort-by SORT` - The field to sort results on. Can be `created`, `downloads` , or `updated`. Defaults to `downloads`. ## Examples If you are looking for a HashiCorp box: ```text vagrant cloud search hashicorp --limit 5 | NAME | VERSION | DOWNLOADS | PROVIDERS | +-------------------------+---------+-----------+---------------------------------+ | hashicorp/precise64 | 1.1.0 | 6,675,725 | virtualbox,vmware_fusion,hyperv | | hashicorp/precise32 | 1.0.0 | 2,261,377 | virtualbox | | hashicorp/boot2docker | 1.7.8 | 59,284 | vmware_desktop,virtualbox | | hashicorp/connect-vm | 0.1.0 | 6,912 | vmware_desktop,virtualbox | | hashicorp/vagrant-share | 0.1.0 | 3,488 | vmware_desktop,virtualbox | +-------------------------+---------+-----------+---------------------------------+ ``` # Cloud Version **Command: `vagrant cloud version`** The `cloud version` command is used to manage life cycle operations for all `version` entities for a box on Vagrant Cloud. - [`create`](#cloud-version-create) - [`delete`](#cloud-version-delete) - [`release`](#cloud-version-release) - [`revoke`](#cloud-version-revoke) - [`update`](#cloud-version-update) ## Cloud Version Create **Command: `vagrant cloud version create ORGANIZATION/BOX-NAME VERSION`** The cloud create command creates a version entry for a box on Vagrant Cloud. ### Options - `--description DESCRIPTION` - Description of the version that will be created. ## Cloud Version Delete **Command: `vagrant cloud version delete ORGANIZATION/BOX-NAME VERSION`** The cloud delete command deletes a version entry for a box on Vagrant Cloud. Before making the request, it will ask if you are sure you want to delete the version. ## Cloud Version Release **Command: `vagrant cloud version release ORGANIZATION/BOX-NAME VERSION`** The cloud release command releases a version entry for a box on Vagrant Cloud if it already exists. Before making the request, it will ask if you are sure you want to release the version. ## Cloud Version Revoke **Command: `vagrant cloud version revoke ORGANIZATION/BOX-NAME VERSION`** The cloud revoke command revokes a version entry for a box on Vagrant Cloud if it already exists. Before making the request, it will ask if you are sure you want to revoke the version. ## Cloud Version Update **Command: `vagrant cloud version update ORGANIZATION/BOX-NAME VERSION`** ### Options - `--description DESCRIPTION` - Description of the version that will be created. ================================================ FILE: website/content/docs/cli/connect.mdx ================================================ --- layout: docs page_title: vagrant connect - Command-Line Interface description: |- The "vagrant connect" command compliments the "vagrant share" command to allow a user to remotely connect to your Vagrant environment. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Connect **Command: `vagrant connect NAME`** The connect command complements the [share command](/vagrant/docs/cli/share) by enabling access to shared environments. You can learn about all the details of Vagrant Share in the [Vagrant Share section](/vagrant/docs/share/). The reference of available command-line flags to this command is available below. ## Options - `--disable-static-ip` - The connect command will not spin up a small virtual machine to create a static IP you can access. When this flag is set, the only way to access the connection is to use the SOCKS proxy address outputted. - `--static-ip IP` - Tells connect what static IP address to use for the virtual machine. By default, Vagrant connect will use an IP address that looks available in the 172.16.0.0/16 space. - `--ssh` - Connects via SSH to an environment shared with `vagrant share --ssh`. ================================================ FILE: website/content/docs/cli/destroy.mdx ================================================ --- layout: docs page_title: vagrant destroy - Command-Line Interface description: |- The "vagrant destroy" command is used to stop the running virtual machine and terminate use of all resources that were in use by that machine. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Destroy **Command: `vagrant destroy [name|id]`** This command stops the running machine Vagrant is managing and destroys all resources that were created during the machine creation process. After running this command, your computer should be left at a clean state, as if you never created the guest machine in the first place. For Linux-based guests, Vagrant uses the `shutdown` command to gracefully terminate the machine. Due to the varying nature of operating systems, the `shutdown` command may exist at many different locations in the guest's `$PATH`. It is the guest machine's responsibility to properly populate the `$PATH` with directory containing the `shutdown` command. ## Options - `-f` or `--force` - Do not ask for confirmation before destroying. - `--[no-]parallel` - Destroys multiple machines in parallel if the provider supports it. Please consult the provider documentation to see if this feature is supported. - `-g` or `--graceful` - Shuts down the machine gracefully. -> The `destroy` command does not remove a box that may have been installed on your computer during `vagrant up`. Thus, even if you run `vagrant destroy`, the box installed in the system will still be present on the hard drive. To return your computer to the state as it was before `vagrant up` command, you need to use `vagrant box remove`. For more information, read about the [`vagrant box remove`](/vagrant/docs/cli/box) command. ================================================ FILE: website/content/docs/cli/global-status.mdx ================================================ --- layout: docs page_title: vagrant global-status - Command-Line Interface description: |- The "vagrant global-status" command is used to determine the state of all active Vagrant environments on the system for the currently logged in user. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Global Status **Command: `vagrant global-status`** This command will tell you the state of all active Vagrant environments on the system for the currently logged in user. ~> **This command does not actively verify the state of machines**, and is instead based on a cache. Because of this, it is possible to see stale results (machines say they're running but they're not). For example, if you restart your computer, Vagrant would not know. To prune the invalid entries, run global status with the `--prune` flag. The IDs in the output that look like `a1b2c3` can be used to control the Vagrant machine from anywhere on the system. Any Vagrant command that takes a target machine (such as `up`, `halt`, `destroy`) can be used with this ID to control it. For example: `vagrant destroy a1b2c3`. ## Options - `--prune` - Prunes invalid entries from the list. This is much more time consuming than simply listing the entries. ## Environment Not Showing Up If your environment is not showing up, you may have to do a `vagrant destroy` followed by a `vagrant up`. If you just upgraded from a previous version of Vagrant, existing environments will not show up in global-status until they are destroyed and recreated. ================================================ FILE: website/content/docs/cli/halt.mdx ================================================ --- layout: docs page_title: vagrant halt - Command-Line Interface description: |- The "vagrant halt" command is used to shut down the virtual machine that Vagrant is currently managing. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Halt **Command: `vagrant halt [name|id]`** This command shuts down the running machine Vagrant is managing. Vagrant will first attempt to gracefully shut down the machine by running the guest OS shutdown mechanism. If this fails, or if the `--force` flag is specified, Vagrant will effectively just shut off power to the machine. For Linux-based guests, Vagrant will: 1. Attempt to detect and use Systemd to execute `systemctl poweroff`; but otherwise, 2. Fallback to using the `shutdown` command to gracefully terminate the machine. Due to the varying nature of operating systems, these executables may exist at many different locations in the guest's `$PATH`. It is the guest machine's responsibility to properly populate the `$PATH` with directory containing the `shutdown` command. ## Options - `-f` or `--force` - Do not attempt to gracefully shut down the machine. This effectively pulls the power on the guest machine. ================================================ FILE: website/content/docs/cli/index.mdx ================================================ --- layout: docs page_title: Command-Line Interface description: Almost all interaction with Vagrant is done via the command-line interface. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Command-Line Interface Almost all interaction with Vagrant is done through the command-line interface. The interface is available using the `vagrant` command, and comes installed with Vagrant automatically. The `vagrant` command in turn has many subcommands, such as `vagrant up`, `vagrant destroy`, etc. If you run `vagrant` by itself, help will be displayed showing all available subcommands. In addition to this, you can run any Vagrant command with the `-h` flag to output help about that specific command. For example, try running `vagrant init -h`. The help will output a one sentence synopsis of what the command does as well as a list of all the flags the command accepts. In depth documentation and use cases of various Vagrant commands is available by reading the appropriate sub-section available in the left navigational area of this site. You may also wish to consult the [documentation](/vagrant/docs/other/environmental-variables) regarding the environmental variables that can be used to configure and control Vagrant in a global way. ## Autocompletion Vagrant provides the ability to autocomplete commands. Currently, the `bash` and `zsh` shells are supported. These can be enabled by running `vagrant autocomplete install --bash --zsh`. ================================================ FILE: website/content/docs/cli/init.mdx ================================================ --- layout: docs page_title: vagrant init - Command-Line Interface description: |- The "vagrant init" command is used to initialize the current directory to be a Vagrant environment by creating an initial Vagrantfile. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Init **Command: `vagrant init [name [url]]`** This initializes the current directory to be a Vagrant environment by creating an initial [Vagrantfile](/vagrant/docs/vagrantfile/) if one does not already exist. If a first argument is given, it will prepopulate the `config.vm.box` setting in the created Vagrantfile. If a second argument is given, it will prepopulate the `config.vm.box_url` setting in the created Vagrantfile. ## Options - `--box-version` - (Optional) The box version or box version constraint to add to the `Vagrantfile`. - `--force` - If specified, this command will overwrite any existing `Vagrantfile`. - `--minimal` - If specified, a minimal Vagrantfile will be created. This Vagrantfile does not contain the instructional comments that the normal Vagrantfile contains. - `--output FILE` - This will output the Vagrantfile to the given file. If this is "-", the Vagrantfile will be sent to stdout. - `--template FILE` - Provide a custom ERB template for generating the Vagrantfile. ## Examples Create a base Vagrantfile: ```shell-session $ vagrant init hashicorp/bionic64 ``` Create a minimal Vagrantfile (no comments or helpers): ```shell-session $ vagrant init -m hashicorp/bionic64 ``` Create a new Vagrantfile, overwriting the one at the current path: ```shell-session $ vagrant init -f hashicorp/bionic64 ``` Create a Vagrantfile with the specific box, from the specific box URL: ```shell-session $ vagrant init my-company-box https://example.com/my-company.box ``` Create a Vagrantfile, locking the box to a version constraint: ```shell-session $ vagrant init --box-version '> 0.1.5' hashicorp/bionic64 ``` ================================================ FILE: website/content/docs/cli/login.mdx ================================================ --- layout: docs page_title: vagrant login - Command-Line Interface description: |- The "vagrant login" command is used to authenticate Vagrant with HashiCorp's Vagrant Cloud service to use features like private boxes and "vagrant push". --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Login **Command: `vagrant login`** The login command is used to authenticate with the [HashiCorp's Vagrant Cloud](/vagrant/vagrant-cloud) server. Logging in is only necessary if you are accessing protected boxes or using [Vagrant Share](/vagrant/docs/share/). **Logging in is not a requirement to use Vagrant.** The vast majority of Vagrant does _not_ require a login. Only certain features such as protected boxes or [Vagrant Share](/vagrant/docs/share/) require a login. The reference of available command-line flags to this command is available below. ## Options - `--check` - This will check if you are logged in. In addition to outputting whether you are logged in or not, the command will have exit status 0 if you are logged in, and exit status 1 if you are not. - `--logout` - This will log you out if you are logged in. If you are already logged out, this command will do nothing. It is not an error to call this command if you are already logged out. - `--token` - This will set the Vagrant Cloud login token manually to the provided string. It is assumed this token is a valid Vagrant Cloud access token. ## Examples Securely authenticate to Vagrant Cloud using a username and password: ```shell-session $ vagrant login # ... Vagrant Cloud username: Vagrant Cloud password: ``` Check if the current user is authenticated: ```shell-session $ vagrant login --check You are already logged in. ``` Securely authenticate with Vagrant Cloud using a token: ```shell-session $ vagrant login --token ABCD1234 The token was successfully saved. ``` ================================================ FILE: website/content/docs/cli/machine-readable.mdx ================================================ --- layout: docs page_title: Machine Readable Output - Command-Line Interface description: |- Almost all commands in Vagrant accept a --machine-readable flag to enable machine-readable output mode. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Machine Readable Output Every Vagrant command accepts a `--machine-readable` flag which enables machine readable output mode. In this mode, the output to the terminal is replaced with machine-friendly output. This mode makes it easy to programmatically execute Vagrant and read data out of it. This output format is protected by our [backwards compatibility](/vagrant/docs/installation/backwards-compatibility) policy. Until Vagrant 2.0 is released, however, the machine readable output may change as we determine more use cases for it. But the backwards compatibility promise should make it safe to write client libraries to parse the output format. ~> **Advanced topic!** This is an advanced topic for use only if you want to programmatically execute Vagrant. If you are just getting started with Vagrant, you may safely skip this section. ## Work-In-Progress The machine-readable output is very new (released as part of Vagrant 1.4). We're still gathering use cases for it and building up the output for each of the commands. It is likely that what you may want to achieve with the machine-readable output is not possible due to missing information. In this case, we ask that you please [open an issue](https://github.com/hashicorp/vagrant/issues) requesting that certain information become available. We will most likely add it! ## Format The machine readable format is a line-oriented, comma-delimited text format. This makes it extremely easy to parse using standard Unix tools such as awk or grep in addition to full programming languages like Ruby or Python. The format is: ``` timestamp,target,type,data... ``` Each component is explained below: - **timestamp** is a Unix timestamp in UTC of when the message was printed. - **target** is the target of the following output. This is empty if the message is related to Vagrant globally. Otherwise, this is generally a machine name so you can relate output to a specific machine when multi-VM is in use. - **type** is the type of machine-readable message being outputted. There are a set of standard types which are covered later. - **data** is zero or more comma-separated values associated with the prior type. The exact amount and meaning of this data is type-dependent, so you must read the documentation associated with the type to understand fully. Within the format, if data contains a comma, it is replaced with `%!(VAGRANT_COMMA)`. This was preferred over an escape character such as \' because it is more friendly to tools like awk. Newlines within the format are replaced with their respective standard escape sequence. Newlines become a literal `\n` within the output. Carriage returns become a literal `\r`. ## Types This section documents all the available types that may be outputted with the machine-readable output. | Type | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | box-name | Name of a box installed into Vagrant. | | box-provider | Provider for an installed box. | | cli-command | A subcommand of vagrant that is available. | | error-exit | An error occurred that caused Vagrant to exit. This contains that error. Contains two data elements: type of error, error message. | | provider-name | The provider name of the target machine. `targeted` | | ssh-config | The OpenSSH compatible SSH config for a machine. This is usually the result of the "ssh-config" command. `targeted` | | state | The state ID of the target machine. `targeted` | | state-human-long | Human-readable description of the state of the machine. This is the long version, and may be a paragraph or longer. `targeted` | | state-human-short | Human-readable description of the state of the machine. This is the short version, limited to at most a sentence. `targeted` | ================================================ FILE: website/content/docs/cli/non-primary.mdx ================================================ --- layout: docs page_title: More Vagrant Commands - Command-Line Interface description: |- In addition to the commands listed in the sidebar and shown in "vagrant -h", Vagrant comes with some more commands that are hidden from basic help output. These commands are hidden because they're not useful to beginners or they're not commonly used. We call these commands "non-primary subcommands". --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # More Commands In addition to the commands listed in the sidebar and shown in `vagrant -h`, Vagrant comes with some more commands that are hidden from basic help output. These commands are hidden because they're not useful to beginners or they're not commonly used. We call these commands "non-primary subcommands". You can view all subcommands, including the non-primary subcommands, by running `vagrant list-commands`, which itself is a non-primary subcommand! Note that while you have to run a special command to list the non-primary subcommands, you do not have to do anything special to actually _run_ the non-primary subcommands. They're executed just like any other subcommand: `vagrant COMMAND`. The list of non-primary commands is below. Click on any command to learn more about it. - [docker-exec](/vagrant/docs/providers/docker/commands) - [docker-logs](/vagrant/docs/providers/docker/commands) - [docker-run](/vagrant/docs/providers/docker/commands) - [rsync](/vagrant/docs/cli/rsync) - [rsync-auto](/vagrant/docs/cli/rsync-auto) ================================================ FILE: website/content/docs/cli/package.mdx ================================================ --- layout: docs page_title: vagrant package - Command-Line Interface description: |- The "vagrant package" command is used to package a currently-running VirtualBox or Hyper-V vagrant environment into a reusable Vagrant box. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Package **Command: `vagrant package [name|id]`** This packages a currently running _VirtualBox_ or _Hyper-V_ environment into a re-usable [box](/vagrant/docs/boxes). This command can only be used with other [providers](/vagrant/docs/providers/) based on the provider implementation and if the provider supports it. ## Options - `--base NAME` - Instead of packaging a VirtualBox machine that Vagrant manages, this will package a VirtualBox machine that VirtualBox manages. `NAME` should be the name or UUID of the machine from the VirtualBox GUI. Currently this option is only available for VirtualBox. **In a multi-machine environment, the UUID is required.** This info can be gathered in two different ways `ls -l ~/VirtualBox\ VMs` or `vboxmanage list vms`. - `--output NAME` - The resulting package will be saved as `NAME`. By default, it will be saved as `package.box`. - `--include x,y,z` - Additional files will be packaged with the box. These can be used by a packaged Vagrantfile (documented below) to perform additional tasks. - `--info path/to/info.json` - The package will include a custom JSON file containing information to be displayed by the [list](/vagrant/docs/cli/box#box-list) command when invoked with the `-i` flag - `--vagrantfile FILE` - Packages a Vagrantfile with the box, that is loaded as part of the [Vagrantfile load order](/vagrant/docs/vagrantfile/#load-order) when the resulting box is used. -> **A common misconception** is that the `--vagrantfile` option will package a Vagrantfile that is used when `vagrant init` is used with this box. This is not the case. Instead, a Vagrantfile is loaded and read as part of the Vagrant load process when the box is used. For more information, read about the [Vagrantfile load order](/vagrant/docs/vagrantfile/#load-order). ================================================ FILE: website/content/docs/cli/plugin.mdx ================================================ --- layout: docs page_title: vagrant plugin - Command-Line Interface description: |- The "vagrant plugin" command is used to manage Vagrant plugins including installing, uninstalling, and license management. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin **Command: `vagrant plugin`** This is the command used to manage [plugins](/vagrant/docs/plugins/). The main functionality of this command is exposed via another level of subcommands: - [`expunge`](#plugin-expunge) - [`install`](#plugin-install) - [`license`](#plugin-license) - [`list`](#plugin-list) - [`repair`](#plugin-repair) - [`uninstall`](#plugin-uninstall) - [`update`](#plugin-update) # Plugin Expunge **Command: `vagrant plugin expunge`** This removes all user installed plugin information. All plugin gems, their dependencies, and the `plugins.json` file are removed. This command provides a simple mechanism to fully remove all user installed custom plugins. When upgrading Vagrant it may be required to reinstall plugins due to an internal incompatibility. The expunge command can help make that process easier by attempting to automatically reinstall currently configured plugins: ```shell-session # Delete all plugins and reinstall $ vagrant plugin expunge --reinstall ``` This command accepts optional command-line flags: - `--force` - Do not prompt for confirmation prior to removal - `--global-only` - Only expunge global plugins - `--local` - Include plugins in local project - `--local-only` - Only expunge local project plugins - `--reinstall` - Attempt to reinstall plugins after removal # Plugin Install **Command: `vagrant plugin install ...`** This installs a plugin with the given name or file path. If the name is not a path to a file, then the plugin is installed from remote repositories, usually [RubyGems](https://rubygems.org). This command will also update a plugin if it is already installed, but you can also use `vagrant plugin update` for that. ```shell-session # Installing a plugin from a known gem source $ vagrant plugin install my-plugin # Installing a plugin from a local file source $ vagrant plugin install /path/to/my-plugin.gem ``` If multiple names are specified, multiple plugins will be installed. If flags are given below, the flags will apply to _all_ plugins being installed by the current command invocation. If the plugin is already installed, this command will reinstall it with the latest version available. This command accepts optional command-line flags: - `--entry-point ENTRYPOINT` - By default, installed plugins are loaded internally by loading an initialization file of the same name as the plugin. Most of the time, this is correct. If the plugin you are installing has another entrypoint, this flag can be used to specify it. - `--local` - Install plugin to the local Vagrant project only. - `--plugin-clean-sources` - Clears all sources that have been defined so far. This is an advanced feature. The use case is primarily for corporate firewalls that prevent access to RubyGems.org. - `--plugin-source SOURCE` - Adds a source from which to fetch a plugin. Note that this does not only affect the single plugin being installed, by all future plugin as well. This is a limitation of the underlying plugin installer Vagrant uses. - `--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 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" (do not forget to quote these on the command-line). # Plugin License **Command: `vagrant plugin license `** This command installs a license for a proprietary Vagrant plugin, such as the [VMware Fusion provider](/vagrant/docs/providers/vmware). # Plugin List **Command: `vagrant plugin list`** This lists all installed plugins and their respective installed versions. If a version constraint was specified for a plugin when installing it, the constraint will be listed as well. Other plugin-specific information may be shown, too. This command accepts optional command-line flags: - `--local` - Include local project plugins. # Plugin Repair Vagrant may fail to properly initialize user installed custom plugins. This can be caused my improper plugin installation/removal, or by manual manipulation of plugin related files like the `plugins.json` data file. Vagrant can attempt to automatically repair the problem. If automatic repair is not successful, refer to the [expunge](#plugin-expunge) command This command accepts optional command-line flags: - `--local` - Repair local project plugins. # Plugin Uninstall **Command: `vagrant plugin uninstall [ ...]`** This uninstalls the plugin with the given name. Any dependencies of the plugin will also be uninstalled assuming no other plugin needs them. If multiple plugins are given, multiple plugins will be uninstalled. This command accepts optional command-line flags: - `--local` - Uninstall plugin from local project. # Plugin Update **Command: `vagrant plugin update []`** This updates the plugins that are installed within Vagrant. If you specified version constraints when installing the plugin, this command will respect those constraints. If you want to change a version constraint, re-install the plugin using `vagrant plugin install`. If a name is specified, only that single plugin will be updated. If a name is specified of a plugin that is not installed, this command will not install it. This command accepts optional command-line flags: - `--local` - Update plugin from local project. ================================================ FILE: website/content/docs/cli/port.mdx ================================================ --- layout: docs page_title: vagrant port - Command-Line Interface description: |- The "vagrant port" command is used to display the full list of guest ports mapped to the host machine ports. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Port **Command: `vagrant port [name|id]`** The port command displays the full list of guest ports mapped to the host machine ports: ```shell-session $ vagrant port 22 (guest) => 2222 (host) 80 (guest) => 8080 (host) ``` In a multi-machine Vagrantfile, the name of the machine must be specified: ```shell-session $ vagrant port my-machine ``` ## Options - `--guest PORT` - This displays just the host port that corresponds to the given guest port. If the guest is not forwarding that port, an error is returned. This is useful for quick scripting, for example: ```shell-session $ ssh -p $(vagrant port --guest 22) ``` - `--machine-readable` - This tells Vagrant to display machine-readable output instead of the human-friendly output. More information is available in the [machine-readable output](/vagrant/docs/cli/machine-readable) documentation. ================================================ FILE: website/content/docs/cli/powershell.mdx ================================================ --- layout: docs page_title: vagrant powershell - Command-Line Interface description: |- The "vagrant powershell" command is used to open a PowerShell prompt running inside the guest machine. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # PowerShell **Command: `vagrant powershell`** This will open a PowerShell prompt on the host into a running Vagrant guest machine. This command will only work if the machines supports PowerShell. Not every environment will support PowerShell. At the moment, only Windows is supported with this command. ## Options - `-c COMMAND` or `--command COMMAND` - This executes a single PowerShell command, prints out the stdout and stderr, and exits. ================================================ FILE: website/content/docs/cli/provision.mdx ================================================ --- layout: docs page_title: vagrant provision - Command-Line Interface description: |- The "vagrant provision" command is used to run any provisioners configured for the guest machine, such as Puppet, Chef, Ansible, Salt, or Shell. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Provision **Command: `vagrant provision [vm-name]`** Runs any configured [provisioners](/vagrant/docs/provisioning/) against the running Vagrant managed machine. This command is a great way to quickly test any provisioners, and is especially useful for incremental development of shell scripts, Chef cookbooks, or Puppet modules. You can just make simple modifications to the provisioning scripts on your machine, run a `vagrant provision`, and check for the desired results. Rinse and repeat. # Options - `--provision-with x,y,z` - This will only run the given provisioners. For example, if you have a `:shell` and `:chef_solo` provisioner and run `vagrant provision --provision-with shell`, only the shell provisioner will be run. ================================================ FILE: website/content/docs/cli/rdp.mdx ================================================ --- layout: docs page_title: vagrant rdp - Command-Line Interface description: |- The "vagrant rdp" command is used to start an RDP client for a remote desktop session with the guest machine. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # RDP **Command: `vagrant rdp`** This will start an RDP client for a remote desktop session with the guest. This only works for Vagrant environments that support remote desktop, which is typically only Windows. ## Raw Arguments You can pass raw arguments through to your RDP client on the command-line by appending it after a `--`. Vagrant just passes these through. For example: ```shell-session $ vagrant rdp -- /span ``` The above command on Windows will execute `mstsc.exe /span config.rdp`, allowing your RDP to span multiple desktops. On Darwin hosts, such as Mac OS X, the additional arguments are added to the generated RDP configuration file. Since these files can contain multiple options with different spacing, you _must_ quote multiple arguments. For example: ```shell-session $ vagrant rdp -- "screen mode id:i:0" "other config:s:value" ``` Note that as of the publishing of this guide, the Microsoft RDP Client for Mac does _not_ perform validation on the configuration file. This means if you specify an invalid configuration option or make a typographical error, the client will silently ignore the error and continue! ================================================ FILE: website/content/docs/cli/reload.mdx ================================================ --- layout: docs page_title: vagrant reload - Command-Line Interface description: |- The "vagrant reload" command is the equivalent of running "vagrant halt" followed by "vagrant up". --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Reload **Command: `vagrant reload [name|id]`** The equivalent of running a [halt](/vagrant/docs/cli/halt) followed by an [up](/vagrant/docs/cli/up). This command is usually required for changes made in the Vagrantfile to take effect. After making any modifications to the Vagrantfile, a `reload` should be called. The configured provisioners will not run again, by default. You can force the provisioners to re-run by specifying the `--provision` flag. # Options - `--provision` - Force the provisioners to run. - `--provision-with x,y,z` - This will only run the given provisioners. For example, if you have a `:shell` and `:chef_solo` provisioner and run `vagrant reload --provision-with shell`, only the shell provisioner will be run. ================================================ FILE: website/content/docs/cli/resume.mdx ================================================ --- layout: docs page_title: vagrant resume - Command-Line Interface description: |- The "vagrant resume" command is used to bring a machine back into the "up" state, perhaps if it was previously suspended via "vagrant halt" or "vagrant suspend". --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Resume **Command: `vagrant resume [name|id]`** This resumes a Vagrant managed machine that was previously suspended, perhaps with the [suspend command](/vagrant/docs/cli/suspend). The configured provisioners will not run again, by default. You can force the provisioners to re-run by specifying the `--provision` flag. # Options - `--provision` - Force the provisioners to run. - `--provision-with x,y,z` - This will only run the given provisioners. For example, if you have a `:shell` and `:chef_solo` provisioner and run `vagrant provision --provision-with shell`, only the shell provisioner will be run. ================================================ FILE: website/content/docs/cli/rsync-auto.mdx ================================================ --- layout: docs page_title: vagrant rsync-auto - Command-Line Interface description: |- The "vagrant rsync-auto" command watches all local directories of any rsync configured synced folders and automatically initiates an rsync transfer when changes are detected. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # rsync-auto **Command: `vagrant rsync-auto`** This command watches all local directories of any [rsync synced folders](/vagrant/docs/synced-folders/rsync) and automatically initiates an rsync transfer when changes are detected. This command does not exit until an interrupt is received. The change detection is optimized to use platform-specific APIs to listen for filesystem changes, and does not simply poll the directory. ## Options - `--[no-]rsync-chown` - Use rsync to modify ownership of transferred files. Enabling this option can result in faster completion due to a secondary process not being required to update ownership. By default this is disabled. - `--[no-]poll` - Force Vagrant to watch for changes using filesystem polling instead of filesystem events. This is required for some filesystems that do not support events. Warning: enabling this will make `rsync-auto` _much_ slower. By default, polling is disabled. ## Machine State Changes The `rsync-auto` command does not currently handle machine state changes gracefully. For example, if you start the `rsync-auto` command, then halt the guest machine, then make changes to some files, then boot it back up, `rsync-auto` will not attempt to resync. To ensure that the command works properly, you should start `rsync-auto` only when the machine is running, and shut it down before any machine state changes. You can always force a resync with the [rsync](/vagrant/docs/cli/rsync) command. ## Vagrantfile Changes If you change or move your Vagrantfile, the `rsync-auto` command will have to be restarted. For example, if you add synced folders to the Vagrantfile, or move the directory that contains the Vagrantfile, the `rsync-auto` command will either not pick up the changes or may begin experiencing strange behavior. Before making any such changes, it is recommended that you turn off `rsync-auto`, then restart it afterwards. ================================================ FILE: website/content/docs/cli/rsync.mdx ================================================ --- layout: docs page_title: vagrant rsync - Command-Line Interface description: The "vagrant rsync" command forces a re-sync of any rsync synced folders. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Rsync **Command: `vagrant rsync`** This command forces a re-sync of any [rsync synced folders](/vagrant/docs/synced-folders/rsync). Note that if you change any settings within the rsync synced folders such as exclude paths, you will need to `vagrant reload` before this command will pick up those changes. ## Options - `--[no-]rsync-chown` - Use rsync to modify ownership of transferred files. Enabling this option can result in faster completion due to a secondary process not being required to update ownership. By default this is disabled. ================================================ FILE: website/content/docs/cli/share.mdx ================================================ --- layout: docs page_title: vagrant share - Command-Line Interface description: |- The "vagrant share" command initializes a new Vagrant share session, which allows you to share your virtual machine with the public Internet. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Share **Command: `vagrant share`** The share command initializes a Vagrant Share session, allowing you to share your Vagrant environment with anyone in the world, enabling collaboration directly in your Vagrant environment in almost any network environment. You can learn about all the details of Vagrant Share in the [Vagrant Share section](/vagrant/docs/share/). The reference of available command-line flags to this command is available below. ## Options - `--disable-http` - Disables the creation of a publicly accessible HTTP endpoint to your Vagrant environment. With this set, the only way to access your share is with `vagrant connect`. - `--http PORT` - The port of the HTTP server running in the Vagrant environment. By default, Vagrant will attempt to find this for you. This has no effect if `--disable-http` is set. - `--https PORT` - The port of an HTTPS server running in the Vagrant environment. By default, Vagrant will attempt to find this for you. This has no effect if `--disable-http` is set. - `--ssh` - Enables SSH sharing (more information below). By default, this is not enabled. - `--ssh-no-password` - Disables the encryption of the SSH keypair created when SSH sharing is enabled. - `--ssh-port PORT` - The port of the SSH server running in the Vagrant environment. By default, Vagrant will attempt to find this for you. - `--ssh-once` - Allows SSH access only once. After the first attempt to connect via SSH to the Vagrant environment, the generated keypair is destroyed. ================================================ FILE: website/content/docs/cli/snapshot.mdx ================================================ --- layout: docs page_title: vagrant snapshot - Command-Line Interface description: |- The "vagrant snapshot" command is used to manage snapshots of the guest machine. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # 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 is not supported, Vagrant will give you an error message. The main functionality of this command is exposed via even more subcommands: - [`push`](#snapshot-push) - [`pop`](#snapshot-pop) - [`save`](#snapshot-save) - [`restore`](#snapshot-restore) - [`list`](#snapshot-list) - [`delete`](#snapshot-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 do not 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. ## Options - `--[no-]provision` - Force the provisioners to run (or prevent them from doing so). - `--no-delete` - Prevents deletion of the snapshot after restoring (so that you can restore to the same point again later). - `--no-start` - Prevents the guest from being started after restore # Snapshot Save **Command: `vagrant snapshot save [vm-name] 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 [vm-name] NAME`** This command restores the named snapshot. - `--[no-]provision` - Force the provisioners to run (or prevent them from doing so). - `--no-start` - Prevents the guest from being started after restore # Snapshot List **Command: `vagrant snapshot list`** This command will list all the snapshots taken. # Snapshot Delete **Command: `vagrant snapshot delete [vm-name] NAME`** This command will delete the named snapshot. Some providers require all "child" snapshots to be deleted first. Vagrant itself does not 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 is not an option, or is not ideal, then the deletion can also be done online with most providers. ================================================ FILE: website/content/docs/cli/ssh.mdx ================================================ --- layout: docs page_title: vagrant ssh - Command-Line Interface description: |- The "vagrant ssh" command is used to establish an SSH session into a running virtual machine to give you shell access. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # SSH **Command: `vagrant ssh [name|id] [-- extra_ssh_args]`** This will SSH into a running Vagrant machine and give you access to a shell. On a simple vagrant project, the instance created will be named default. Vagrant will ssh into this instance without the instance name: ```shell-session $ vagrant ssh Welcome to your Vagrant-built virtual machine. Last login: Fri Sep 14 06:23:18 2012 from 10.0.2.2 $ logout Connection to 127.0.0.1 closed. ``` Or you could use the name: ```shell-session $ vagrant ssh default Welcome to your Vagrant-built virtual machine. Last login: Fri Jul 20 15:09:52 2018 from 10.0.2.2 $ logout Connection to 127.0.0.1 closed. $ ``` On multi-machine setups, you can login to each VM using the name as displayed on `vagrant status` ```shell-session $ vagrant status Current machine states: node1 running (virtualbox) node2 running (virtualbox) This environment represents multiple VMs. The VMs are all listed above with their current state. $ vagrant ssh node1 Welcome to your Vagrant-built virtual machine. Last login: Fri Sep 14 06:23:18 2012 from 10.0.2.2 vagrant@bionic64:~$ logout Connection to 127.0.0.1 closed. $ vagrant ssh node2 Welcome to your Vagrant-built virtual machine. Last login: Fri Sep 14 06:23:18 2012 from 10.0.2.2 vagrant@bionic64:~$ logout Connection to 127.0.0.1 closed. $ ``` On a system with machines running from different projects, you could use the id as listed in `vagrant global-status` ```shell-session $ vagrant global-status id name provider state directory ----------------------------------------------------------------------- 13759ff node1 virtualbox running /Users/user/vagrant/folder The above shows information about all known Vagrant environments on this machine. This data is cached and may not be completely up-to-date (use "vagrant global-status --prune" to prune invalid entries). To interact with any of the machines, you can go to that directory and run Vagrant, or you can use the ID directly with Vagrant commands from any directory. $ vagrant ssh 13759ff Welcome to your Vagrant-built virtual machine. Last login: Fri Jul 20 15:19:36 2018 from 10.0.2.2 vagrant@bionic64:~$ logout Connection to 127.0.0.1 closed. $ ``` If a `--` (two hyphens) are found on the command line, any arguments after this are passed directly into the `ssh` executable. This allows you to pass any arbitrary commands to do things such as reverse tunneling down into the `ssh` program. ## Options - `-c COMMAND` or `--command COMMAND` - This executes a single SSH command, prints out the stdout and stderr, and exits. - `-p` or `--plain` - This does an SSH without authentication, leaving authentication up to the user. ## SSH client usage Vagrant will attempt to use the local SSH client installed on the host machine. On POSIX machines, an SSH client must be installed and available on the PATH. For Windows installations, an SSH client is provided within the installer image. If no SSH client is found on the current PATH, Vagrant will use the SSH client it provided. Depending on the local environment used for running Vagrant, the installer provided SSH client may not work correctly. For example, when using a cygwin or msys2 shell the SSH client will fail to work as expected when run interactively. Installing the SSH package built for the current working environment will resolve this issue. ## Background Execution If the command you specify runs in the background (such as appending a `&` to a shell command), it will be terminated almost immediately. This is because when Vagrant executes the command, it executes it within the context of a shell, and when the shell exits, all of the child processes also exit. To avoid this, you will need to detach the process from the shell. Please Google to learn how to do this for your shell. One method of doing this is the `nohup` command. ## Pageant on Windows The SSH executable will not be able to access Pageant on Windows. While Vagrant is capable of accessing Pageant via internal libraries, the SSH executable does not have support for Pageant. This means keys from Pageant will not be available for forwarding when using the `vagrant ssh` command. Third party programs exist to allow the SSH executable to access Pageant by creating a Unix socket for the SSH executable to read. For more information please see [ssh-pageant](https://github.com/cuviper/ssh-pageant). ================================================ FILE: website/content/docs/cli/ssh_config.mdx ================================================ --- layout: docs page_title: vagrant ssh-config - Command-Line Interface description: |- The "vagrant ssh-config" command is used to output a valid SSH configuration file capable of SSHing into the guest machine directly. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # SSH Config **Command: `vagrant ssh-config [name|id]`** This will output valid configuration for an SSH config file to SSH into the running Vagrant machine from `ssh` directly (instead of using `vagrant ssh`). ## Options - `--host NAME` - Name of the host for the outputted configuration. ================================================ FILE: website/content/docs/cli/status.mdx ================================================ --- layout: docs page_title: vagrant status - Command-Line Interface description: |- The "vagrant status" command is used to tell you the status of the virtual machines in the current Vagrant environment. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Status **Command: `vagrant status [name|id]`** This will tell you the state of the machines Vagrant is managing. It is quite easy, especially once you get comfortable with Vagrant, to forget whether your Vagrant machine is running, suspended, not created, etc. This command tells you the state of the underlying guest machine. ================================================ FILE: website/content/docs/cli/suspend.mdx ================================================ --- layout: docs page_title: vagrant suspend - Command-Line Interface description: |- The "vagrant suspend" command is used to suspend the guest machine Vagrant is currently managing. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Suspend **Command: `vagrant suspend [name|id]`** This suspends the guest machine Vagrant is managing, rather than fully [shutting it down](/vagrant/docs/cli/halt) or [destroying it](/vagrant/docs/cli/destroy). A suspend effectively saves the _exact point-in-time state_ of the machine, so that when you [resume](/vagrant/docs/cli/resume) it later, it begins running immediately from that point, rather than doing a full boot. This generally requires extra disk space to store all the contents of the RAM within your guest machine, but the machine no longer consumes the RAM of your host machine or CPU cycles while it is suspended. ================================================ FILE: website/content/docs/cli/up.mdx ================================================ --- layout: docs page_title: vagrant up - Command-Line Interface description: |- The "vagrant up" command is used to create, configuration, and provision a guest machine according to your Vagrantfile. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Up **Command: `vagrant up [name|id]`** This command creates and configures guest machines according to your [Vagrantfile](/vagrant/docs/vagrantfile/). This is the single most important command in Vagrant, since it is how any Vagrant machine is created. ## Options - `name` - Name of machine defined in [Vagrantfile](/vagrant/docs/vagrantfile/). Using `name` to specify the Vagrant machine to act on must be done from within a Vagrant project (directory where the Vagrantfile exists). - `id` - Machine id found with `vagrant global-status`. Using `id` allows you to call `vagrant up id` from any directory. - `--[no-]destroy-on-error` - Destroy the newly created machine if a fatal, 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. - `--provider x` - Bring the machine up with the given [provider](/vagrant/docs/providers/). By default this is "virtualbox". - `--[no-]provision` - Force, or prevent, the provisioners to run. - `--provision-with x,y,z` - This will only run the given provisioners. For example, if you have a `:shell` and `:chef_solo` provisioner and run `vagrant provision --provision-with shell`, only the shell provisioner will be run. ================================================ FILE: website/content/docs/cli/upload.mdx ================================================ --- layout: docs page_title: vagrant upload - Command-Line Interface description: |- The "vagrant upload" command is used to upload files from the host to a guest machine. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Upload **Command: `vagrant upload source [destination] [name|id]`** This command uploads files and directories from the host to the guest machine. ## Options - `destination` - Path on the guest machine to upload file or directory. - `source` - Path to file or directory on host to upload to guest machine. - `--compress` - Compress the file or directory before uploading to guest machine. - `--compression-type type` - Type of compression to use when compressing file or directory for upload. Defaults to `zip` for Windows guests and `tgz` for non-Windows guests. Valid values: `tgz`, `zip`. - `--temporary` - Create a temporary location on the guest machine and upload files to that location. ================================================ FILE: website/content/docs/cli/validate.mdx ================================================ --- layout: docs page_title: vagrant validate - Command-Line Interface description: The "vagrant validate" command is used to validate your Vagrantfile. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Validate **Command: `vagrant validate`** This command validates your [Vagrantfile](/vagrant/docs/vagrantfile/). ## Options - `--ignore-provider` - Ignores provider config options. ## Examples Validate the syntax of the Vagrantfile to ensure it is correctly structured and free of errors ```shell-session $ vagrant validate Vagrantfile validated successfully. ``` Ensure that the Vagrantfile is correctly structured while ignoring provider-specific configuration options: ```shell-session $ vagrant validate --ignore-provider virtualbox ==> default: Ignoring provider config for validation... Vagrantfile validated successfully. ``` ================================================ FILE: website/content/docs/cli/version.mdx ================================================ --- layout: docs page_title: vagrant version - Command-Line Interface description: |- The "vagrant version" command is used to output the version of Vagrant currently installed on the system. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Version **Command: `vagrant version`** This command tells you the version of Vagrant you have installed as well as the latest version of Vagrant that is currently available. In order to determine the latest available Vagrant version, this command must make a network call. If you only want to see the currently installed version, use `vagrant --version`. ================================================ FILE: website/content/docs/cli/winrm.mdx ================================================ --- layout: docs page_title: vagrant winrm - Command-Line Interface description: |- The "vagrant winrm" command is used execute commands on the remote machine via WinRM --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # WinRM **Command: `vagrant winrm [name|id]`** Executes the provided command(s) on the guest machine using the WinRM communicator. Commands are provided with the `--command` option and multiple `--command` flags may be provided for executing multiple commands. This command requires the guest machine to be configured with the WinRM communicator. ## Options - `--command COMMAND` - Command to execute. - `--elevated` - Run command(s) with elevated credentials. - `--shell (cmd|powershell)` - Shell to execute commands. Defaults to `powershell`. ================================================ FILE: website/content/docs/cli/winrm_config.mdx ================================================ --- layout: docs page_title: vagrant winrm-config - Command-Line Interface description: |- The "vagrant winrm-config" command is used to output the WinRM configuration used to connect to the guest machine. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # WinRM Config **Command: `vagrant winrm-config [name|id]`** This will output the WinRM configuration used for connecting to the guest machine. It requires that the WinRM communicator is in use for the guest machine. ## Options - `--host NAME` - Name of the host for the outputted configuration. ================================================ FILE: website/content/docs/cloud-init/configuration.mdx ================================================ --- layout: docs page_title: Vagrant Cloud-Init Configuration description: Documentation of various configuration options for Vagrant cloud-init --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Configuration Vagrant cloud-init has several options that allow users to define a config to be used with cloud-init. For more detailed information about these config values and how to use cloud-init, please read the [official documentation for cloud-init](https://cloudinit.readthedocs.io/en/latest/index.html). ## cloud_init Options It should be noted that Vagrant will not validate the correctness of the `cloud-init` config provided, only that a cloud-init config has been provided through `path` or directly `inline` in a Vagrantfile. - `content_type` (string) - Required argument that defines the Content-Type of the given cloud_init config. Vagrant only supports the following options for `content_type`: - `"text/cloud-boothook"` - `"text/cloud-config"` - `"text/cloud-config-archive"` - `"text/jinja2"` - `"text/part-handler"` - `"text/upstart-job"` - `"text/x-include-once-url"` - `"text/x-include-url"` - `"text/x-shellscript"` - `path` (string) - Path to a file on the host machine that contains cloud-init user data. This will be added to the multipart user-data file along with its `content_type`. Incompatible with the `inline` option. - `inline` (string) - Inline cloud-init user data. This will be added to the multipart user-data file along with its `content_type`. Incompatible with `path` option. Examples of how to define these options can be found in the [usage documentation](/vagrant/docs/cloud-init/configuration). ### cloud_init Type When defining a config for cloud_init, you can optionally define a `type` for the config: ```ruby config.vm.cloud_init :user_data, content_type: "text/cloud-config", path: "config.cfg" config.vm.cloud_init :user_data do |cloud_init| cloud_init.content_type = "text/cloud-config" cloud_init.path = "config.cfg" end ``` However, this is not a requirement. Leaving off `type` will default to `:user_data`. - `type` (Symbol) - This is an optional config that defines the type of cloud-init config. Currently, the only supported `type` is `:user_data`. If a type is not defined, it will default to `:user_data`. ================================================ FILE: website/content/docs/cloud-init/index.mdx ================================================ --- layout: docs page_title: Cloud-Init description: Introduction to using cloud-init with Vagrant --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Vagrant cloud-init For examples on how to achieve this, among other use cases, please refer to the [usage](/vagrant/docs/cloud-init/usage) guide for more information! For more information about what options are available for configuring cloud-init, see the [configuration section](/vagrant/docs/cloud-init/configuration). ================================================ FILE: website/content/docs/cloud-init/usage.mdx ================================================ --- layout: docs page_title: Vagrant Cloud-Init Usage description: Various Vagrant Cloud-Init examples --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Basic Usage Below are some very simple examples of how to use Vagrant Cloud-Init with the VirtualBox provider. For more detailed information about these config values and how to use cloud-init, please read the [official documentation for cloud-init](https://cloudinit.readthedocs.io/en/latest/index.html). ## Basic Examples A cloud_init config can be defined as a "hash" of key values, or as a block. Below are two examples of this for defining a cloud_init config: ```ruby # Simplified form config.vm.cloud_init content_type: "text/x-shellscript", path: "./foo/bar.sh" # Block form config.vm.cloud_init do |cloud_init| cloud_init.content_type = "text/cloud-config" cloud_init.inline = <<-EOF package_update: true packages: - nginx EOF end ``` The first part will be read from a local file `./foo/bar`, and the second part will be attached using the inline content. Both "block" and "hash" forms are supported, and should work interchangeably. Individual machines may have their own cloud-init data: ```ruby config.vm.define "web" do |web| web.vm.cloud_init content_type: "text/cloud-config", inline: <<-EOF package_update: true packages: - nginx EOF end end config.vm.define "db" do |db| db.vm.cloud_init content_type: "text/cloud-config", inline: <<-EOF package_update: true packages: - postgresql EOF end ``` ================================================ FILE: website/content/docs/disks/configuration.mdx ================================================ --- layout: docs page_title: Vagrant Disks Configuration description: Documentation of various configuration options for Vagrant Disks --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Configuration Vagrant Disks has several options that allow users to define and attach disks to guests. ## Disk Options - `disk_ext` (string) - Optional argument that defines what kind of file extension a disk should have. Defaults to `"vdi"` if unspecified. For a list of supported disk extensions, please check the specific provider being used. Not used for type `:dvd.` - `file` (string) - For type `:dvd`, this is a required argument that should point to an `.iso` file on the host machine. For type `:disk`, this is an optional argument that can point to the location of a disk file that already exists. - `name` (string) - Required option to give the disk a name. This name will also be used as the filename when creating a virtual hard disk. - `primary` (boolean) - Optional argument that configures a given disk to be the "primary" disk to manage on the guest. There can only be one `primary` disk per guest, and it must be of type `:disk`. Defaults to `false` if not specified. - `provider_config` (hash) - Additional provider specific options for managing a given disk. Please refer to the provider specific documentation to see any available provider_config options. Generally, the disk option accepts two kinds of ways to define a provider config: - `providername__diskoption: value` - The provider name followed by a double underscore, and then the provider specific option for that disk - `{providername: {diskoption: value}, otherprovidername: {diskoption: value}` - A hash where the top level key(s) are one or more providers, and each provider keys values are a hash of options and their values. - `size` (String) - The size of the disk to create. For example, `"10GB"`. Not used for type `:dvd.` **Note:** More specific examples of these can be found under the provider specific disk page. The `provider_config` option will depend on the provider you are using. Please read the provider specific documentation for disk management to learn about what options are available to use. ## Disk Types The disk config currently accepts three kinds of disk types: - `disk` (symbol) - `dvd` (symbol) - `floppy` (symbol) **NOTE:** These types depend on the provider used, and may not yet be functional. Please refer to the provider specific implementation for more details for what is supported. You can set a disk type with the first argument of a disk config in your Vagrantfile: ```ruby config.vm.disk :disk, name: "backup", size: "10GB" config.vm.disk :dvd, name: "installer", file: "./installer.iso" config.vm.disk :floppy, name: "cool_files" ``` ## Provider Author Guide If you are a vagrant plugin author who maintains a provider for Vagrant, this short guide will hopefully give some information on how to use the internal disk config object. ~> **Warning!** This guide is still being written as we develop this new feature for Vagrant. Is something missing, or could this be improved? Please let us know on GitHub by opening an issue or open a pull request directly. All providers must implement the capability `configure_disks`, and `cleanup_disks`. These methods are responsible for the following: - `configure_disks` - Reads in a Vagrant config for defined disks from a Vagrantfile, and creates and attaches the disks based on the given config - `cleanup_disks` - Compares the current Vagrant config for defined disks and detaches any disks that are no longer valid for a guest. These methods are called in the builtin Vagrant actions _Disk_ and _CleanupDisks_. If the provider does not support these capabilities, they will be skipped over and no disks will be configured. It is the providers job to implement these provider capabilities and handle the methods required to support disk creation and deletion. Vagrant will handle parsing and supplying the config object based on what has been defined inside a users Vagrantfile. For a more detailed example of how to use this disk configuration with Vagrant, please check out how it was implemented using the VirtualBox provider. ### The disk_meta file Both builtin disk actions `configure_disks` and `cleanup_disks` expect to read and write down a `disk_meta` file inside a machines data dir. This file is specifically for keeping track of the _last configured state_ for disks in a given provider. Generally, this file is used as a way for Vagrant to keep track of what disks are being managed by Vagrant with the provider uses, so that it does not accidentally delete or manage disks that were configured outside of Vagrants configuration. For the VirtualBox provider, Vagrant uses this file to see what disks were configured on the _last run_ of Vagrant, and compares that to the current configured state for the Vagrantfile on the _current run_ of Vagrant. It specifically stores each disks UUID and disk name for use. If it notices a disk that is no longer in the Vagrantfile, it can be assumed that the disk is no longer valid for that guest, and cleans up the disk. This may not be required for your provider, however with the VirtualBox provider, Vagrant needs a way to keep track of the defined disks managed by Vagrant and their disk UUIDs that VirtualBox uses to keep track of these disks. ### The provider_config hash The disk config class supports an optional hash of options called `provider_config`. This allows the user to define some additional options for a provider to use that may be non-standard across different providers. ================================================ FILE: website/content/docs/disks/hyperv/common-issues.mdx ================================================ --- layout: docs page_title: Common Issues - Disks Hyper-V Provider description: |- This page lists some common issues people run into with Vagrant and Hyper-V as well as solutions for those issues. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Common Issues and Troubleshooting This page lists some common issues people run into with Vagrant and Hyper-V as well as solutions for those issues. ## Are my disks attached? A handy way to figure out what disks are attached (or not attached) to your guest is to open up the Hyper-V GUI and select the guest. When selecting a guest on the GUI, it should open more information about the guest, including storage information. Here you should see a list of disks attached to your guest. ## Applying Vagrant disk configuration changes to guests Due to how Hyper-V works, you must reload your guest for any disk config changes to be applied. So if you update your Vagrantfile to update or even remove disks, make sure to `vagrant reload` your guests for these changes to be applied. ================================================ FILE: website/content/docs/disks/hyperv/index.mdx ================================================ --- layout: docs page_title: Disks for Hyper-V Provider description: |- Vagrant comes with support out of the box for Hyper-V, a free, cross-platform consumer virtualization product. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Hyper-V Because of how Hyper-V handles disk management, a Vagrant guest _must_ be powered off for any changes to be applied to a guest. If you make a configuration change with a guests disk, you will need to `vagrant reload` the guest for any changes to be applied. For more information on how to use Hyper-V to configure disks for a guest, refer to the [general usage](/vagrant/docs/disks/usage) and [configuration](/vagrant/docs/disks/configuration) guide for more information. ================================================ FILE: website/content/docs/disks/hyperv/usage.mdx ================================================ --- layout: docs page_title: Usage - Disks Hyper-V Provider description: |- The Vagrant Hyper-V provider is used just like any other provider. Please read the general basic usage page for providers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Usage For examples of how to use the disk feature with Hyper-V, please refer to the [general disk usage guide](/vagrant/docs/disks/usage) for more examples. ## provider_config options Most options are used for either creating or attaching a hard disk to your guest. Vagrant supports most options for these operations. You should be able to define the PowerShell specific argument to a given Hyper-V command in the provider_config hash, and Vagrant should properly pass it along to the command. To define a provider specific option, please refer to the [Disk Options documentation page](/vagrant/docs/disks/configuration) for more info. ### Note about options defined below It is possible these options could be out of date or stale. If you happen to see an option that has changed or is missing from this page, please open an issue or pull request on Vagrants GitHub page to correct this. ### New-VHD Supported Options For more information about each option, please visit the [New-VHD Hyper-V documentation](https://docs.microsoft.com/en-us/powershell/module/hyper-v/new-vhd?view=win10-ps). **Note:** By default, all Hyper-V disks are defined as a Dynamic virtual hard disk. If you wish to make the disk a fixed size, you can set the `Fixed` option below when creating a new disk. - `BlockSizeBytes` (string) - Optional argument, i.e. `"128MB"` - `Differencing` (bool) - If set, the disk will be used to store differencing changes from parent disk (must set `ParentPath`) - `Fixed` (bool) - If set, the disk will be a fixed size, not dynamically allocated. - `LogicalSectorSizeBytes` (int) - Optional argument, must be either `512` or `4096` - `ParentPath` (string) - The parent disk path used if a `Differencing` disk is defined - `PhysicalSectorSizeBytes` (string) - Optional argument, must be either `512` or `4096` - `SourceDisk` (int) - Existing disk to use as a source for the new disk ### Add-VMHardDiskDrive Supported Options For more information about each option, please visit the [Add-VMHardDiskDrive Hyper-V documentation](https://docs.microsoft.com/en-us/powershell/module/hyper-v/add-vmharddiskdrive?view=win10-ps) Generally, these options do not need to be set or handled by most users. Only use these options if you are sure you know what you are doing. Vagrant will be able to attach disks for you without these options, but they are available if it is required that you specify a specific location for a disk. - `ControllerLocation` (int) - The location that the disk should be attached to on the controller - `ControllerNumber` (int) - The controller to use for attaching the disk - `ControllerType` (string) - The kind of controller to use when attaching the a disk. Only `"IDE"` and `"SCSI"` are valid. ================================================ FILE: website/content/docs/disks/index.mdx ================================================ --- layout: docs page_title: Vagrant Disks description: Introduction to Vagrant Disks --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Vagrant Disks Vagrant Disks is a feature that allows users to define what mediums should be attached to their guests, as well as allowing users to resize their primary disk. For examples on how to achieve this, among other use cases, please refer to the [usage](/vagrant/docs/disks/usage) guide for more information! For more information about what options are available for configuring disks, see the [configuration section](/vagrant/docs/disks/configuration). ## Supported Providers Currently, only VirtualBox is supported. Please refer to the [VirtualBox documentation](/vagrant/docs/disks/virtualbox) for more information on using disks with the VirtualBox provider! ================================================ FILE: website/content/docs/disks/usage.mdx ================================================ --- layout: docs page_title: Vagrant Disk Usage description: Various Vagrant Disk examples --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Basic Usage Below are some very simple examples of how to use Vagrant Disks with the VirtualBox provider. ## Basic Examples ### Resizing your primary disk Sometimes, the primary disk for a guest is not large enough and you will need to add more space. To resize a disk, you can simply add a config like this below to expand the size of your guests drive: ```ruby config.vm.disk :disk, size: "100GB", primary: true ``` Note: the `primary: true` is what tells Vagrant to expand the guests main drive. Without this option, Vagrant will instead attach a _new_ disk to the guest. For example, this Ubuntu guest will now come with 100GB of space, rather than the default: ```ruby Vagrant.configure("2") do |config| config.vm.define "hashicorp" do |h| h.vm.box = "hashicorp/bionic64" h.vm.provider :virtualbox h.vm.disk :disk, size: "100GB", primary: true end end ``` It should be noted that due to how VirtualBox functions, it is not possible to shrink the size of a disk. ### Attaching new hard disks Vagrant can attach multiple disks to a guest using the VirtualBox provider. An example of attaching a single disk to a guest with 10 GB of storage can be found below: ```ruby Vagrant.configure("2") do |config| config.vm.define "hashicorp" do |h| h.vm.box = "hashicorp/bionic64" h.vm.provider :virtualbox h.vm.disk :disk, size: "10GB", name: "extra_storage" end end ``` Optionally, if you need to attach many disks, you can use Ruby to generate multiple disks for Vagrant to create and attach to your guest: ```ruby Vagrant.configure("2") do |config| config.vm.define "hashicorp" do |h| h.vm.box = "hashicorp/bionic64" h.vm.provider :virtualbox (0..3).each do |i| h.vm.disk :disk, size: "5GB", name: "disk-#{i}" end end end ``` Note: VirtualBox has a hard limit on the number of disks that can be attached to a given storage controller, which is defined by the controller type. Attempting to configure more disks than are supported by the primary controller will result in a Vagrant error. ### Attaching optical drives Vagrant can attach `.iso` files as optical drives using the VirtualBox provider. An example of attaching an optical drive to a guest can be found below: ```ruby Vagrant.configure("2") do |config| config.vm.define "hashicorp" do |h| h.vm.box = "hashicorp/bionic64" h.vm.provider :virtualbox h.vm.disk :dvd, name: "installer", file: "./installer.iso" end end ``` As with hard disks, configuring more disks than are supported by your VM's storage controller arrangement will result in a Vagrant error. ### Removing Disks If you have removed a disk from your Vagrant config and wish for it to be detached from the guest, you will need to `vagrant reload` your guest to apply these changes. **NOTE:** Removing virtual hard disks created by Vagrant will also delete the medium from your hard drive. ================================================ FILE: website/content/docs/disks/virtualbox/common-issues.mdx ================================================ --- layout: docs page_title: Common Issues - Disks VirtualBox Provider description: |- This page lists some common issues people run into with Vagrant and VirtualBox as well as solutions for those issues. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Common Issues and Troubleshooting This page lists some common issues people run into with Vagrant and VirtualBox as well as solutions for those issues. ## Are my disks attached? A handy way to figure out what disks are attached (or not attached) to your guest is to open up the VirtualBox GUI and select the guest. When selecting a guest on the GUI, it should open more information about the guest, including storage information. Here you should see a list of disks attached to your guest. ## How many disks can I attach? Vagrant attaches all new disks defined to guest's primary controller. As of VirtualBox 6.1.x, storage controllers have the following limits to the number of disks that are supported per guest: - IDE Controllers: 4 - SATA Controllers: 30 - SCSI Controllers: 16 Therefore if your primary disk is attached to a SATA Controller and you try to define and attach more than 30, it will result in an error. This number _includes_ the primary disk for the guest. DVD attachments are subject to the same limits. Optical disk attachments will be attached to the storage controller with the highest boot priority (usually the IDE controller). ## Resizing VMDK format disks VMDK disks cannot be resized in their current state, so Vagrant will automatically convert these disks to VDI, resize the disk, and convert it back to its original format. Many Vagrant boxes default to using the VMDK disk format, so resizing disks for many users will require Vagrant to convert these disks. Generally, this will be transparent to the user. However if Vagrant crashes or if a user interrupts Vagrant during the cloning process, there is a chance that you might lose your data. ## Applying Vagrant disk configuration changes to guests Due to how VirtualBox works, you must reload your guest for any disk config changes to be applied. So if you update your Vagrantfile to update or even remove disks, make sure to `vagrant reload` your guests for these changes to be applied. ================================================ FILE: website/content/docs/disks/virtualbox/index.mdx ================================================ --- layout: docs page_title: Disks for VirtualBox Provider description: |- Vagrant comes with support out of the box for VirtualBox, a free, cross-platform consumer virtualization product. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # VirtualBox **Vagrant currently only supports VirtualBox version 5.x and newer for configuring and attaching disks.** Because of how VirtualBox handles disk management, a Vagrant guest _must_ be powered off for any changes to be applied to a guest. If you make a configuration change with a guests disk, you will need to `vagrant reload` the guest for any changes to be applied. When new disks are defined to be attached to a guest, Vagrant will attach disks to a particular storage controller based on the type of disk configured: - For the `:disk` type, Vagrant will use the storage controller containing the boot disk. It will also create the disk if necessary. - For the `:dvd` type, Vagrant will attach the disk to the storage controller that comes earliest in the machine's boot order. For example, if a VM has a SATA controller and an IDE controller, the disk will be attached to the IDE controller. Vagrant will not be able to configure disks of a given type if the associated storage controller does not exist. In this case, you may use [provider-specific customizations](/vagrant/docs/providers/virtualbox/configuration#vboxmanage-customizations) to add a required storage controller. It should also be noted that storage controllers have different limits for the number of disks that can be attached. Attempting to configure more than the maximum number of disks for a storage controller type will result in a Vagrant error. For more information on how to use VirtualBox to configure disks for a guest, refer to the [general usage](/vagrant/docs/disks/usage) and [configuration](/vagrant/docs/disks/configuration) guide for more information. ================================================ FILE: website/content/docs/disks/virtualbox/usage.mdx ================================================ --- layout: docs page_title: Usage - Disks VirtualBox Provider description: |- The Vagrant VirtualBox provider is used just like any other provider. Please read the general basic usage page for providers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Usage For examples of how to use the disk feature with VirtualBox, please refer to the [general disk usage guide](/vagrant/docs/disks/usage) for more examples. ## provider_config options Currently, there are no additional options supported for the `provider_config` option. This page will be updated with any valid options as they become supported. ================================================ FILE: website/content/docs/disks/vmware/common-issues.mdx ================================================ --- layout: docs page_title: Common Issues - Disks VMware Provider description: |- HashiCorp develops an official VMware Fusion and VMware Workstation provider for Vagrant. This provider allows Vagrant to power VMware based machines and take advantage of the improved stability and performance that VMware software offers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Common Issues and Troubleshooting This page lists some common issues people run into with Vagrant and VMware as well as solutions for those issues. ## Are my disks attached? A handy way to figure out what disks are attached (or not attached) to your guest is to open up the VMware GUI and open up the guest settings and selecting the disks options. ## How many disks can I attach? Vagrant will attempt to attach all disks specified in the Vagrantfile. If more than four `ide` type disks are specified, only the first four will be attached. ## Applying Vagrant disk configuration changes to guests Due to how VMware works, you must reload your guest for any disk config changes to be applied. So if you update your Vagrantfile to update or even remove disks, make sure to `vagrant reload` your guests for these changes to be applied. Also note, that Vagrant will not decrease the size of a disk. ## Disk functionality with snapshots If snapshots exist for a VM, disk functionality will be limited. Vagrant will return an error for any actions that are limited due to the existence of snapshots. In order to restore functionality the snapshots must be removed. This can be done using the [`vagrant snapshot delete`](/vagrant/docs/cli/snapshot) command. To delete all snapshots for a VMware backed VM try `vagrant cap provider delete_all_snapshots --target `. Note once a snapshot is deleted, it can not be restored. ================================================ FILE: website/content/docs/disks/vmware/index.mdx ================================================ --- layout: docs page_title: Disks for VMware Provider description: |- HashiCorp develops an official VMware Fusion and VMware Workstation provider for Vagrant. This provider allows Vagrant to power VMware based machines and take advantage of the improved stability and performance that VMware software offers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # VMware Because of how VMware handles disk management, a Vagrant guest _must_ be powered off for any changes to be applied to a guest. If you make a configuration change with a guests disk, you will need to `vagrant reload` the guest for any changes to be applied. For more information on how to use VMware to configure disks for a guest, refer to the [general usage](/vagrant/docs/disks/usage) and [configuration](/vagrant/docs/disks/configuration) guide for more information. ================================================ FILE: website/content/docs/disks/vmware/usage.mdx ================================================ --- layout: docs page_title: Usage - Disks VMware Provider description: |- HashiCorp develops an official VMware Fusion and VMware Workstation provider for Vagrant. This provider allows Vagrant to power VMware based machines and take advantage of the improved stability and performance that VMware software offers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Usage For examples of how to use the disk feature with VMWware, please refer to the [general disk usage guide](/vagrant/docs/disks/usage) for more examples. ## provider_config options Vagrant supports some additional VMWware specific options for specifying disk. To define a provider specific option, please refer to the [Disk Options documentation page](/vagrant/docs/disks/configuration) for more info. ### Note about options defined below It is possible these options could be out of date or stale. If you happen to see an option that has changed or is missing from this page, please open an issue or pull request on Vagrants GitHub page to correct this. - `bus_type` (string) - Sets the bus type when attaching the disk. Possible options are `sata`, `ide`, and `scsi`. Defaults to `scsi` - `adapter_type` (string) - Sets the adapter type when creating the disk. Possible options are `ide`, `buslogic` and `lsilogic`. Defaults to `lsilogic` ================================================ FILE: website/content/docs/experimental/index.mdx ================================================ --- layout: docs page_title: Vagrant Experimental Feature Flag description: Introduction to Vagrants Experimental Feature Flag --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Experimental Feature Flag Some features that aren't ready for release can be enabled through this feature flag. There are a couple of different ways of going about enabling these features. It is also worth noting that Vagrant will not validate the existence of a feature flag. For example if you are on Linux or Mac, and you wish to enable every single experimental feature, you can set the flag to "on" by setting it to `1`: ```shell export VAGRANT_EXPERIMENTAL="1" ``` You can also enable some or many features if there are specific ones you would like, but don't want every single feature enabled: ```shell # Only enables feature_one export VAGRANT_EXPERIMENTAL="feature_one" ``` ```shell # Enables both feature_one and feature_two export VAGRANT_EXPERIMENTAL="feature_one,feature_two" ``` ## Valid experimental features ~> **Advanced topic!** This is an advanced topic for use only if you want to use new Vagrant features. If you are just getting started with Vagrant, you may safely skip this section. This is a list of all the valid experimental features that Vagrant recognizes: * `none_communicator` - Allows Vagrant to manage remote machines without the ability to connect to them for configuration/provisioning. ================================================ FILE: website/content/docs/index.mdx ================================================ --- layout: docs page_title: Documentation description: |- Welcome to the documentation for Vagrant - the command line utility for managing the lifecycle of virtual machines. This website aims to document every feature of Vagrant from top-to-bottom, covering as much detail as possible. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Vagrant Documentation Welcome to the documentation for Vagrant - the command line utility for managing the lifecycle of virtual machines. This website aims to document every feature of Vagrant from top-to-bottom, covering as much detail as possible. If you are just getting started with Vagrant, we highly recommended starting with the [getting started tutorial](/vagrant/tutorials) on HashiCorp's Learn platform first, and then returning to this page. The navigation will take you through each component of Vagrant. Click on a navigation item to get started, or read more about [why developers, designers, and operators choose Vagrant](/vagrant/intro) for their needs. ================================================ FILE: website/content/docs/installation/backwards-compatibility.mdx ================================================ --- layout: docs page_title: Backwards Compatibility description: Vagrant makes a very strict backwards-compatibility promise. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Backwards Compatibility ## For 1.0.x Vagrant 1.1+ provides full backwards compatibility for valid Vagrant 1.0.x Vagrantfiles which do not use plugins. After installing Vagrant 1.1, your 1.0.x environments should continue working without modifications, and existing running machines will continue to be managed properly. This compatibility layer will remain in Vagrant up to and including Vagrant 2.0. It may still exist after that, but Vagrant's compatibility promise is only for two versions. Seeing that major Vagrant releases take years to develop and release, it is safe to stick with your version 1.0.x Vagrantfile for the time being. If you use any Vagrant 1.0.x plugins, you must remove references to these from your Vagrantfile prior to upgrading. Vagrant 1.1+ introduces a new plugin format that will protect against this sort of incompatibility from ever happening again. ## For 1.x Backwards compatibility between 1.x is not promised, and Vagrantfile syntax stability is not promised until 2.0 final. Any backwards incompatibilities within 1.x will be clearly documented. This is similar to how Vagrant 0.x was handled. In practice, Vagrant 0.x only introduced a handful of backwards incompatibilities during the entire development cycle, but the possibility of backwards incompatibilities is made clear so people are not surprised. Vagrant 2.0 final will have a stable Vagrantfile format that will remain backwards compatible, just as 1.0 is considered stable. ================================================ FILE: website/content/docs/installation/index.mdx ================================================ --- layout: docs page_title: Install Vagrant description: |- Vagrant is available for most platforms. Install the Vagrant package using standard procedures for your operating system. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Install Vagrant To get started with Vagrant, download the appropriate installer or package for your platform from our [Vagrant downloads page](/vagrant/downloads). Install the package with the standard procedures for your operating system. The installer automatically adds `vagrant` to your system path so that it is available in terminals. If it is not found, log out and back into your system; this is a common issue for Windows. ~> **Rubygem installation is unsupported** Vagrant 1.0.x has the option to be installed as a [RubyGem](https://en.wikipedia.org/wiki/RubyGems). However, this installation method is no longer supported. If you have an old version of Vagrant installed via Rubygems, remove it prior to installing newer versions of Vagrant. ## First development environment If you are new to Vagrant, the next step to set up a development environment is to install a [box](/vagrant/tutorials/getting-started/getting-started-boxes). ## How to use multiple hypervisors Hypervisors often do not allow you to bring up virtual machines if you have more than one hypervisor in use. Below are a couple of examples to allow you to use Vagrant and VirtualBox if another hypervisor is present. ### Linux, VirtualBox, and KVM If you encounter the following error message, it is because another hypervisor, like KVM, is in use. ```shell-session There was an error while executing `VBoxManage`, a CLI used by Vagrant for controlling VirtualBox. The command and stderr is shown below. Command: ["startvm", , "--type", "headless"] Stderr: VBoxManage: error: VT-x is being used by another hypervisor (VERR_VMX_IN_VMX_ROOT_MODE). VBoxManage: error: VirtualBox can't operate in VMX root mode. Please disable the KVM kernel extension, recompile your kernel and reboot (VERR_VMX_IN_VMX_ROOT_MODE) VBoxManage: error: Details: code NS_ERROR_FAILURE (0x80004005), component ConsoleWrap, interface IConsole ``` You must add the additional hypervisors to the deny list in order for VirtualBox to run correctly. First, find out the name of the hypervisor. ```shell-session $ lsmod | grep kvm kvm_intel 204800 6 kvm 593920 1 kvm_intel irqbypass 16384 1 kvm ``` Use the `blacklist` command to add the hypervisor to your denylist. ```shell-session $ echo 'blacklist kvm-intel' >> /etc/modprobe.d/blacklist.conf ``` Restart your machine and try the `vagrant` command again. ### Windows, VirtualBox, and Hyper-V If you encounter an issue with Windows, you will get a blue screen if you attempt to bring up a VirtualBox VM with Hyper-V enabled. If you wish to use VirtualBox on Windows, you must ensure that Hyper-V is not enabled on Windows. You can turn off the feature with the following Powershell command for Windows 10. ```powershell Disable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All ``` For Windows 11, you can use an elevated Powershell. ```powershell bcdedit /set hypervisorlaunchtype off ``` You can also disable Hyper-V in the Windows system settings. - Right click on the Windows button and select ‘Apps and Features’. - Select Turn Windows Features on or off. - Unselect Hyper-V and click OK. You might have to reboot your machine for the changes to take effect. More information about Hyper-V can be read [here](https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v). ================================================ FILE: website/content/docs/installation/source.mdx ================================================ --- layout: docs page_title: Installing Vagrant from Source description: |- Vagrant installations from source is an advanced operation. It is only recommended if you cannot use the official installer. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Install Vagrant from source Vagrant installations from source is an advanced operation. We only recommended if you cannot use the official installer. This page details the prerequisites and three steps to install Vagrant from source. 1. Install Ruby 1. Clone Vagrant 1. Configure locally ## Install Ruby To develop and build Vagrant, you must install a specific version of Ruby. The specific Ruby version that you will need is documented in `vagrant.gemspec`, located in the repository on GitHub. It contains the most up-to-date requirements. You will also need to be aware of plugin compatibility between Vagrant source installations and package based installations. Since Vagrant plugins are configured based on the current environment, plugins installed with Vagrant from source will not work with the package based Vagrant installation. ## Clone Vagrant Clone Vagrant's repository from GitHub into the directory where you keep code on your machine. ```shell-session $ git clone https://github.com/hashicorp/vagrant.git ``` Next, `cd` into that path. You must initiate all commands from this directory. ```shell-session $ cd /path/to/your/vagrant/clone ``` Use the `bundle` command with a required version\* to install the requirements. ```shell-session $ bundle install ``` You can now start Vagrant with the `bundle` command. ```shell-session $ bundle exec vagrant ``` ## Configure locally In order to use your locally-installed version of Vagrant in other projects, you will need to create a binstub and add it to your path. First, use the `bundle` command from your Vagrant directory. ```shell-session $ bundle --binstubs exec ``` This will generate files in `exec/`, including `vagrant`. You can now specify the full path to the `exec/vagrant` anywhere on your operating system. ```shell-session $ /path/to/vagrant/exec/vagrant init -m hashicorp/bionic64 ``` -> **Tip:** Warning messages are expected when you use a local Vagrant from source installation, since it is not supported or recommended. If you do not want to specify the full path to Vagrant (i.e. you just want to run `vagrant`), you can create a symbolic link to your exec. ```shell-session $ ln -sf /path/to/vagrant/exec/vagrant /usr/local/bin/vagrant ``` When you want to switch back to the official Vagrant version, remove the symlink. ================================================ FILE: website/content/docs/installation/uninstallation.mdx ================================================ --- layout: docs page_title: Uninstalling Vagrant description: |- Uninstalling Vagrant is easy and straightforward. You can either uninstall the Vagrant binary, the user data, or both. The sections below cover how to do this on every platform. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Uninstall Vagrant To uninstall Vagrant, you can either uninstall the Vagrant binary, the user data, or both. The two sections below detail how to uninstall Vagrant on any platform. ## Remove the Vagrant program When you remove the Vagrant program, it will remove the `vagrant` binary and all dependencies from your machine. It will _not_ remove user data. The next section provides more details on how to remove that directory from your system. ### Windows machines Uninstall using the **Add/Remove Programs** section of the control panel. ### Mac OS X machines As a super user, force remove the directory and remove it from your path with `pkgutil`.\ ```shell-session sudo rm -rf /opt/vagrant /usr/local/bin/vagrant sudo pkgutil --forget com.vagrant.vagrant ``` ### Linux machines As a super user, force remove the Vagrant directories. ```shell-session rm -rf /opt/vagrant rm -f /usr/bin/vagrant ``` ## Remove user data The removal of user data will remove all [boxes](/vagrant/docs/boxes), [plugins](/vagrant/docs/plugins), license files, and any stored state that may be used by Vagrant. Removing the user data effectively makes Vagrant think it is a fresh install. On Linux and Mac OS platforms, the user data directory location is in the root of your home directory under `vagrant.d`. Remove the `~/.vagrant.d` directory to delete all the user data. On Windows, this directory is, `C:\Users\YourUsername\.vagrant.d`, where `YourUsername` is the username of your local user. If the Vagrant support team asks you to remove this directory to debug, you should make a backup. When you use Vagrant again, Vagrant will automatically regenerate any data necessary to operate. ## Reinstall Vagrant If you decide to reinstall Vagrant, you can follow the [installation docs](/vagrant/docs/installation) again for any standard method. ================================================ FILE: website/content/docs/installation/upgrading-from-1-0.mdx ================================================ --- layout: docs page_title: Upgrade from Vagrant 1.0 description: |- The upgrade process from 1.0.x to 1.x is straightforward, Vagrant is backwards compatible with Vagrant 1.0.x. To reinstall Vagrant over your previous installation, download the latest package and install it with standard procedures for your operating system. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Upgrade from Vagrant 1.0.x The upgrade process from 1.0.x to 1.x is straightforward. Vagrant is quite [backwards compatible](/vagrant/docs/installation/backwards-compatibility) with Vagrant 1.0.x, so you can simply reinstall Vagrant over your previous installation by downloading the latest package and installing it using standard procedures for your operating system. As the [backwards compatibility](/vagrant/docs/installation/backwards-compatibility) page says, **Vagrant 1.0.x plugins will not work with Vagrant 1.1+**. Many of these plugins have been updated to work with newer versions of Vagrant, so you will need to verify if they've been updated. If not however, you will have to remove them before the upgrade. We recommend that you remove _all_ plugins before the upgrade and then slowly add them back. This usually makes for a smoother upgrade process. ~> **If your version of Vagrant was installed via Rubygems**, you must uninstall the old version prior to installing the package for the new version of Vagrant. The Rubygems installation is no longer supported. ================================================ FILE: website/content/docs/installation/upgrading.mdx ================================================ --- layout: docs page_title: Upgrade Vagrant description: |- This page details the general process to upgrade Vagrant for the 1.x.x series. If you need to upgrade from Vagrant 1.0.x, read the specific page dedicated to that. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Upgrade Vagrant This page details how to upgrade Vagrant in the 1.x.x series. ~> If you need to upgrade from Vagrant 1.0.x, read the [specific page dedicated to that](/vagrant/docs/installation/upgrading-from-1-0). Vagrant upgrades during the 1.x.x release series are straightforward: 1. [Download](/vagrant/downloads) the new package 2. Install it over the existing package The installers will properly overwrite and remove old files. It is recommended that no other Vagrant processes are running during the upgrade process. ## Vagrantfile compatibility with 3.0 Note that Vagrantfile stability for the new Vagrantfile syntax is not guaranteed until Vagrant 3.0. While Vagrantfiles made for 1.0.x will [continue to work](/vagrant/docs/installation/backwards-compatibility), newer Vagrantfiles may have backwards incompatible changes until 3.0. ## Issue reports If you encounter any problems at upgrade time, [report them as an issue in Github](https://github.com/hashicorp/vagrant/issues). Upgrades are meant to be a smooth process and we consider it a bug if it was not. ================================================ FILE: website/content/docs/multi-machine.mdx ================================================ --- layout: docs page_title: Multi-Machine description: |- Vagrant is able to define and control multiple guest machines per Vagrantfile. This is known as a "multi-machine" environment. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Multi-Machine Vagrant is able to define and control multiple guest machines per Vagrantfile. This is known as a "multi-machine" environment. These machines are generally able to work together or are somehow associated with each other. Here are some use-cases people are using multi-machine environments for today: - Accurately modeling a multi-server production topology, such as separating a web and database server. - Modeling a distributed system and how they interact with each other. - Testing an interface, such as an API to a service component. - Disaster-case testing: machines dying, network partitions, slow networks, inconsistent world views, etc. Historically, running complex environments such as these was done by flattening them onto a single machine. The problem with that is that it is an inaccurate model of the production setup, which can behave far differently. Using the multi-machine feature of Vagrant, these environments can be modeled in the context of a single Vagrant environment without losing any of the benefits of Vagrant. ## Defining Multiple Machines Multiple machines are defined within the same project [Vagrantfile](/vagrant/docs/vagrantfile/) using the `config.vm.define` method call. This configuration directive is a little funny, because it creates a Vagrant configuration within a configuration. An example shows this best: ```ruby Vagrant.configure("2") do |config| config.vm.provision "shell", inline: "echo Hello" config.vm.define "web" do |web| web.vm.box = "apache" end config.vm.define "db" do |db| db.vm.box = "mysql" end end ``` As you can see, `config.vm.define` takes a block with another variable. This variable, such as `web` above, is the _exact_ same as the `config` variable, except any configuration of the inner variable applies only to the machine being defined. Therefore, any configuration on `web` will only affect the `web` machine. And importantly, you can continue to use the `config` object as well. The configuration object is loaded and merged before the machine-specific configuration, just like other Vagrantfiles within the [Vagrantfile load order](/vagrant/docs/vagrantfile/#load-order). If you are familiar with programming, this is similar to how languages have different variable scopes. When using these scopes, order of execution for things such as provisioners becomes important. Vagrant enforces ordering outside-in, in the order listed in the Vagrantfile. For example, with the Vagrantfile below: ```ruby Vagrant.configure("2") do |config| config.vm.provision :shell, inline: "echo A" config.vm.define :testing do |test| test.vm.provision :shell, inline: "echo B" end config.vm.provision :shell, inline: "echo C" end ``` The provisioners in this case will output "A", then "C", then "B". Notice that "B" is last. That is because the ordering is outside-in, in the order of the file. If you want to apply a slightly different configuration to multiple machines, see [this tip](/vagrant/docs/vagrantfile/tips#loop-over-vm-definitions). ## Controlling Multiple Machines The moment more than one machine is defined within a Vagrantfile, the usage of the various `vagrant` commands changes slightly. The change should be mostly intuitive. Commands that only make sense to target a single machine, such as `vagrant ssh`, now _require_ the name of the machine to control. Using the example above, you would say `vagrant ssh web` or `vagrant ssh db`. Other commands, such as `vagrant up`, operate on _every_ machine by default. So if you ran `vagrant up`, Vagrant would bring up both the web and DB machine. You could also optionally be specific and say `vagrant up web` or `vagrant up db`. Additionally, you can specify a regular expression for matching only certain machines. This is useful in some cases where you specify many similar machines, for example if you are testing a distributed service you may have a `leader` machine as well as a `follower0`, `follower1`, `follower2`, etc. If you want to bring up all the followers but not the leader, you can just do `vagrant up /follower[0-9]/`. If Vagrant sees a machine name within forward slashes, it assumes you are using a regular expression. ## Communication Between Machines In order to facilitate communication within machines in a multi-machine setup, the various [networking](/vagrant/docs/networking/) options should be used. In particular, the [private network](/vagrant/docs/networking/private_network) can be used to make a private network between multiple machines and the host. ## Specifying a Primary Machine You can also specify a _primary machine_. The primary machine will be the default machine used when a specific machine in a multi-machine environment is not specified. To specify a default machine, just mark it primary when defining it. Only one primary machine may be specified. ```ruby config.vm.define "web", primary: true do |web| # ... end ``` ## Autostart Machines By default in a multi-machine environment, `vagrant up` will start all of the defined machines. The `autostart` setting allows you to tell Vagrant to _not_ start specific machines. Example: ```ruby config.vm.define "web" config.vm.define "db" config.vm.define "db_follower", autostart: false ``` When running `vagrant up` with the settings above, Vagrant will automatically start the "web" and "db" machines, but will not start the "db_follower" machine. You can manually force the "db_follower" machine to start by running `vagrant up db_follower`. ================================================ FILE: website/content/docs/networking/basic_usage.mdx ================================================ --- layout: docs page_title: Basic Usage - Networking description: |- Vagrant offers multiple options for how you are able to connect your guest machines to the network, but there is a standard usage pattern as well as some points common to all network configurations that are important to know. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Basic Usage of Networking Vagrant offers multiple options for how you are able to connect your guest machines to the network, but there is a standard usage pattern as well as some points common to all network configurations that are important to know. ## Configuration All networks are configured within your [Vagrantfile](/vagrant/docs/vagrantfile/) using the `config.vm.network` method call. For example, the Vagrantfile below defines some port forwarding: ```ruby Vagrant.configure("2") do |config| # ... config.vm.network "forwarded_port", guest: 80, host: 8080 end ``` Every network type has an identifier such as `"forwarded_port"` in the above example. Following this is a set of configuration arguments that can differ for each network type. In the case of forwarded ports, two numeric arguments are expected: the port on the guest followed by the port on the host that the guest port can be accessed by. ## Multiple Networks Multiple networks can be defined by having multiple `config.vm.network` calls within the Vagrantfile. The exact meaning of this can differ for each [provider](/vagrant/docs/providers/), but in general the order specifies the order in which the networks are enabled. ## Enabling Networks Networks are automatically configured and enabled after they've been defined in the Vagrantfile as part of the `vagrant up` or `vagrant reload` process. ## Setting Hostname A hostname may be defined for a Vagrant VM using the `config.vm.hostname` setting. By default, this will modify `/etc/hosts`, adding the hostname on a loopback interface that is not in use. For example: ```ruby Vagrant.configure("2") do |config| # ... config.vm.hostname = "myhost.local" end ``` will add the entry `127.0.X.1 myhost myhost.local` to `/etc/hosts`. A public or private network with an assigned IP may be flagged for hostname. In this case, the hostname will be added to the flagged network. Note, that if there are multiple networks only one may be flagged for hostname. For example: ```ruby Vagrant.configure("2") do |config| # ... config.vm.hostname = "myhost.local" config.vm.network "public_network", ip: "192.168.0.1", hostname: true config.vm.network "public_network", ip: "192.168.0.2" end ``` will add the entry `192.168.0.1 myhost myhost.local` to `/etc/hosts`. ================================================ FILE: website/content/docs/networking/forwarded_ports.mdx ================================================ --- layout: docs page_title: Forwarded Ports - Networking description: |- Vagrant forwarded ports allow you to access a port on your host machine and have all data forwarded to a port on the guest machine, over either TCP or UDP. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Forwarded Ports **Network identifier: `forwarded_port`** Vagrant forwarded ports allow you to access a port on your host machine and have all data forwarded to a port on the guest machine, over either TCP or UDP. For example: If the guest machine is running a web server listening on port 80, you can make a forwarded port mapping to port 8080 (or anything) on your host machine. You can then open your browser to `localhost:8080` and browse the website, while all actual network data is being sent to the guest. ## Defining a Forwarded Port The forwarded port configuration expects two parameters, the port on the guest and the port on the host. Example: ```ruby Vagrant.configure("2") do |config| config.vm.network "forwarded_port", guest: 80, host: 8080 end ``` This will allow accessing port 80 on the guest via port 8080 on the host. For most providers, forwarded ports by default bind to all interfaces. This means that other devices on your network can access the forwarded ports. If you want to restrict access, see the `guest_ip` and `host_ip` settings below. ## Options Reference This is a complete list of the options that are available for forwarded ports. Only the `guest` and `host` options are required. Below this section, there are more detailed examples of using these options. - `auto_correct` (boolean) - If true, the host port will be changed automatically in case it collides with a port already in use. By default, this is false. - `guest` (int) - The port on the guest that you want to be exposed on the host. This can be any port. - `guest_ip` (string) - The guest IP to bind the forwarded port to. If this is not set, the port will go to every IP interface. By default, this is empty. - `host` (int) - The port on the host that you want to use to access the port on the guest. This must be greater than port 1024 unless Vagrant is running as root (which is not recommended). - `host_ip` (string) - The IP on the host you want to bind the forwarded port to. If not specified, it will be bound to every IP. By default, this is empty. - `protocol` (string) - Either "udp" or "tcp". This specifies the protocol that will be allowed through the forwarded port. By default this is "tcp". - `id` (string) - Name of the rule (can be visible in VirtualBox). By default this is "protocol""guest" (example : "tcp123"). ## Forwarded Port Protocols By default, any defined port will only forward the TCP protocol. As an optional third parameter, you may specify `protocol: 'udp'` in order to pass UDP traffic. If a given port needs to be able to listen to the same port on both protocols, you must define the port twice with each protocol specified, like so: ```ruby Vagrant.configure("2") do |config| config.vm.network "forwarded_port", guest: 2003, host: 12003, protocol: "tcp" config.vm.network "forwarded_port", guest: 2003, host: 12003, protocol: "udp" end ``` ## Port Collisions and Correction It is common when running multiple Vagrant machines to unknowingly create forwarded port definitions that collide with each other (two separate Vagrant projects forwarded to port 8080, for example). Vagrant includes built-in mechanism to detect this and correct it, automatically. Port collision detection is always done. Vagrant will not allow you to define a forwarded port where the port on the host appears to be accepting traffic or connections. Port collision auto-correction must be manually enabled for each forwarded port, since it is often surprising when it occurs and can lead the Vagrant user to think that the port was not properly forwarded. Enabling auto correct is easy: ```ruby Vagrant.configure("2") do |config| config.vm.network "forwarded_port", guest: 80, host: 8080, auto_correct: true end ``` The final `:auto_correct` parameter set to true tells Vagrant to auto correct any collisions. During a `vagrant up` or `vagrant reload`, Vagrant will output information about any collisions detections and auto corrections made, so you can take notice and act accordingly. You can define allowed port range assignable by Vagrant when port collision is detected via [config.vm.usable_port_range](/vagrant/docs/vagrantfile/machine_settings) property. ```ruby Vagrant.configure("2") do |config| config.vm.usable_port_range = 8000..8999 end ``` ================================================ FILE: website/content/docs/networking/index.mdx ================================================ --- layout: docs page_title: Networking description: |- In order to access the Vagrant environment created, Vagrant exposes some high-level networking options for things such as forwarded ports, connecting to a public network, or creating a private network. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Networking In order to access the Vagrant environment created, Vagrant exposes some high-level networking options for things such as forwarded ports, connecting to a public network, or creating a private network. The high-level networking options are meant to define an abstraction that works across multiple [providers](/vagrant/docs/providers/). This means that you can take your Vagrantfile you used to spin up a VirtualBox machine and you can reasonably expect that Vagrantfile to behave the same with something like VMware. You should first read the [basic usage](/vagrant/docs/networking/basic_usage) page and then continue by reading the documentation for a specific networking primitive by following the navigation to the left. ## Advanced Configuration In some cases, these options are _too_ high-level, and you may want to more finely tune and configure the network interfaces of the underlying machine. Most providers expose [provider-specific configuration](/vagrant/docs/providers/configuration) to do this, so please read the documentation for your specific provider to see what options are available. -> **For beginners:** It is strongly recommended you use only the high-level networking options until you are comfortable with the Vagrant workflow and have things working at a basic level. Provider-specific network configuration can very quickly lock you out of your guest machine if improperly done. ## Networking Assumptions ### There is a NAT available Vagrant assumes there is an available NAT device on eth0. This ensures that Vagrant always has a way of communicating with the guest machine. It is possible to change this manually (outside of Vagrant), however, this may lead to inconsistent behavior. Providers might have additional assumptions. For example, in VirtualBox, this assumption means that network adapter 1 is a NAT device. ================================================ FILE: website/content/docs/networking/private_network.mdx ================================================ --- layout: docs page_title: Private Networks - Networking description: |- Vagrant private networks allow you to access your guest machine by some address that is not publicly accessible from the global internet. In general, this means your machine gets an address in the private address space. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Private Networks **Network identifier: `private_network`** Vagrant private networks allow you to access your guest machine by some address that is not publicly accessible from the global internet. In general, this means your machine gets an address in the [private address space](https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces). Multiple machines within the same private network (also usually with the restriction that they're backed by the same [provider](/vagrant/docs/providers/)) can communicate with each other on private networks. -> **Guest operating system support.** Private networks generally require configuring the network adapters on the guest machine. This process varies from OS to OS. Vagrant ships with knowledge of how to configure networks on a variety of guest operating systems, but it is possible if you are using a particularly old or new operating system that private networks will not properly configure. ## DHCP The easiest way to use a private network is to allow the IP to be assigned via DHCP. ```ruby Vagrant.configure("2") do |config| config.vm.network "private_network", type: "dhcp" end ``` This will automatically assign an IP address from the reserved address space. The IP address 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` or `ip addr show`. ## Static IP You can also specify a static IP address for the machine. This lets you access the Vagrant managed machine using a static, known IP. The Vagrantfile for a static IP looks like this: ```ruby Vagrant.configure("2") do |config| config.vm.network "private_network", ip: "192.168.50.4" end ``` It is up to the users to make sure that the static IP does not collide with any other machines on the same network. While you can choose any IP you would like, you _should_ use an IP from the [reserved private address space](https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces). These IPs are guaranteed to never be publicly routable, and most routers actually block traffic from going to them from the outside world. For some operating systems, additional configuration options for the static IP address are available such as setting the default gateway or MTU. ~> **Warning!** Do not choose an IP that overlaps with any other IP space on your system. This can cause the network to not be 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 can disable Vagrant's auto-configure feature by specifying `auto_config`: ```ruby Vagrant.configure("2") do |config| config.vm.network "private_network", ip: "192.168.50.4", auto_config: false end ``` If you already started the Vagrant environment before setting `auto_config`, the files it initially placed there will stay there. You will have to remove those files manually or destroy and recreate the machine. The files created by Vagrant depend on the OS. For example, for many Linux distros, this is `/etc/network/interfaces`. In general you should look in the normal location that network interfaces are configured for your distro. ================================================ FILE: website/content/docs/networking/public_network.mdx ================================================ --- layout: docs page_title: Public Networks - Networking description: |- Vagrant public networks are less private than private networks, and the exact meaning actually varies from provider to provider, hence the ambiguous definition. The idea is that while private networks should never allow the general public access to your machine, public networks can. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Public Networks **Network identifier: `public_network`** Vagrant public networks are less private than private networks, and the exact meaning actually varies from [provider to provider](/vagrant/docs/providers/), hence the ambiguous definition. The idea is that while [private networks](/vagrant/docs/networking/private_network) should never allow the general public access to your machine, public networks can. -> **Confused?** We kind of are, too. It is likely that public networks will be replaced by `:bridged` in a future release, since that is in general what should be done with public networks, and providers that do not support bridging generally do not have any other features that map to public networks either. ~> **Warning!** Vagrant boxes are insecure by default and by design, featuring public passwords, insecure keypairs for SSH access, and potentially allow root access over SSH. With these known credentials, your box is easily accessible by anyone on your network. Before configuring Vagrant to use a public network, consider _all_ potential security implications and review the [default box configuration](/vagrant/docs/boxes/base) to identify potential security risks. ## DHCP The easiest way to use a public network is to allow the IP to be assigned via DHCP. In this case, defining a public network is trivially easy: ```ruby Vagrant.configure("2") do |config| config.vm.network "public_network" end ``` 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 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 end ``` ## Static IP Depending on your setup, you may wish to manually set the IP of your bridged interface. To do so, add a `:ip` clause to the network definition. ```ruby config.vm.network "public_network", ip: "192.168.0.17" ``` ## Default Network Interface If more than one network interface is available on the host machine, Vagrant will ask you to choose which interface the virtual machine should bridge to. A default interface can be specified by adding a `:bridge` clause to the network definition. ```ruby config.vm.network "public_network", bridge: "en1: Wi-Fi (AirPort)" ``` The string identifying the desired interface must exactly match the name of an available interface. If it cannot be found, Vagrant will ask you to pick from a list of available network interfaces. With some providers, it is possible to specify a list of adapters to bridge against: ```ruby config.vm.network "public_network", bridge: [ "en1: Wi-Fi (AirPort)", "en6: Broadcom NetXtreme Gigabit Ethernet Controller", ] ``` In this example, the first network adapter that exists and can successfully be bridge will be used. ## Disable Auto-Configuration If you want to manually configure the network interface yourself, you can disable auto-configuration by specifying `auto_config`: ```ruby Vagrant.configure("2") do |config| config.vm.network "public_network", auto_config: false end ``` Then the shell provisioner can be used to configure the ip of the interface: ```ruby Vagrant.configure("2") do |config| config.vm.network "public_network", auto_config: false # manual ip config.vm.provision "shell", run: "always", inline: "ifconfig eth1 192.168.0.17 netmask 255.255.255.0 up" # manual ipv6 config.vm.provision "shell", run: "always", inline: "ifconfig eth1 inet6 add fc00::17/7" end ``` ## Default Router Depending on your setup, you may wish to manually override the default router configuration. This is required if you need to access the Vagrant box from other networks over the public network. To do so, you can use a shell provisioner script: ```ruby Vagrant.configure("2") do |config| config.vm.network "public_network", ip: "192.168.0.17" # default router config.vm.provision "shell", run: "always", inline: "route add default gw 192.168.0.1" # default router ipv6 config.vm.provision "shell", run: "always", inline: "route -A inet6 add default gw fc00::1 eth1" # delete default gw on eth0 config.vm.provision "shell", run: "always", inline: "eval `route -n | awk '{ if ($8 ==\"eth0\" && $2 != \"0.0.0.0\") print \"route del default gw \" $2; }'`" end ``` Or, an alternative, simpler version, assuming you get DHCP from your public network: ```ruby Vagrant.configure("2") do |config| config.vm.network "public_network" # default router config.vm.provision "shell", run: "always", inline: "ip route del default via 10.0.2.2 || true" end ``` Note the above are fairly complex and will be guest OS specific, but we document the rough idea of how to do it because it is a common question. ================================================ FILE: website/content/docs/other/debugging.mdx ================================================ --- layout: docs page_title: Debugging and Troubleshooting description: |- As much as we try to keep Vagrant stable and bug free, it is inevitable that issues will arise and Vagrant will behave in unexpected ways. In these cases, Vagrant has amazing support channels available to assist you. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Debugging As much as we try to keep Vagrant stable and bug free, it is inevitable that issues will arise and Vagrant will behave in unexpected ways. When using these support channels, it is generally helpful to include debugging logs along with any error reports. These logs can often help you troubleshoot any problems you may be having. !> **Scan for sensitive information!** Vagrant debug logs include information about your system including environment variables and user information. If you store sensitive information in the environment or in your user account, please scan or scrub the debug log of this information before uploading the contents to the public Internet. ~> **Submit debug logs using GitHub Gist.** If you plan on submitting a bug report or issue that includes debug-level logs, please use a service like [Gist](https://gist.github.com). **Do not** paste the raw debug logs into an issue as it makes it very difficult to scroll and parse the information. To enable detailed logging, set the `VAGRANT_LOG` environmental variable to the desired log level name, which is one of `debug` (loud), `info` (normal), `warn` (quiet), and `error` (very quiet). When asking for support, please set this to `debug`. When troubleshooting your own issues, you should start with `info`, which is much quieter, but contains important information about the behavior of Vagrant. On Linux and Mac systems, this can be done by prepending the `vagrant` command with an environmental variable declaration: ```shell-session $ VAGRANT_LOG=info vagrant up ``` On Windows, multiple steps are required: ```shell-session $ set VAGRANT_LOG=info $ vagrant up ``` You can also get the debug level output using the `--debug` command line option. For example: ```shell-session $ vagrant up --debug ``` On Linux and Mac, if you are saving the output to a file, you may need to redirect stderr and stdout using `&>`: ```shell-session $ vagrant up --debug &> vagrant.log ``` On Windows in PowerShell (outputs to log and screen): ```shell-session $ vagrant up --debug 2>&1 | Tee-Object -FilePath ".\vagrant.log" ``` ================================================ FILE: website/content/docs/other/environmental-variables.mdx ================================================ --- layout: docs page_title: Environmental Variables description: |- Vagrant has a set of environmental variables that can be used to configure and control it in a global way. This page lists those environmental variables. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Environmental Variables Vagrant has a set of environmental variables that can be used to configure and control it in a global way. This page lists those environmental variables. ## `CFLAGS` If set the contents of this environment variable will be appended to the value generated by the Vagrant launcher. ## `CPPFLAGS` If set the contents of this environment variable will be appended to the value generated by the Vagrant launcher. ## `CURL_CA_BUNDLE` If set this environment variable will be passed through to the Vagrant process. By default Vagrant will use the CA certificate included with the Vagrant installation. ## `LDFLAGS` If set the contents of this environment variable will be appended to the value generated by the Vagrant launcher. ## `SSL_CERT_FILE` If set this environment variable will be passed through to the Vagrant process. By default Vagrant will use the CA certificate included with the Vagrant installation. ## `VAGRANT_ALIAS_FILE` `VAGRANT_ALIAS_FILE` can be set to change the file where Vagrant aliases are defined. By default, this is set to `~/.vagrant.d/aliases`. ## `VAGRANT_ALLOW_PLUGIN_SOURCE_ERRORS` If this is set to any value, then Vagrant will not error when a configured plugin source is unavailable. When installing a Vagrant plugin Vagrant will error and halt if a plugin source is inaccessible. In some cases it may be desirable to ignore inaccessible sources and continue with the plugin installation. Enabling this value will cause Vagrant to simply log the plugin source error and continue. ## `VAGRANT_BOX_UPDATE_CHECK_DISABLE` By default, Vagrant will query the metadata API server to see if a newer box version is available for download. This optional can be disabled on a per-Vagrantfile basis with `config.vm.box_check_update`, but it can also be disabled globally setting `VAGRANT_BOX_UPDATE_CHECK_DISABLE` to any non-empty value. This option will not affect global box functions like `vagrant box update`. ## `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 suppress these 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 default, Vagrant uses the current directory you are in. The working directory is important because it is where Vagrant looks for the Vagrantfile. It also defines how relative paths in the Vagrantfile are expanded, since they're expanded relative to where the Vagrantfile is found. This environmental variable is most commonly set when running Vagrant from a scripting environment in order to set the directory that Vagrant sees. ## `VAGRANT_DEBUG_LAUNCHER` For performance reasons, especially for Windows users, Vagrant uses a static binary to launch the actual Vagrant process. If you have _very_ early issues 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_DEFAULT_PROVIDER` This configures the default provider Vagrant will use. This normally does not need to be set since Vagrant is fairly intelligent about how to detect the default provider. By setting this, you will force Vagrant to use this provider for any _new_ Vagrant environments. Existing Vagrant environments will continue to use the provider they came `up` with. Once you `vagrant destroy` existing environments, this will take effect. ## `VAGRANT_DEFAULT_TEMPLATE` This configures the template used by `vagrant init` when the `--template` option is not provided. ## `VAGRANT_DETECTED_ARCH` This environment variable may be set by the Vagrant launcher to help determine the current runtime architecture in use. In general Vagrant will set this value when running on a Windows host using a cygwin or msys based shell. The value the Vagrant launcher may set in this environment variable will not always match the actual architecture of the platform itself. Instead it signifies the detected architecture of the environment it is running within. If this value is set, the Vagrant launcher will not modify it. ## `VAGRANT_DETECTED_OS` This environment variable may be set by the Vagrant launcher to help determine the current runtime platform. In general Vagrant will set this value when running on a Windows host using a cygwin or msys based shell. If this value is set, the Vagrant launcher will not modify it. ## `VAGRANT_DISABLE_RESOLV_REPLACE` Vagrant can optionally use the Ruby Resolv library in place of the libc resolver. This can be disabled setting this environment variable. ## `VAGRANT_DISABLE_VBOXSYMLINKCREATE` If set, this will completely disable the ability to create symlinks with all VirtualBox shared folders. If this environment variable is not set, the VirtualBox synced folders option `SharedFoldersEnableSymlinksCreate` will be enabled by default. This option can be overridden on a per-folder basis within your Vagrantfile config by setting the `SharedFoldersEnableSymlinksCreate` option to true if you do not wish to completely disable this feature for all VirtualBox guests. More information on the option can be read in the [VirtualBox synced folders docs page.](/vagrant/docs/synced-folders/virtualbox#sharedfoldersenablesymlinkscreate) ## `VAGRANT_DISABLE_SMBMFSYMLINKS` If set, this will disable the `mfsymlinks` option for mounting SMB filesystems. If not set, then the `mfsymlinks` option will be enabled by default. This option can be overriden on a pre-folder basis with your Vagrantfile by setting `mount_options: ['mfsymlinks']`. ## `VAGRANT_DOTFILE_PATH` `VAGRANT_DOTFILE_PATH` can be set to change the directory where Vagrant stores VM-specific state, such as the VirtualBox VM UUID. By default, this is set to `.vagrant`. If you keep your Vagrantfile in a Dropbox folder in order to share the folder between your desktop and laptop (for example), Vagrant will overwrite the files in this directory with the details of the VM on the most recently-used host. To avoid this, you could set `VAGRANT_DOTFILE_PATH` to `.vagrant-laptop` and `.vagrant-desktop` on the respective machines. (Remember to update your `.gitignore`!) ## `VAGRANT_ENABLE_RESOLV_REPLACE` Use the Ruby Resolv library in place of the libc resolver. ## `VAGRANT_FORCE_COLOR` If this is set to any value, then Vagrant will force colored output, even if it detected that there is no TTY or the current environment does not support it. The equivalent behavior can be achieved by using the `--color` flag on a command-by-command basis. This environmental variable is useful for setting this flag globally. ## `VAGRANT_HOME` `VAGRANT_HOME` can be set to change the directory where Vagrant stores global state. By default, this is set to `~/.vagrant.d`. The Vagrant home directory is where things such as boxes are stored, so it can actually become quite large on disk. ## `VAGRANT_IGNORE_WINRM_PLUGIN` Vagrant will not display warning when `vagrant-winrm` plugin is installed. ## `VAGRANT_INSTALL_LOCAL_PLUGINS` If this is set to any value, Vagrant will not prompt for confirmation prior to installing local plugins which have been defined within the local Vagrantfile. ## `VAGRANT_IS_HYPERV_ADMIN` Disable Vagrant's check for Hyper-V admin privileges and allow Vagrant to assume the current user has full access to Hyper-V. This is useful if the internal privilege check incorrectly determines the current user does not have access to Hyper-V. ## `VAGRANT_LOCAL_PLUGINS_LOAD` If this is set Vagrant will not stub the Vagrantfile when running `vagrant plugin` commands. When this environment variable is set the `--local` flag will not be required by `vagrant plugin` commands to enable local project plugins. ## `VAGRANT_LOG` `VAGRANT_LOG` specifies the verbosity of log messages from Vagrant. By default, Vagrant does not actively show any log messages. Log messages are very useful when troubleshooting issues, reporting bugs, or getting support. At the most verbose level, Vagrant outputs basically everything it is doing. Available log levels are "debug," "info," "warn," and "error." Both "warn" and "error" are practically useless since there are very few cases of these, and Vagrant generally reports them within the normal output. "info" is a good level to start with if you are having problems, because while it is much louder than normal output, it is still very human-readable and can help identify certain issues. "debug" output is _extremely_ verbose and can be difficult to read without some knowledge of Vagrant internals. It is the best output to attach to a support request or bug report, however. ## `VAGRANT_MAX_REBOOT_RETRY_DURATION` By default, Vagrant will wait up to 120 seconds for a machine to reboot. However, if you're finding your OS is taking longer than 120 seconds to reboot successfully, you can configure this environment variable and Vagrant will wait for the configured number of seconds. ## `VAGRANT_NO_COLOR` If this is set to any value, then Vagrant will not use any colorized output. This is useful if you are logging the output to a file or on a system that does not support colors. The equivalent behavior can be achieved by using the `--no-color` flag on a command-by-command basis. This environmental variable is useful for setting this flag globally. ## `VAGRANT_NO_PARALLEL` If this is set, Vagrant will not perform any parallel operations (such as parallel box provisioning). All operations will be performed in serial. ## `VAGRANT_NO_PLUGINS` If this is set to any value, then Vagrant will not load any 3rd party plugins. This is useful if you install a plugin and it is introducing instability to Vagrant, or if you want a specific Vagrant environment to not load plugins. Note that any `vagrant plugin` commands automatically do not load any plugins, so if you do install any unstable plugins, you can always use the `vagrant plugin` commands without having to worry. ## `VAGRANT_POWERSHELL_VERSION_DETECTION_TIMEOUT` Vagrant will use a default timeout when checking for the installed version of PowerShell. Occasionally the default can be too low and Vagrant will report being unable to detect the installed version of PowerShell. This environment variable can be used to extend the timeout used during PowerShell version detection. When setting this environment variable, its value will be in seconds. By default, it will use 30 seconds as a timeout. ## `VAGRANT_PREFERRED_POWERSHELL` When executing PowerShell commands, Vagrant will prefer to use `pwsh.exe` over `powershell.exe` by default. This environment variable can be used to modify this preference and make Vagrant prefer `powershell.exe`. The value set in this environment variable are any supported PowerShell executables which currently are: `powershell` and `pwsh`. ## `VAGRANT_PREFERRED_PROVIDERS` This configures providers that Vagrant should prefer. Much like the `VAGRANT_DEFAULT_PROVIDER` this environment variable normally does not need to be set. By setting this you will instruct Vagrant to _prefer_ providers defined in this environment variable for any _new_ Vagrant environments. Existing Vagrant environments will continue to use the provider they came `up` with. Once you `vagrant destroy` existing environments, this will take effect. A single provider can be defined within this environment variable or a comma delimited list of providers. ## `VAGRANT_PREFER_SYSTEM_BIN` If this is set, Vagrant will prefer using utility executables (like `ssh` and `rsync`) from the local system instead of those vendored within the Vagrant installation. Vagrant will default to using a system provided `ssh` on Windows. This environment variable can also be used to disable that behavior to force Vagrant to use the embedded `ssh` executable by setting it to `0`. ## `VAGRANT_SUPPRESS_GO_EXPERIMENTAL_WARNING` If this is set, Vagrant-go will not output a warning message about compatibility with Vagrant-ruby. This does not effect the stable Ruby release of Vagrant. ## `VAGRANT_DISABLE_WINCURL` If set Vagrant will use the mingw build of curl which uses the installer provided ca-certificates bundle instead of the native Windows curl executable. ## `VAGRANT_SERVER_URL` This configures the remote server which Vagrant will connect to for fetching Vagrant boxes. By default this is configured for Vagrant Cloud (https://vagrantcloud.com) ## `VAGRANT_SERVER_ACCESS_TOKEN_BY_URL` If this is set Vagrant will change the way it authenticates with the configured Vagrant server. When set, the authentication behavior will be reverted to the deprecated authentication behavior of: 1. not adding an authentication header to the request 2. setting the configured access token as a query parameter on URLs This behavior can be useful for third party servers which do not accept the authentication header currently used with Vagrant Cloud. ## `VAGRANT_SKIP_SUBPROCESS_JAILBREAK` As of Vagrant 1.7.3, Vagrant tries to intelligently detect if it is running in the installer or running via Bundler. Although not officially supported, Vagrant tries its best to work when executed via Bundler. When Vagrant detects that you have spawned a subprocess that lives outside of Vagrant's installer, Vagrant will do its best to reset the preserved environment during the subprocess execution. If Vagrant detects it is running outside of the officially installer, the original environment will always be restored. You can disable this automatic jailbreak by setting `VAGRANT_SKIP_SUBPROCESS_JAILBREAK`. ## `VAGRANT_USER_AGENT_PROVISIONAL_STRING` Vagrant will append the contents of this variable to the default user agent header. ## `VAGRANT_USE_VAGRANT_TRIGGERS` Vagrant will not display the warning about disabling the core trigger feature if the community plugin is installed. ## `VAGRANT_VAGRANTFILE` This specifies the filename of the Vagrantfile that Vagrant searches for. By default, this is "Vagrantfile". Note that this is _not_ a file path, but just a filename. This environmental variable is commonly used in scripting environments where a single folder may contain multiple Vagrantfiles representing different configurations. ## `VAGRANT_WINPTY_DISABLE` If this is set, Vagrant will _not_ wrap interactive processes with winpty where required. ================================================ FILE: website/content/docs/other/index.mdx ================================================ --- layout: docs page_title: Other description: |- This page covers Vagrant information that does not quite fit under the other categories. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Other This section covers other information that does not quite fit under the other categories. - [Debugging](/vagrant/docs/other/debugging) - [Environment Variables](/vagrant/docs/other/environmental-variables) ================================================ FILE: website/content/docs/other/macos-catalina.mdx ================================================ --- layout: docs page_title: Vagrant and macOS Catalina description: An overview of using Vagrant on macOS Catalina. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Vagrant and macOS Catalina The latest version of macOS (Catalina) includes security changes that prevent applications from accessing data in your Documents, Desktop, and Downloads folders without explicit permission. If you keep any virtual machine files in these folders, you will need to allow access to these folders for your terminal emulator. Initially when you try to access one of these folders from the command line, you should see a popup that says something like: > “Terminal” would like to access files in your Documents folder. Click "OK" to grant those permissions. If you click "Don't Allow" and find that you need to grant access later on, you can go to "System Preferences" -> "Security & Privacy" -> "Files and Folders" and you should see your terminal emulator there. Click on the lock, and then click on the checkbox next to the folder that contains the files that Vagrant needs to access. Note that granting the `vagrant` binary "Full Disk Access" is not sufficient or necessary. If Terminal (or iTerm2/Hyper/etc.) is granted access to a particular folder, then Vagrant will also be able to access that folder. ================================================ FILE: website/content/docs/other/wsl.mdx ================================================ --- layout: docs page_title: Vagrant and Windows Subsystem for Linux description: |- An overview of using Vagrant on Windows within the Windows Subsystem for Linux. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Vagrant and Windows Subsystem for Linux Recent versions of Windows 10 now include Windows Subsystem for Linux (WSL) as an optional Windows feature. The WSL supports running a Linux environment within Windows. Vagrant support for WSL is still in development and should be considered _beta_. ~> **Warning: Advanced Topic!** Using Vagrant within the Windows Subsystem for Linux is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Windows, WSL, and Linux should approach. ## Vagrant Installation Vagrant _must_ be installed within the Linux distribution used with WSL. While the `vagrant.exe` executable provided by the Vagrant Windows installation is accessible from within the WSL, it will not function as expected. Download the installer package for the Linux distribution from the releases page and install Vagrant. **NOTE: When Vagrant is installed on the Windows system the version installed within the Linux distribution _must_ match.** # Vagrant Usage ## Windows Access By default Vagrant will not access features available on the Windows system from within the WSL. This means the VirtualBox and Hyper-V providers will not be available. To enable Windows access, which will also enable the VirtualBox and Hyper-V providers, set the `VAGRANT_WSL_ENABLE_WINDOWS_ACCESS` environment variable: ```shell-session $ export VAGRANT_WSL_ENABLE_WINDOWS_ACCESS="1" ``` When Windows access is enabled Vagrant will automatically adjust `VAGRANT_HOME` to be located on the Windows host. This is required to ensure `VAGRANT_HOME` is located on a DrvFs file system. ## PATH modifications Vagrant will detect when it is being run within the WSL and adjust how it locates and executes third party executables. For example, when using the VirtualBox provider Vagrant will interact with VirtualBox installed on the Windows system, not within the WSL. It is important to ensure that any required Windows executable is available within your `PATH` to allow Vagrant to access them. For example, when using the VirtualBox provider: ```shell export PATH="$PATH:/mnt/c/Program Files/Oracle/VirtualBox" ``` ## Synced Folders Support for synced folders within the WSL is implementation dependent. In most cases synced folders will not be supported when running Vagrant within WSL on a VolFs file system. Synced folder implementations must "opt-in" to supporting usage from VolFs file systems. To use synced folders from within the WSL that do not support VolFs file systems, move the Vagrant project directory to a DrvFs file system location (/mnt/c/ prefixed path for example). ## Windows Access Working within the WSL provides a layer of isolation from the actual Windows system. In most cases Vagrant will need access to the actual Windows system to function correctly. As most Vagrant providers will need to be installed on Windows directly (not within the WSL) Vagrant will require Windows access. Access to the Windows system is controlled via an environment variable: `VAGRANT_WSL_ENABLE_WINDOWS_ACCESS`. If this environment variable is set, Vagrant will access the Windows system to run executables and enable things like synced folders. When running in a bash shell within WSL, the environment variable can be setup like so: ```shell-session $ export VAGRANT_WSL_ENABLE_WINDOWS_ACCESS="1" ``` This will enable Vagrant to access the Windows system outside of the WSL and properly interact with Windows executables. This will automatically modify the `VAGRANT_HOME` environment variable if it is not already defined, setting it to be within the user's home directory on Windows. It is important to note that paths shared with the Windows system will not have Linux permissions enforced. For example, when a directory within the WSL is synced to a guest using the VirtualBox provider, any local permissions defined on that directory (or its contents) will not be visible from the guest. Likewise, any files created from the guest within the synced folder will be world readable/writeable in WSL. Other useful WSL related environment variables: - `VAGRANT_WSL_WINDOWS_ACCESS_USER` - Override current Windows username - `VAGRANT_WSL_DISABLE_VAGRANT_HOME` - Do not modify the `VAGRANT_HOME` variable - `VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH` - Custom Windows system home path If a Vagrant project directory is not within the user's home directory on the Windows system, certain actions that include permission checks may fail (like `vagrant ssh`). When accessing Vagrant projects outside the WSL Vagrant will skip these permission checks when the project path is within the path defined in the `VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH` environment variable. For example, if a user wants to run a Vagrant project from the WSL that is located at `C:\TestDir\vagrant-project`: ```shell-session C:\Users\vagrant> cd C:\TestDir\vagrant-project C:\TestDir\vagrant-project> bash vagrant@vagrant-10:/mnt/c/TestDir/vagrant-project$ export VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH="/mnt/c/TestDir" vagrant@vagrant-10:/mnt/c/TestDir/vagrant-project$ vagrant ssh ``` ## Using Docker The docker daemon cannot be run inside the Windows Subsystem for Linux. However, the daemon _can_ be run on Windows and accessed by Vagrant while running in the WSL. Once docker is installed and running on Windows, export the following environment variable to give Vagrant access: ```shell-session vagrant@vagrant-10:/mnt/c/Users/vagrant$ export DOCKER_HOST=tcp://127.0.0.1:2375 ``` ================================================ FILE: website/content/docs/plugins/action-hooks.mdx ================================================ --- layout: docs page_title: Plugin Development Basics - Action Hooks description: |- Action hooks provide ways to interact with Vagrant at a very low level by injecting middleware in various phases of Vagrant's lifecycle. This is an advanced option, even for plugin development. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Action Hooks Action hooks provide ways to interact with Vagrant at a very low level by injecting middleware in various phases of Vagrant's lifecycle. This is an advanced option, even for plugin development. ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. ## Public Action Hooks The following action hooks are available in the core of Vagrant. Please note that this list is not exhaustive and additional hooks can be added via plugins. - `environment_plugins_loaded` - called after the plugins have been loaded, but before the configurations, provisioners, providers, etc. are loaded. * `environment_load` - called after the environment and all configurations are fully loaded. - `environment_unload` - called after the environment is done being used. The environment should not be used in this hook. * `machine_action_boot` - called after the hypervisor has reported the machine was booted. - `machine_action_config_validate` - called after all `Vagrantfile`s have been loaded, merged, and validated. * `machine_action_destroy` - called after the hypervisor has reported the virtual machine is down. - `machine_action_halt` - called after the hypervisor has moved the machine into a halted state (usually "stopped" but not "terminated"). * `machine_action_package` - called after Vagrant has successfully packaged a new box. - `machine_action_provision` - called after all provisioners have executed. * `machine_action_read_state` - called after Vagrant has loaded state from disk and the hypervisor. - `machine_action_reload` - called after a virtual machine is reloaded (varies by hypervisor). * `machine_action_resume` - called after a virtual machine is moved from the halted to up state. - `machine_action_run_command` - called after a command is executed on the machine. * `machine_action_ssh` - called after an SSH connection has been established. - `machine_action_ssh_run` - called after an SSH command is executed. * `machine_action_start` - called after the machine has been started. - `machine_action_suspend` - called after the machine has been suspended. * `machine_action_sync_folders` - called after synced folders have been set up. - `machine_action_up` - called after the machine has entered the up state. ## Private API You may find additional action hooks if you browse the Vagrant source code, but only the list of action hooks here are guaranteed to persist between Vagrant releases. Please do not rely on the internal API as it is subject to change without notice. ================================================ FILE: website/content/docs/plugins/commands.mdx ================================================ --- layout: docs page_title: Command Plugins - Plugin Development description: |- This page documents how to add new commands to Vagrant, invocable via "vagrant YOUR-COMMAND". Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development: Commands This page documents how to add new commands to Vagrant, invocable via `vagrant YOUR-COMMAND`. Prior to reading this, you should be familiar with the [plugin development basics](/vagrant/docs/plugins/development-basics). ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. ## Definition Component Within the context of a plugin definition, new commands can be defined like so: ```ruby command "foo" do require_relative "command" Command end ``` Commands are defined with the `command` method, which takes as an argument the name of the command, in this case "foo." This means the command will be invocable via `vagrant foo`. Then the block argument returns a class that implements the `Vagrant.plugin(2, "command")` interface. You can also define _non-primary commands_. These commands do not show up in the `vagrant -h` output. They only show up if the user explicitly does a `vagrant list-commands` which shows the full listing of available commands. This is useful for highly specific commands or plugins that a beginner to Vagrant would not be using anyways. Vagrant itself uses non-primary commands to expose some internal functions, as well. To define a non-primary command: ```ruby command("foo", primary: false) do require_relative "command" Command end ``` ## Implementation Implementations of commands should subclass `Vagrant.plugin(2, :command)`, which is a Vagrant method that will return the proper superclass for a version 2 command. The implementation itself is quite simple, since the class needs to only implement a single method: `execute`. Example: ```ruby class Command < Vagrant.plugin(2, :command) def execute puts "Hello!" 0 end end ``` The `execute` method is called when the command is invoked, and it should return the exit status (0 for success, anything else for error). This is a command at its simplest form. Of course, the command superclass gives you access to the Vagrant environment and provides some helpers to do common tasks such as command line parsing. ## Parsing Command-Line Options The `parse_options` method is available which will parse the command line for you. It takes an [OptionParser](http://ruby-doc.org/stdlib-1.9.3/libdoc/optparse/rdoc/OptionParser.html) as an argument, and adds some common elements to it such as the `--help` flag, automatically showing help if requested. View the API docs directly for more information. This is recommended over raw parsing/manipulation of command line flags. The following is an example of parsing command line flags pulled directly from the built-in Vagrant `destroy` command: ```ruby options = {} options[:force] = false opts = OptionParser.new do |o| o.banner = "Usage: vagrant destroy [vm-name]" o.separator "" o.on("-f", "--force", "Destroy without confirmation.") do |f| options[:force] = f end end # Parse the options argv = parse_options(opts) ``` ## Using Vagrant Machines The `with_target_vms` method is a helper that helps you interact with the machines that Vagrant manages in a standard Vagrant way. This method automatically does the right thing in the case of multi-machine environments, handling target machines on the command line (`vagrant foo my-vm`), etc. If you need to do any manipulation of a Vagrant machine, including SSH access, this helper should be used. An example of using the helper, again pulled directly from the built-in `destroy` command: ```ruby with_target_vms(argv, reverse: true) do |machine| machine.action(:destroy) end ``` In this case, it asks for the machines in reverse order and calls the destroy action on each of them. If a user says `vagrant destroy foo`, then the helper automatically only yields the `foo` machine. If no parameter is given and it is a multi-machine environment, every machine in the environment is yielded, and so on. It just does the right thing. ## Using the Raw Vagrant Environment The raw loaded `Vagrant::Environment` object is available with the '@env' instance variable. ================================================ FILE: website/content/docs/plugins/configuration.mdx ================================================ --- layout: docs page_title: Custom Configuration - Plugin Development description: |- This page documents how to add new configuration options to Vagrant, settable with "config.YOURKEY" in Vagrantfiles. Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development: Configuration This page documents how to add new configuration options to Vagrant, settable with `config.YOURKEY` in Vagrantfiles. Prior to reading this, you should be familiar with the [plugin development basics](/vagrant/docs/plugins/development-basics). ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. ## Definition Component Within the context of a plugin definition, new configuration keys can be defined like so: ```ruby config "foo" do require_relative "config" Config end ``` Configuration keys are defined with the `config` method, which takes as an argument the name of the configuration variable as the argument. This means that the configuration object will be accessible via `config.foo` in Vagrantfiles. Then, the block argument returns a class that implements the `Vagrant.plugin(2, :config)` interface. ## Implementation Implementations of configuration keys should subclass `Vagrant.plugin(2, :config)`, which is a Vagrant method that will return the proper subclass for a version 2 configuration section. The implementation is very simple, and acts mostly as a plain Ruby object. Here is an example: ```ruby class Config < Vagrant.plugin(2, :config) attr_accessor :widgets def initialize @widgets = UNSET_VALUE end def finalize! @widgets = 0 if @widgets == UNSET_VALUE end end ``` When using this configuration class, it looks like the following: ```ruby Vagrant.configure("2") do |config| # ... config.foo.widgets = 12 end ``` Easy. The only odd thing is the `UNSET_VALUE` bits above. This is actually so that Vagrant can properly automatically merge multiple configurations. Merging is covered in the next section, and `UNSET_VALUE` will be explained there. ## Merging Vagrant works by loading [multiple Vagrantfiles and merging them](/vagrant/docs/vagrantfile/#load-order). This merge logic is built-in to configuration classes. When merging two configuration objects, we will call them "old" and "new", it'll by default take all the instance variables defined on "new" that are not `UNSET_VALUE` and set them onto the merged result. The reason `UNSET_VALUE` is used instead of Ruby's `nil` is because it is possible that you want the default to be some value, and the user actually wants to set the value to `nil`, and it is impossible for Vagrant to automatically determine whether the user set the instance variable, or if it was defaulted as nil. This merge logic is what you want almost every time. Hence, in the example above, `@widgets` is set to `UNSET_VALUE`. If we had two Vagrant configuration objects in the same file, then Vagrant would properly merge the follows. The example below shows this: ```ruby Vagrant.configure("2") do |config| config.widgets = 1 end Vagrant.configure("2") do |config| # ... other stuff end Vagrant.configure("2") do |config| config.widgets = 2 end ``` If this were placed in a Vagrantfile, after merging, the value of widgets would be "2". The `finalize!` method is called only once ever on the final configuration object in order to set defaults. If `finalize!` is called, that configuration will never be merged again, it is final. This lets you detect any `UNSET_VALUE` and set the proper default, as we do in the above example. Of course, sometimes you want custom merge logic. Let us say we wanted our widgets to be additive. We can override the `merge` method to do this: ```ruby class Config < Vagrant.config("2", :config) attr_accessor :widgets def initialize @widgets = 0 end def merge(other) super.tap do |result| result.widgets = @widgets + other.widgets end end end ``` In this case, we did not use `UNSET_VALUE` for widgets because we did not need that behavior. We default to 0 and always merge by summing the two widgets. Now, if we ran the example above that had the 3 configuration blocks, the final value of widgets would be "3". ## Validation Configuration classes are also responsible for validating their own values. Vagrant will call the `validate` method to do this. An example validation method is shown below: ```ruby class Config < Vagrant.plugin("2", :config) # ... def validate(machine) errors = _detected_errors if @widgets <= 5 errors << "widgets must be greater than 5" end { "foo" => errors } end end ``` The validation method is given a `machine` object, since validation is done for each machine that Vagrant is managing. This allows you to conditionally validate some keys based on the state of the machine and so on. The `_detected_errors` method returns any errors already detected by Vagrant, such as unknown configuration keys. This returns an array of error messages, so be sure to turn it into the proper Hash object to return later. The return value is a Ruby Hash object, where the key is a section name, and the value is a list of error messages. These will be displayed by Vagrant. The hash must not contain any values if there are no errors. ## Accessing After all the configuration options are merged and finalized, you will likely want to access the finalized value in your plugin. The initializer function varies with each type of plugin, but _most_ plugins expose an initializer like this: ```ruby def initialize(machine, config) @machine = machine @config = config end ``` When authoring a plugin, simply call `super` in your initialize function to setup these instance variables: ```ruby def initialize(*) super @config.is_now_available # ...existing code end def my_helper @config.is_here_too end ``` For examples, take a look at Vagrant's own internal plugins in the `plugins` folder in Vagrant's source on GitHub. ================================================ FILE: website/content/docs/plugins/development-basics.mdx ================================================ --- layout: docs page_title: Plugin Development Basics - Plugins description: |- Plugins are a great way to augment or change the behavior and functionality of Vagrant. Since plugins introduce additional external dependencies for users, they should be used as a last resort when attempting to do something with Vagrant. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development Basics Plugins are a great way to augment or change the behavior and functionality of Vagrant. Since plugins introduce additional external dependencies for users, they should be used as a last resort when attempting to do something with Vagrant. But if you need to introduce custom behaviors into Vagrant, plugins are the best way, since they are safe against future upgrades and use a stable API. ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. Plugins are written using [Ruby](https://www.ruby-lang.org/en/) and are packaged using [RubyGems](https://rubygems.org/). Familiarity with Ruby is required, but the [packaging and distribution](/vagrant/docs/plugins/packaging) section should help guide you to packaging your plugin into a RubyGem. ## Setup and Workflow Because plugins are packaged as RubyGems, Vagrant plugins should be developed as if you were developing a regular RubyGem. The easiest way to do this is to use the `bundle gem` command. Once the directory structure for a RubyGem is setup, you will want to modify your Gemfile. Here is the basic structure of a Gemfile for Vagrant plugin development: ```ruby source "https://rubygems.org" group :development do gem "vagrant", git: "https://github.com/hashicorp/vagrant.git" end group :plugins do gem "my-vagrant-plugin", path: "." end ``` This Gemfile gets "vagrant" for development. This allows you to `bundle exec vagrant` to run Vagrant with your plugin already loaded, so that you can test it manually that way. The only thing about this Gemfile that may stand out as odd is the "plugins" group and putting your plugin in that group. Because `vagrant plugin` commands do not work in development, this is how you "install" your plugin into Vagrant. Vagrant will automatically load any gems listed in the "plugins" group. Note that this also allows you to add multiple plugins to Vagrant for development, if your plugin works with another plugin. When you want to manually test your plugin, use `bundle exec vagrant` in order to run Vagrant with your plugin loaded (as we specified in the Gemfile). ## Plugin Definition All plugins are required to have a definition. A definition contains details about the plugin such as the name of it and what components it contains. A definition at the bare minimum looks like the following: ```ruby class MyPlugin < Vagrant.plugin("2") name "My Plugin" end ``` A definition is a class that inherits from `Vagrant.plugin("2")`. The "2" there is the version that the plugin is valid for. API stability is only promised for each major version of Vagrant, so this is important. (The 1.x series is working towards 2.0, so the API version is "2") **The most critical feature of a plugin definition** is that it must _always_ load, no matter what version of Vagrant is running. Theoretically, Vagrant version 87 (does not actually exist) would be able to load a version 2 plugin definition. This is achieved through clever lazy loading of individual components of the plugin, and is covered shortly. ## Plugin Components Within the definition, a plugin advertises what components it adds to Vagrant. An example is shown below where a command and provisioner are added: ```ruby class MyPlugin < Vagrant.plugin("2") name "My Plugin" command "run-my-plugin" do require_relative "command" Command end provisioner "my-provisioner" do require_relative "provisioner" Provisioner end end ``` Let us go over the major pieces of what is going on here. Note from a general Ruby language perspective the above _should_ be familiar. The syntax should not scare you. If it does, then please familiarize with Ruby further before attempting to write a plugin. The first thing to note is that individual components are defined by making a method call with the component name, such as `command` or `provisioner`. These in turn take some parameters. In the case of our example it is just the name of the command and the name of the provisioner. All component definitions then take a block argument (a callback) that must return the actual component implementation class. The block argument is where the "clever lazy loading" (mentioned above) comes into play. The component blocks should lazy load the actual file that contains the implementation of the component, and then return that component. This is done because the actual dependencies and APIs used when defining components are not stable across major Vagrant versions. A command implementation written for Vagrant 2.0 will not be compatible with Vagrant 3.0 and so on. But the _definition_ is just plain Ruby that must always be forward compatible to future Vagrant versions. To repeat, **the lazy loading aspect of plugin components is critical** to the way Vagrant plugins work. All components must be lazily loaded and returned within their definition blocks. Now, each component has a different API. Please visit the relevant section using the navigation to the left under "Plugins" to learn more about developing each type of component. ## Error Handling One of Vagrant's biggest strength is gracefully handling errors and reporting them in human-readable ways. Vagrant has always strongly believed that if a user sees a stack trace, it is a bug. It is expected that plugins will behave the same way, and Vagrant provides strong error handling mechanisms to assist with this. Error handling in Vagrant is done entirely by raising Ruby exceptions. But Vagrant treats certain errors differently than others. If an error is raised that inherits from `Vagrant::Errors::VagrantError`, then the `vagrant` command will output the message of the error in nice red text to the console and exit with an exit status of 1. Otherwise, Vagrant reports an "unexpected error" that should be reported as a bug, and shows a full stack trace and other ugliness. Any stack traces should be considered bugs. Therefore, to fit into Vagrant's error handling mechanisms, subclass `VagrantError` and set a proper message on your exception. To see examples of this, look at Vagrant's [built-in errors](https://github.com/hashicorp/vagrant/blob/main/lib/vagrant/errors.rb). ## Console Input and Output Most plugins are likely going to want to do some sort of input/output. Plugins should _never_ use Ruby's built-in `puts` or `gets` style methods. Instead, all input/output should go through some sort of Vagrant UI object. The Vagrant UI object properly handles cases where there is no TTY, output pipes are closed, there is no input pipe, etc. A UI object is available on every `Vagrant::Environment` via the `ui` property and is exposed within every middleware environment via the `:ui` key. UI objects have [decent documentation](https://github.com/hashicorp/vagrant/blob/main/lib/vagrant/ui.rb) within the comments of their source. ================================================ FILE: website/content/docs/plugins/go-plugins/guests.mdx ================================================ --- layout: docs page_title: Custom Guests - Go Plugin Development description: |- This page documents how to add new guest OS detection to Vagrant, allowing Vagrant to properly configure new operating systems. Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Go Plugin Development: Guests Outside of these components, the caller must provide all other arguments that are required. The most basic guest plugin is composed of: 1. A detection function that determines if the plugin is usable on the system (is the expected guest) 2. A set of capabilities that define actions that can be run against the guest 3. An entry point defining the plugin options **Note**: To quickly get started writing Go guest plugins, clone the [vagrant-guest-plugin-skeleton](https://github.com/soapy1/vagrant-guest-plugin-skeleton) template and follow the Readme. The file structure of a guest plugin looks like: ``` - myplugin |- main.go \- guest |- myguest.go \- cap |- mycapability.go ``` Where `main.go` defines the plugin options. `guest/myguest.go` defines the core plugin functionality including the detection of the guest. `cap/*` has the definitions of all the guest plugin capabilities. These capabilities are the same as those for [Ruby plugins](/vagrant/docs/plugins/guest-capabilities). ## Writing a guest plugin A guest must satisfy the interface defined for a guest component ``` GuestDetectFunc() interface{} ParentFunc() interface{} HasCapabilityFunc() interface{} CapabilityFunc(capName string) interface{} ``` Src: https://github.com/hashicorp/vagrant-plugin-sdk/blob/main/component/component.go `GuestDetectFunc`: returns a function that defines the code that determines if the guest is detected. The returned function must return a `bool`. `ParentFunc`: returns a function that defines the code that determines the most immediate parent plugin. A child plugin will inherit all the capabilities defined in the parent. The returned function must return a `string`. `HasCapabilityFunc`: returns a function that defines a lookup for a capability. The returned function must return an `bool`. `CapabilityFunc`: returns a capability function that is defined by the plugin registered by a given name. An example guest plugin ``` // file: myplugin/guest/myguest.go package guest import ( "github.com/hashicorp/vagrant-plugin-sdk/component" sdkcore "github.com/hashicorp/vagrant-plugin-sdk/core" ) // AlwaysTrueGuest is a Guest implementation for myplugin. type AlwaysTrueGuest struct { } // DetectFunc implements component.Guest func (h *AlwaysTrueGuest) GuestDetectFunc() interface{} { return h.Detect } func (h *AlwaysTrueGuest) Detect(t sdkcore.Target) (bool, error) { return true, nil } // ParentsFunc implements component.Guest func (h *AlwaysTrueGuest) ParentFunc() interface{} { return h.Parent } func (h *AlwaysTrueGuest) Parent() string { // This plugin has no parents return "" } // HasCapabilityFunc implements component.Guest func (h *AlwaysTrueGuest) HasCapabilityFunc() interface{} { return h.CheckCapability } func (h *AlwaysTrueGuest) CheckCapability(n *component.NamedCapability) bool { // This plugin has no capabilities return false } // CapabilityFunc implements component.Guest func (h *AlwaysTrueGuest) CapabilityFunc(name string) interface{} { return fmt.Errorf("requested capability %s not found", name) } var ( _ component.Guest = (*AlwaysTrueGuest)(nil) ) ``` ``` // file: myplugin/main.go package myplugin import ( sdk "github.com/hashicorp/vagrant-plugin-sdk" "github.com/hashicorp/vagrant/builtin/myplugin/guest" ) // Options are the SDK options to use for instantiation. var ComponentOptions = []sdk.Option{ sdk.WithComponents( // Include the defined guest as a component defined in this plugin &guest.AlwaysTrueGuest{}, ), } func main() { sdk.Main(ComponentOptions...) os.Exit(0) } ``` In this example, the guest plugin will always be detected. It does not define any capabilities, or have any parent plugins. ### Defining and registering guest capabilities A guest plugin may have capabilities two ways: 1. By defining and implementing the capability in the plugin 2. By inheriting the capability from a parent guest plugin Define a capability by writing out a function that returns the desired capability ``` // file: myplugin/guest/cap/mycapability.go package cap import ( "io/ioutil" "github.com/hashicorp/vagrant-plugin-sdk/terminal" ) func WriteHelloFunc() interface{} { return WriteHello } func WriteHello(trm terminal.UI) error { trm.Output("Hello world") return nil } ``` Make the capability available to the plugin by filling in the capability functions ``` myplugin/guest/myguest.go // HasCapabilityFunc implements component.Guest func (h *AlwaysTrueGuest) HasCapabilityFunc() interface{} { return h.CheckCapability } func (h *AlwaysTrueGuest) CheckCapability(n *component.NamedCapability) bool { if n.Capability == "write_hello" { return true } return false } // CapabilityFunc implements component.Guest func (h *AlwaysTrueGuest) CapabilityFunc(name string) interface{} { if name == "write_hello" { return h.WriteHelloCap } return errors.New("Invalid capability requested") } func (h *AlwaysTrueGuest) WriteHelloCap(ui terminal.UI) error { return cap.WriteHello(ui) } ``` A guest plugin may inherit the capabilities of a parent function by defining a parent in the plugin implementation. This is done by setting the return value of the `Parent` function to the name of the desired parent plugin. Go based guest plugins may use Ruby based plugins as their parent. ``` // file: myplugin/guest/myguest.go ... // ParentsFunc implements component.Guest func (h *AlwaysTrueGuest) ParentFunc() interface{} { return h.Parent } func (h *AlwaysTrueGuest) Parent() string { // This plugin sets the parent to the "debian" plugin which is provided // as a Ruby plugin. This AlwaysTrueGuest now inherits all the capabilities // of the debian Ruby guest plugin. return "debian" } ``` ================================================ FILE: website/content/docs/plugins/go-plugins/index.mdx ================================================ --- layout: docs page_title: Go Based Plugins description: |- Vagrant comes with many great features out of the box to get your environments up and running. Sometimes, however, you want to change the way Vagrant does something or add additional functionality to Vagrant. This can be done via Vagrant plugins. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Go Vagrant Plugins With the introduction of Vagrant-go, Vagrant now supports running plugins implemented in Go. Note that Vagrant-go can run Go and Ruby based plugins while Vagrant-ruby only runs Ruby based plugins. ## Anatomy of a Go plugin When a plugin is started, it runs in its own process and is able to communicate with the other plugins over GRPC. A Vagrant-go plugin must implement an interface for a [plugin component](https://github.com/hashicorp/vagrant-plugin-sdk/blob/main/component/component.go). A plugin may satisfy more than one component interface. The plugin interfaces define the `Func` functions for the plugin. These functions are meant to return the actual function that represents the named action. For example the `Host` component defines a `DetectFunc`. So, a plugin must have a `DetectFunc` implementation that returns a function that can detect if Vagrant is running on the given host. For example ``` // DetectFunc implements component.Host func (h *AlwaysTrueHost) DetectFunc() interface{} { return h.Detect } func (h *AlwaysTrueHost) Detect() bool { // This plugin always detects that it is running on the expected host return true } ``` Using this pattern of `Func` functions, Vagrant is able to allow flexibility over the receivers of the functions that actually define the behavior of the plugin. This injection of dependencies is done using go-argmapper. So, in the same example, the `Detect` function may be made to accept some arguments. ``` // DetectFunc implements component.Host func (h *AlwaysTrueHost) DetectFunc() interface{} { return h.Detect } func (h *AlwaysTrueHost) Detect(string msg, trm terminal.UI) bool { trm.Output(msg) return true } ``` Now, in order to run the `Detect` function, the caller must provide a `string` and `terminal.UI` argument. By default, Vagrant will always inject the following arguments into a call to a plugin: - `terminal.UI` component - basis - project (when available) - context - logger For each component type, Vagrant may also inject some additional arguments. Outside of these components, the caller must provide all other arguments that are required. So, in the case above, in order to successfully call this `Detect` function, the caller must also provide a `string`. It is recommended that plugin authors do not rely on arguments being injected into their implementations outside of these sets of arguments. ================================================ FILE: website/content/docs/plugins/guest-capabilities.mdx ================================================ --- layout: docs page_title: Guest Capabilities - Plugin Development description: |- This page documents how to add new capabilities for guests to Vagrant, allowing Vagrant to perform new actions on specific guest operating systems. Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development: Guest Capabilities This page documents how to add new capabilities for [guests](/vagrant/docs/plugins/guests) to Vagrant, allowing Vagrant to perform new actions on specific guest operating systems. Prior to reading this, you should be familiar with the [plugin development basics](/vagrant/docs/plugins/development-basics). ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. Guest capabilities augment [guests](/vagrant/docs/plugins/guests) by attaching specific "capabilities" to the guest, which are actions that can be performed in the context of that guest operating system. The power of capabilities is that plugins can add new capabilities to existing guest operating systems without modifying the core of Vagrant. In earlier versions of Vagrant, all the guest logic was contained in the core of Vagrant and was not easily augmented. ## Definition Component Within the context of a plugin definition, guest capabilities can be defined like so: ```ruby guest_capability "ubuntu", "my_custom_capability" do require_relative "cap/my_custom_capability" Cap::MyCustomCapability end ``` Guest capabilities are defined by calling the `guest_capability` method, which takes two parameters: the guest to add the capability to, and the name of the capability itself. Then, the block argument returns a class that implements a method named the same as the capability. This is covered in more detail in the next section. ## Implementation Implementations should be classes or modules that have a method with the same name as the capability. The method must be immediately accessible on the class returned from the `guest_capability` component, meaning that if it is an instance method, an instance should be returned. In general, class methods are used for capabilities. For example, here is the implementation for the capability above: ```ruby module Cap class MyCustomCapability def self.my_custom_capability(machine) # implementation end end end ``` All capabilities get the Vagrant machine object as the first argument. Additional arguments are determined by the specific capability, so view the documentation or usage of the capability you are trying to implement for more information. Some capabilities must also return values back to the caller, so be aware of that when implementing a capability. Capabilities always have access to communication channels such as SSH on the machine, and the machine can generally be assumed to be booted. ## Calling Capabilities Since you have access to the machine in every capability, capabilities can also call _other_ capabilities. This is useful for using the inheritance mechanism of capabilities to potentially ask helpers for more information. For example, the "redhat" guest has a "network_scripts_dir" capability that simply returns the directory where networking scripts go. Capabilities on child guests of RedHat such as CentOS or Fedora use this capability to determine where networking scripts go, while sometimes overriding it themselves. Capabilities can be called like so: ```ruby machine.guest.capability(:capability_name) ``` Any additional arguments given to the method will be passed on to the capability, and the capability will return the value that the actual capability returned. ================================================ FILE: website/content/docs/plugins/guests.mdx ================================================ --- layout: docs page_title: Custom Guests - Plugin Development description: |- This page documents how to add new guest OS detection to Vagrant, allowing Vagrant to properly configure new operating systems. Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development: Guests This page documents how to add new guest OS detection to Vagrant, allowing Vagrant to properly configure new operating systems. Prior to reading this, you should be familiar with the [plugin development basics](/vagrant/docs/plugins/development-basics). ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. Vagrant has many features that requires doing guest OS-specific actions, such as mounting folders, configuring networks, etc. These tasks vary from operating system to operating system. If you find that one of these does not work for your operating system, then maybe the guest implementation is incomplete or incorrect. ## Definition Component Within the context of a plugin definition, new guests can be defined like so: ```ruby guest "ubuntu" do require_relative "guest" Guest end ``` Guests are defined with the `guest` method. The first argument is the name of the guest. This name is not actually used anywhere, but may in the future, so choose something helpful. Then, the block argument returns a class that implements the `Vagrant.plugin(2, :guest)` interface. ## Implementation Implementations of guests subclass `Vagrant.plugin("2", "guest")`. Within this implementation, only the `detect?` method needs to be implemented. The `detect?` method is called by Vagrant at some point after the machine is booted in order to determine what operating system the guest is running. If you detect that it is your operating system, return `true` from `detect?`. Otherwise, return `false`. Communication channels to the machine are guaranteed to be running at this point, so the most common way to detect the operating system is to do some basic testing: ```ruby class MyGuest < Vagrant.plugin("2", "guest") def detect?(machine) machine.communicate.test("cat /etc/myos-release") end end ``` After detecting an OS, that OS is used for various [guest capabilities](/vagrant/docs/plugins/guest-capabilities) that may be required. ## Guest Inheritance Vagrant also supports a form of inheritance for guests, since sometimes operating systems stem from a common root. A good example of this is Linux is the root of Debian, which further is the root of Ubuntu in many cases. Inheritance allows guests to share a lot of common behavior while allowing distro-specific overrides. Inheritance is not done via standard Ruby class inheritance because Vagrant uses a custom [capability-based](/vagrant/docs/plugins/guest-capabilities) system. Vagrant handles inheritance dispatch for you. To subclass another guest, specify that guest's name as a second parameter in the guest definition: ```ruby guest "ubuntu", "debian" do require_relative "guest" Guest end ``` With the above component, the "ubuntu" guest inherits from "debian." When a capability is looked up for "ubuntu", all capabilities from "debian" are also available, and any capabilities in "ubuntu" override parent capabilities. When detecting operating systems with `detect?`, Vagrant always does a depth-first search by searching the children operating systems before checking their parents. Therefore, it is guaranteed in the above example that the `detect?` method on "ubuntu" will be called before "debian." ================================================ FILE: website/content/docs/plugins/host-capabilities.mdx ================================================ --- layout: docs page_title: Host Capabilities - Plugin Development description: >- This page documents how to add new capabilities for hosts to Vagrant, allowing Vagrant to perform new actions on specific host operating systems. Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development: Host Capabilities This page documents how to add new capabilities for [hosts](/vagrant/docs/plugins/hosts) to Vagrant, allowing Vagrant to perform new actions on specific host operating systems. Prior to reading this, you should be familiar with the [plugin development basics](/vagrant/docs/plugins/development-basics). ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. Host capabilities augment [hosts](/vagrant/docs/plugins/hosts) by attaching specific "capabilities" to the host, which are actions that can be performed in the context of that host operating system. The power of capabilities is that plugins can add new capabilities to existing host operating systems without modifying the core of Vagrant. In earlier versions of Vagrant, all the host logic was contained in the core of Vagrant and was not easily augmented. ## Definition and Implementation The definition and implementation of host capabilities is identical to [guest capabilities](/vagrant/docs/plugins/guest-capabilities). The main difference from guest capabilities, however, is that instead of taking a machine as the first argument, all host capabilities take an instance of `Vagrant::Environment` as their first argument. Access to the environment allows host capabilities to access global state, specific machines, and also allows them to call other host capabilities. ## Calling Capabilities Since you have access to the environment in every capability, capabilities can also call _other_ host capabilities. This is useful for using the inheritance mechanism of capabilities to potentially ask helpers for more information. For example, the "linux" guest has a "nfs_check_command" capability that returns the command to use to check if NFS is running. Capabilities on child guests of Linux such as RedHat or Arch use this capability to mostly inherit the Linux behavior, except for this minor detail. Capabilities can be called like so: ```ruby environment.host.capability(:capability_name) ``` Any additional arguments given to the method will be passed on to the capability, and the capability will return the value that the actual capability returned. ================================================ FILE: website/content/docs/plugins/hosts.mdx ================================================ --- layout: docs page_title: Custom Hosts - Plugin Development description: |- This page documents how to add new host OS detection to Vagrant, allowing Vagrant to properly execute host-specific operations on new operating systems. Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development: Hosts This page documents how to add new host OS detection to Vagrant, allowing Vagrant to properly execute host-specific operations on new operating systems. Prior to reading this, you should be familiar with the [plugin development basics](/vagrant/docs/plugins/development-basics). ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. Vagrant has some features that require host OS-specific actions, such as exporting NFS folders. These tasks vary from operating system to operating system. Vagrant uses host detection as well as [host capabilities](/vagrant/docs/plugins/host-capabilities) to perform these host OS-specific operations. ## Definition Component Within the context of a plugin definition, new hosts can be defined like so: ```ruby host "ubuntu" do require_relative "host" Host end ``` Hosts are defined with the `host` method. The first argument is the name of the host. This name is not actually used anywhere, but may in the future, so choose something helpful. Then, the block argument returns a class that implements the `Vagrant.plugin(2, :host)` interface. ## Implementation Implementations of hosts subclass `Vagrant.plugin("2", "host")`. Within this implementation, only the `detect?` method needs to be implemented. The `detect?` method is called by Vagrant very early on in its initialization process to determine if the OS that Vagrant is running on is this host. If you detect that it is your operating system, return `true` from `detect?`. Otherwise, return `false`. ```ruby class MyHost < Vagrant.plugin("2", "host") def detect?(environment) File.file?("/etc/arch-release") end end ``` After detecting an OS, that OS is used for various [host capabilities](/vagrant/docs/plugins/host-capabilities) that may be required. ## Host Inheritance Vagrant also supports a form of inheritance for hosts, since sometimes operating systems stem from a common root. A good example of this is Linux is the root of Debian, which further is the root of Ubuntu in many cases. Inheritance allows hosts to share a lot of common behavior while allowing distro-specific overrides. Inheritance is not done via standard Ruby class inheritance because Vagrant uses a custom [capability-based](/vagrant/docs/plugins/host-capabilities) system. Vagrant handles inheritance dispatch for you. To subclass another host, specify that host's name as a second parameter in the host definition: ```ruby host "ubuntu", "debian" do require_relative "host" Host end ``` With the above component, the "ubuntu" host inherits from "debian." When a capability is looked up for "ubuntu", all capabilities from "debian" are also available, and any capabilities in "ubuntu" override parent capabilities. When detecting operating systems with `detect?`, Vagrant always does a depth-first search by searching the children operating systems before checking their parents. Therefore, it is guaranteed in the above example that the `detect?` method on "ubuntu" will be called before "debian." ================================================ FILE: website/content/docs/plugins/index.mdx ================================================ --- layout: docs page_title: Plugins description: |- Vagrant comes with many great features out of the box to get your environments up and running. Sometimes, however, you want to change the way Vagrant does something or add additional functionality to Vagrant. This can be done via Vagrant plugins. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugins Vagrant comes with many great features out of the box to get your environments up and running. Sometimes, however, you want to change the way Vagrant does something or add additional functionality to Vagrant. This can be done via Vagrant _plugins_. Plugins are powerful, first-class citizens that extend Vagrant using a well-documented, stable API that can withstand major version upgrades. In fact, most of the core of Vagrant is [implemented using plugins](https://github.com/hashicorp/vagrant/tree/main/plugins). Since Vagrant [dogfoods](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) its own plugin API, you can be confident that the interface is stable and well supported. Use the navigation on the left below the "Plugins" section to learn more about how to use and build your own plugins. ================================================ FILE: website/content/docs/plugins/packaging.mdx ================================================ --- layout: docs page_title: Packaging and Distribution - Plugin Development description: |- This page documents how to organize the file structure of your plugin and distribute it so that it is installable using standard installation methods. Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development: Packaging & Distribution This page documents how to organize the file structure of your plugin and distribute it so that it is installable using [standard installation methods](/vagrant/docs/plugins/usage). Prior to reading this, you should be familiar with the [plugin development basics](/vagrant/docs/plugins/development-basics). ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. ## Example Plugin The best way to describe packaging and distribution is to look at how another plugin does it. The best example plugin available for this is [vagrant-aws](https://github.com/mitchellh/vagrant-aws). By using [Bundler](http://bundler.io) and Rake, building a new vagrant-aws package is easy. By simply calling `rake package`, a `gem` file is dropped into the directory. By calling `rake release`, the gem is built and it is uploaded to the central [RubyGems](https://rubygems.org) repository so that it can be installed using `vagrant plugin install`. Your plugin can and should be this easy, too, since you basically get this for free by using Bundler. ## Setting Up Your Project To setup your project, run `bundle gem vagrant-my-plugin`. This will create a `vagrant-my-plugin` directory that has the initial layout to be a RubyGem. You should modify the `vagrant-my-plugin.gemspec` file to add any dependencies and change any metadata. View the [vagrant-aws.gemspec](https://github.com/mitchellh/vagrant-aws/blob/master/vagrant-aws.gemspec) for a good example. ~> **Do not depend on Vagrant** for your gem. Vagrant is no longer distributed as a gem, and you can assume that it will always be available when your plugin is installed. Once the directory structure for a RubyGem is setup, you will want to modify your Gemfile. Here is the basic structure of a Gemfile for Vagrant plugin development: ```ruby source "https://rubygems.org" group :development do gem "vagrant", git: "https://github.com/hashicorp/vagrant.git" end group :plugins do gem "my-vagrant-plugin", path: "." end ``` This Gemfile gets "vagrant" for development. This allows you to `bundle exec vagrant` to run Vagrant with your plugin already loaded, so that you can test it manually that way. The only thing about this Gemfile that may stand out as odd is the "plugins" group and putting your plugin in that group. Because `vagrant plugin` commands do not work in development, this is how you "install" your plugin into Vagrant. Vagrant will automatically load any gems listed in the "plugins" group. Note that this also allows you to add multiple plugins to Vagrant for development, if your plugin works with another plugin. Next, create a `Rakefile` that has at the very least, the following contents: ```ruby require "rubygems" require "bundler/setup" Bundler::GemHelper.install_tasks ``` If you run `rake -T` now, which lists all the available rake tasks, you should see that you have the `package` and `release` tasks. You can now develop your plugin and build it! You can view the [vagrant-aws Rakefile](https://github.com/mitchellh/vagrant-aws/blob/master/Rakefile) for a more comprehensive example that includes testing. ## Testing Your Plugin To manually test your plugin during development, use `bundle exec vagrant` to execute Vagrant with your plugin loaded (thanks to the Gemfile setup we did earlier). For automated testing, the [vagrant-spec](https://github.com/hashicorp/vagrant-spec) project provides helpers for both unit and acceptance testing plugins. See the giant README for that project for a detailed description of how to integrate vagrant-spec into your project. Vagrant itself (and all of its core plugins) use vagrant-spec for automated testing. ================================================ FILE: website/content/docs/plugins/providers.mdx ================================================ --- layout: docs page_title: Custom Providers - Plugin Development description: |- This page documents how to add support for new providers to Vagrant, allowing Vagrant to run and manage machines powered by a system other than VirtualBox. Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development: Providers This page documents how to add support for new [providers](/vagrant/docs/providers/) to Vagrant, allowing Vagrant to run and manage machines powered by a system other than VirtualBox. Prior to reading this, you should be familiar with the [plugin development basics](/vagrant/docs/plugins/development-basics). Prior to developing a provider you should also be familiar with how [providers work](/vagrant/docs/providers/) from a user standpoint. ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. ## Example Provider: AWS The best way to learn how to write a provider is to see how one is written in practice. To augment this documentation, please heavily study the [vagrant-aws](https://github.com/mitchellh/vagrant-aws) plugin, which implements an AWS provider. The plugin is a good example of how to structure, test, and implement your plugin. ## Definition Component Within the context of a plugin definition, new providers are defined like so: ```ruby provider "my_cloud" do require_relative "provider" Provider end ``` Providers are defined with the `provider` method, which takes a single argument specifying the name of the provider. This is the name that is used with `vagrant up` to specify the provider. So in the case above, our provider would be used by calling `vagrant up --provider=my_cloud`. The block argument then lazily loads and returns a class that implements the `Vagrant.plugin(2, :provider)` interface, which is covered next. ## Provider Class The provider class should subclass and implement `Vagrant.plugin(2, :provider)` which is an upgrade-safe way to let Vagrant return the proper parent class. This class and the methods that need to be implemented are [very well documented](https://github.com/hashicorp/vagrant/blob/main/lib/vagrant/plugin/v2/provider.rb). The documentation done on the class in the comments should be enough to understand what needs to be done. Viewing the [AWS provider class](https://github.com/mitchellh/vagrant-aws/blob/master/lib/vagrant-aws/provider.rb) as well as the [overall structure of the plugin](https://github.com/mitchellh/vagrant-aws) is recommended as a strong getting started point. Instead of going in depth over each method that needs to be implemented, the documentation will cover high-level but important points to help you create your provider. ## Box Format Each provider is responsible for having its own box format. This is actually an extremely simple step due to how generic boxes are. Before explaining you should get familiar with the general [box file format](/vagrant/docs/boxes/format). The only requirement for your box format is that the `metadata.json` file have a `provider` key which matches the name of your provider you chose above. In addition to this, you may put any data in the metadata as well as any files in the archive. Since Vagrant core itself does not care, it is up to your provider to handle the data of the box. Vagrant core just handles unpacking and verifying the box is for the proper provider. As an example of a couple box formats that are actually in use: - The `virtualbox` box format is just a flat directory of the contents of a `VBoxManage export` command. - The `vmware_fusion` box format is just a flat directory of the contents of a `vmwarevm` folder, but only including the bare essential files for VMware to function. - The `aws` box format is just a Vagrantfile defaulting some configuration. You can see an [example aws box unpacked here](https://github.com/mitchellh/vagrant-aws/tree/master/example_box). Before anything with your provider is even written, you can verify your box format works by doing `vagrant box add` with it. When you do a `vagrant box list` you can see what boxes for what providers are installed. You do _not need_ the provider plugin installed to add a box for that provider. ## Actions Probably the most important concept to understand when building a provider is the provider "action" interface. It is the secret sauce that makes providers do the magic they do. Actions are built on top of the concept of [middleware](https://github.com/mitchellh/middleware), which allow providers to execute multiple distinct steps, have error recovery mechanics, as well as before/after behaviors, and much more. Vagrant core requests specific actions from your provider through the `action` method on your provider class. The full list of actions requested is listed in the comments of that method on the superclass. If your provider does not implement a certain action, then Vagrant core will show a friendly error, so do not worry if you miss any, things will not explode or crash spectacularly. Take a look at how the VirtualBox provider [uses actions to build up complicated multi-step processes](https://github.com/hashicorp/vagrant/blob/main/plugins/providers/virtualbox/action.rb#L287). The AWS provider [uses a similar process](https://github.com/mitchellh/vagrant-aws/blob/master/lib/vagrant-aws/action.rb). ## Built-in Middleware To assist with common tasks, Vagrant ships with a set of [built-in middleware](https://github.com/hashicorp/vagrant/tree/main/lib/vagrant/action/builtin). Each of the middleware is well commented on the behavior and options for each, and using these built-in middleware is critical to building a well-behaved provider. These built-in middleware can be thought of as a standard library for your actions on your provider. The core VirtualBox provider uses these built-in middleware heavily. ## Persisting State In the process of creating and managing a machine, providers generally need to store some sort of state somewhere. Vagrant provides each machine with a directory to store this state. As a use-case example for this, the VirtualBox provider stores the UUID of the VirtualBox virtual machine created. This allows the provider to track whether the machine is created, running, suspended, etc. The VMware provider actually copies the entire virtual machine into this state directory, complete with virtual disk drives and everything. The directory is available from the `data_dir` attribute of the `Machine` instance given to initialize your provider. Within middleware actions, the machine is always available via the `:machine` key on the environment. The `data_dir` attribute is a Ruby [Pathname](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/pathname/rdoc/Pathname) object. It is important for providers to carefully manage all the contents of this directory. Vagrant core itself does little to clean up this directory. Therefore, when a machine is destroyed, be sure to clean up all the state from this directory. ## Configuration Vagrant supports [provider-specific configuration](/vagrant/docs/providers/configuration), allowing for users to finely tune and control specific providers from Vagrantfiles. It is easy for your custom provider to expose custom configuration as well. Provider-specific configuration is a special case of a normal [configuration plugin](/vagrant/docs/plugins/configuration). When defining the configuration component, name the configuration the same as the provider, and as a second parameter, specify `:provider`, like so: ```ruby config("my_cloud", :provider) do require_relative "config" Config end ``` As long as the name matches your provider, and the second `:provider` parameter is given, Vagrant will automatically expose this as provider-specific configuration for your provider. Users can now do the following in their Vagrantfiles: ```ruby config.vm.provider :my_cloud do |config| # Your specific configuration! end ``` The configuration class returned from the `config` component in the plugin is the same as any other [configuration plugin](/vagrant/docs/plugins/configuration), so read that page for more information. Vagrant automatically handles configuration validation and such just like any other configuration piece. The provider-specific configuration is available on the machine object via the `provider_config` attribute. So within actions or your provider class, you can access the config via `machine.provider_config`. -> **Best practice:** Your provider should _not require_ provider-specific configuration to function, if possible. Vagrant practices a strong [convention over configuration](https://en.wikipedia.org/wiki/Convention_over_configuration) philosophy. When a user installs your provider, they should ideally be able to `vagrant up --provider=your_provider` and have it just work. ## Parallelization Vagrant supports parallelizing some actions, such as `vagrant up`, if the provider explicitly supports it. By default, Vagrant will not parallelize a provider. When parallelization is enabled, multiple [actions](#actions) may be run in parallel. Therefore, providers must be certain that their action stacks are thread-safe. The core of Vagrant itself (such as box collections, SSH, etc.) is thread-safe. Providers can explicitly enable parallelization by setting the `parallel` option on the provider component: ```ruby provider("my_cloud", parallel: true) do require_relative "provider" Provider end ``` That is the only change that is needed to enable parallelization. ================================================ FILE: website/content/docs/plugins/provisioners.mdx ================================================ --- layout: docs page_title: Custom Provisioners - Plugin Development description: |- This page documents how to add new provisioners to Vagrant, allowing Vagrant to automatically install software and configure software using a custom provisioner. Prior to reading this, you should be familiar with the plugin development basics. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Development: Provisioners This page documents how to add new [provisioners](/vagrant/docs/provisioning/) to Vagrant, allowing Vagrant to automatically install software and configure software using a custom provisioner. Prior to reading this, you should be familiar with the [plugin development basics](/vagrant/docs/plugins/development-basics). ~> **Warning: Advanced Topic!** Developing plugins is an advanced topic that only experienced Vagrant users who are reasonably comfortable with Ruby should approach. ## Definition Component Within the context of a plugin definition, new provisioners can be defined like so: ```ruby provisioner "custom" do require_relative "provisioner" Provisioner end ``` Provisioners are defined with the `provisioner` method, which takes a single argument specifying the name of the provisioner. This is the name that used with `config.vm.provision` when configuring and enabling the provisioner. So in the case above, the provisioner would be enabled using `config.vm.provision :custom`. The block argument then lazily loads and returns a class that implements the `Vagrant.plugin(2, :provisioner)` interface, which is covered next. ## Provisioner Class The provisioner class should subclass and implement `Vagrant.plugin(2, :provisioner)` which is an upgrade-safe way to let Vagrant return the proper parent class for provisioners. This class and the methods that need to be implemented are [very well documented](https://github.com/hashicorp/vagrant/blob/main/lib/vagrant/plugin/v2/provisioner.rb). The documentation on the class in the comments should be enough to understand what needs to be done. There are two main methods that need to be implemented: the `configure` method and the `provision` method. The `configure` method is called early in the machine booting process to allow the provisioner to define new configuration on the machine, such as sharing folders, defining networks, etc. As an example, the [Chef solo provisioner](https://github.com/hashicorp/vagrant/blob/main/plugins/provisioners/chef/provisioner/chef_solo.rb#L24) uses this to define shared folders. The `provision` method is called when the machine is booted and ready for SSH connections. In this method, the provisioner should execute any commands that need to be executed. ================================================ FILE: website/content/docs/plugins/usage.mdx ================================================ --- layout: docs page_title: Plugin Usage - Plugins description: |- Installing a Vagrant plugin is easy, and should not take more than a few seconds. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Plugin Usage Installing a Vagrant plugin is easy, and should not take more than a few seconds. Please refer to the documentation of any plugin you plan on using for more information on how to use it, but there is one common method for installation and plugin activation. ~> **Warning!** 3rd party plugins can introduce instabilities into Vagrant due to the nature of them being written by non-core users. ## Installation Plugins are installed using `vagrant plugin install`: ```shell # Installing a plugin from a known gem source $ vagrant plugin install my-plugin # Installing a plugin from a local file source $ vagrant plugin install /path/to/my-plugin.gem ``` Once a plugin is installed, it will automatically be loaded by Vagrant. Plugins which cannot be loaded should not crash Vagrant. Instead, Vagrant will show an error message that a plugin failed to load. ## Usage Once a plugin is installed, you should refer to the plugin's documentation to see exactly how to use it. Plugins which add commands should be instantly available via `vagrant`, provisioners should be available via `config.vm.provision`, etc. **Note:** In the future, the `vagrant plugin` command will include a subcommand that will document the components that each plugin installs. ## Updating Plugins can be updated by running `vagrant plugin update`. This will update every installed plugin to the latest version. You can update a specific plugin by calling `vagrant plugin update NAME`. Vagrant will output what plugins were updated and to what version. To determine the changes in a specific version of a plugin, refer to the plugin's homepage (usually a GitHub page or similar). It is the plugin author's responsibility to provide a change log if he or she chooses to. ## Uninstallation Uninstalling a plugin is as easy as installing it. Just use the `vagrant plugin uninstall` command and the plugin will be removed. Example: ```shell-session $ vagrant plugin uninstall my-plugin ``` ## Listing Plugins To view what plugins are installed into your Vagrant environment at any time, use the `vagrant plugin list` command. This will list the plugins that are installed along with their version. ================================================ FILE: website/content/docs/providers/basic_usage.mdx ================================================ --- layout: docs page_title: Basic Usage - Providers description: |- Vagrant boxes are all provider-specific. A box for VirtualBox is incompatible with the VMware Fusion provider, or any other provider. A box must be installed for each provider, and can share the same name as other boxes as long as the providers differ. So you can have both a VirtualBox and VMware Fusion "bionic64" box. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Basic Provider Usage ## Boxes Vagrant boxes are all provider-specific. A box for VirtualBox is incompatible with the VMware Fusion provider, or any other provider. A box must be installed for each provider, and can share the same name as other boxes as long as the providers differ. So you can have both a VirtualBox and VMware Fusion "bionic64" box. Installing boxes has not changed at all: ```shell-session $ vagrant box add hashicorp/bionic64 ``` Vagrant now automatically detects what provider a box is for. This is visible when listing boxes. Vagrant puts the provider in parentheses next to the name, as can be seen below. ```shell-session $ vagrant box list bionic64 (virtualbox) bionic64 (vmware_fusion) ``` ## Vagrant Up Once a provider is installed, you can use it by calling `vagrant up` with the `--provider` flag. This will force Vagrant to use that specific provider. No other configuration is necessary! In normal day-to-day usage, the `--provider` flag is not necessary since Vagrant can usually pick the right provider for you. More details on how it does this is below. ```shell-session $ vagrant up --provider=vmware_fusion ``` If you specified a `--provider` flag, you only need to do this for the `up` command. Once a machine is up and running, Vagrant is able to see what provider is backing a running machine, so commands such as `destroy`, `suspend`, etc. do not need to be told what provider to use. -> Vagrant currently restricts you to bringing up one provider per machine. If you have a multi-machine environment, you can bring up one machine backed by VirtualBox and another backed by VMware Fusion, for example, but you cannot back the same machine with both VirtualBox and VMware Fusion. This is a limitation that will be removed in a future version of Vagrant. ## Default Provider As mentioned earlier, you typically do not need to specify `--provider` _ever_. Vagrant is smart enough about being able to detect the provider you want for a given environment. Vagrant attempts to find the default provider in the following order: 1. The `--provider` flag on a `vagrant up` is chosen above all else, if it is present. 2. If the `VAGRANT_DEFAULT_PROVIDER` environmental variable is set, it takes next priority and will be the provider chosen. 3. Vagrant will go through all of the `config.vm.provider` calls in the Vagrantfile and try each in order. It will choose the first provider that is usable. For example, if you configure Hyper-V, it will never be chosen on Mac this way. It must be both configured and usable. 4. Vagrant will go through all installed provider plugins (including the ones that come with Vagrant), and find the first plugin that reports it is usable. There is a priority system here: systems that are known better have a higher priority than systems that are worse. For example, if you have the VMware provider installed, it will always take priority over VirtualBox. 5. If Vagrant still has not found any usable providers, it will error. Using this method, there are very few cases that Vagrant does not find the correct provider for you. This also allows each [Vagrantfile](/vagrant/docs/vagrantfile/) to define what providers the development environment is made for by ordering provider configurations. A trick is to use `config.vm.provider` with no configuration at the top of your Vagrantfile to define the order of providers you prefer to support: ```ruby Vagrant.configure("2") do |config| # ... other config up here # Prefer VMware Fusion before VirtualBox config.vm.provider "vmware_fusion" config.vm.provider "virtualbox" end ``` ================================================ FILE: website/content/docs/providers/configuration.mdx ================================================ --- layout: docs page_title: Configuration - Providers description: |- While well-behaved Vagrant providers should work with any Vagrantfile with sane defaults, providers generally expose unique configuration options so that you can get the most out of each provider --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Configuration While well-behaved Vagrant providers should work with any Vagrantfile with sane defaults, providers generally expose unique configuration options so that you can get the most out of each provider. This provider-specific configuration is done within the Vagrantfile in a way that is portable, easy to use, and easy to understand. ## Portability An important fact is that even if you configure other providers within a Vagrantfile, the Vagrantfile remains portable even to individuals who do not necessarily have that provider installed. For example, if you configure VMware Fusion and send it to an individual who does not have the VMware Fusion provider, Vagrant will silently ignore that part of the configuration. ## Provider Configuration Configuring a specific provider looks like this: ```ruby Vagrant.configure("2") do |config| # ... config.vm.provider "virtualbox" do |vb| vb.customize ["modifyvm", :id, "--cpuexecutioncap", "50"] end end ``` Multiple `config.vm.provider` blocks can exist to configure multiple providers. The configuration format should look very similar to how provisioners are configured. The `config.vm.provider` takes a single parameter: the name of the provider being configured. Then, an inner block with custom configuration options is exposed that can be used to configure that provider. This inner configuration differs among providers, so please read the documentation for your provider of choice to see available configuration options. Remember, some providers do not require any provider-specific configuration and work directly out of the box. Provider-specific configuration is meant as a way to expose more options to get the most of the provider of your choice. It is not meant as a roadblock to running against a specific provider. ## Overriding Configuration Providers can also override non-provider specific configuration, such as `config.vm.box` and any other Vagrant configuration. This is done by specifying a second argument to `config.vm.provider`. This argument is just like the normal `config`, so set any settings you want, and they will be overridden only for that provider. Example: ```ruby Vagrant.configure("2") do |config| config.vm.box = "bionic64" config.vm.provider "vmware_fusion" do |v, override| override.vm.box = "bionic64_fusion" end end ``` In the above case, Vagrant will use the "bionic64" box by default, but will use "bionic64_fusion" if the VMware Fusion provider is used. -> **The Vagrant Way:** The proper "Vagrant way" is to avoid any provider-specific overrides if possible by making boxes for multiple providers that are as identical as possible, since box names can map to multiple providers. However, this is not always possible, and in those cases, overrides are available. ================================================ FILE: website/content/docs/providers/custom.mdx ================================================ --- layout: docs page_title: Custom Provider - Providers description: |- To learn how to make your own custom Vagrant providers, read the Vagrant plugin development guide on creating custom providers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Custom Provider To learn how to make your own custom Vagrant providers, read the Vagrant plugin development guide on [creating custom providers](/vagrant/docs/plugins/providers). ================================================ FILE: website/content/docs/providers/default.mdx ================================================ --- layout: docs page_title: Default Provider - Providers description: |- By default, VirtualBox is the default provider for Vagrant. VirtualBox is still the most accessible platform to use Vagrant: it is free, cross-platform, and has been supported by Vagrant for years. With VirtualBox as the default provider, it provides the lowest friction for new users to get started with Vagrant. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Default Provider By default, VirtualBox is the default provider for Vagrant. VirtualBox is still the most accessible platform to use Vagrant: it is free, cross-platform, and has been supported by Vagrant for years. With VirtualBox as the default provider, it provides the lowest friction for new users to get started with Vagrant. However, you may find after using Vagrant for some time that you prefer to use another provider as your default. In fact, this is quite common. To make this experience better, Vagrant allows specifying the default provider to use by setting the `VAGRANT_DEFAULT_PROVIDER` environmental variable. Just set `VAGRANT_DEFAULT_PROVIDER` to the provider you wish to be the default. For example, if you use Vagrant with VMware Fusion, you can set the environmental variable to `vmware_desktop` and it will be your default. ================================================ FILE: website/content/docs/providers/docker/basics.mdx ================================================ --- layout: docs page_title: Basic Usage - Docker Provider description: |- The Docker provider in Vagrant behaves just like any other provider. If you are familiar with Vagrant already, then using the Docker provider should be intuitive and simple. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Docker Basic Usage The Docker provider in Vagrant behaves just like any other provider. If you are familiar with Vagrant already, then using the Docker provider should be intuitive and simple. The Docker provider _does not_ require a `config.vm.box` setting. Since the "base image" for a Docker container is pulled from the Docker Index or built from a Dockerfile, the box does not add much value, and is optional for this provider. ## Docker Images The first method that Vagrant can use to source a Docker container is via an image. This image can be from any Docker registry. An example is shown below: ```ruby Vagrant.configure("2") do |config| config.vm.provider "docker" do |d| d.image = "foo/bar" end end ``` When `vagrant up --provider=docker` is run, this will bring up the image `foo/bar`. This is useful for extra components of your application that it might depend on: databases, queues, etc. Typically, the primary application you are working on is built with a Dockerfile, or via a container with SSH. ## Dockerfiles Vagrant can also automatically build and run images based on a local Dockerfile. This is useful for iterating on an application locally that is built into an image later. An example is shown below: ```ruby Vagrant.configure("2") do |config| config.vm.provider "docker" do |d| d.build_dir = "." end end ``` The above configuration will look for a `Dockerfile` in the same directory as the Vagrantfile. When `vagrant up --provider=docker` is run, Vagrant automatically builds that Dockerfile and starts a container based on that Dockerfile. The Dockerfile is rebuilt when `vagrant reload` is called. ## Synced Folders and Networking When using Docker, Vagrant automatically converts synced folders and networking options into Docker volumes and forwarded ports. You do not have to use the Docker-specific configurations to do this. This helps keep your Vagrantfile similar to how it has always looked. The Docker provider does not support specifying options for `owner` or `group` on folders synced with a docker container. ### Volume Consistency Docker's [volume consistency](https://docs.docker.com/v17.09/engine/admin/volumes/bind-mounts/) setting can be specified using the `docker_consistency` option when defining a synced folder. This can [greatly improve performance on macOS](https://docs.docker.com/docker-for-mac/osxfs-caching). An example is shown using the `cached` and `delegated` settings: ```ruby config.vm.synced_folder "/host/dir1", "/guest/dir1", docker_consistency: "cached" config.vm.synced_folder "/host/dir2", "/guest/dir2", docker_consistency: "delegated" ``` ## Host VM If the system cannot run Linux containers natively, Vagrant automatically spins up a "host VM" to run Docker. This allows your Docker-based Vagrant environments to remain portable, without inconsistencies depending on the platform they are running on. Vagrant will spin up a single instance of a host VM and run multiple containers on this one VM. This means that with the Docker provider, you only have the overhead of one virtual machine, and only if it is absolutely necessary. By default, the host VM Vagrant spins up is [backed by boot2docker](https://github.com/hashicorp/vagrant/blob/main/plugins/providers/docker/hostmachine/Vagrantfile), because it launches quickly and uses little resources. But the host VM can be customized to point to _any_ Vagrantfile. This allows the host VM to more closely match production by running a VM running Ubuntu, RHEL, etc. It can run any operating system supported by Vagrant. -> **Synced folder note:** Vagrant will attempt to use the "best" synced folder implementation it can. For boot2docker, this is often rsync. In this case, make sure you have rsync installed on your host machine. Vagrant will give you a human-friendly error message if it is not. An example of changing the host VM is shown below. Remember that this is optional, and Vagrant will spin up a default host VM if it is not specified: ```ruby Vagrant.configure("2") do |config| config.vm.provider "docker" do |d| d.vagrant_vagrantfile = "../path/to/Vagrantfile" end end ``` The host VM will be spun up at the first `vagrant up` where the provider is Docker. To control this host VM, use the [global-status command](/vagrant/docs/cli/global-status) along with global control. ================================================ FILE: website/content/docs/providers/docker/boxes.mdx ================================================ --- layout: docs page_title: Boxes - Docker Provider description: |- The Docker provider does not require a Vagrant box. The "config.vm.box" setting is completely optional. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Docker Boxes The Docker provider does not require a Vagrant box. The `config.vm.box` setting is completely optional. A box can still be used and specified, however, to provide defaults. Because the `Vagrantfile` within a box is loaded as part of the configuration loading sequence, it can be used to configure the foundation of a development environment. In general, however, you will not need a box with the Docker provider. ================================================ FILE: website/content/docs/providers/docker/commands.mdx ================================================ --- layout: docs page_title: Commands - Docker Provider description: |- The Docker provider exposes some additional Vagrant commands that are useful for interacting with Docker containers. This helps with your workflow on top of Vagrant so that you have full access to Docker underneath. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Docker Commands The Docker provider exposes some additional Vagrant commands that are useful for interacting with Docker containers. This helps with your workflow on top of Vagrant so that you have full access to Docker underneath. ### docker-exec `vagrant docker-exec` can be used to run one-off commands against a Docker container that is currently running. If the container is not running, an error will be returned. ```shell-session $ vagrant docker-exec app -- rake db:migrate ``` The above would run `rake db:migrate` in the context of an `app` container. Note that the "name" corresponds to the name of the VM, **not** the name of the Docker container. Consider the following Vagrantfile: ```ruby Vagrant.configure(2) do |config| config.vm.provider "docker" do |d| d.image = "consul" end end ``` This Vagrantfile will start the official Docker Consul image. However, the associated Vagrant command to `docker-exec` into this instance is: ```shell-session $ vagrant docker-exec -it -- /bin/sh ``` In particular, the command is actually: ```shell-session $ vagrant docker-exec default -it -- /bin/sh ``` Because "default" is the default name of the first defined VM. In a multi-machine Vagrant setup as shown below, the "name" attribute corresponds to the name of the VM, **not** the name of the container: ```ruby Vagrant.configure do |config| config.vm.define "web" do config.vm.provider "docker" do |d| d.image = "nginx" end end config.vm.define "consul" do config.vm.provider "docker" do |d| d.image = "consul" end end end ``` The following command is invalid: ```shell-session # Not valid $ vagrant docker-exec -it nginx -- /bin/sh ``` This is because the "name" of the VM is "web", so the command is actually: ```shell-session $ vagrant docker-exec -it web -- /bin/sh ``` For this reason, it is recommended that you name the VM the same as the container. In the above example, it is unambiguous that the command to enter the Consul container is: ```shell-session $ vagrant docker-exec -it consul -- /bin/sh ``` ### docker-logs `vagrant docker-logs` can be used to see the logs of a running container. Because most Docker containers are single-process, this is used to see the logs of that one process. Additionally, the logs can be tailed. ### docker-run `vagrant docker-run` can be used to run one-off commands against a Docker container. The one-off Docker container that is started shares all the volumes, links, etc. of the original Docker container. An example is shown below: ```shell-session $ vagrant docker-run app -- rake db:migrate ``` The above would run `rake db:migrate` in the context of an `app` container. ================================================ FILE: website/content/docs/providers/docker/configuration.mdx ================================================ --- layout: docs page_title: Configuration- Docker Provider description: |- The Docker provider has some provider-specific configuration options you may set. A complete reference is shown on this page. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Docker Configuration The Docker provider has some provider-specific configuration options you may set. A complete reference is shown below. ## Required One of the following settings is required when using the Docker provider: - `build_dir` (string) - The path to a directory containing a Dockerfile. - `image` (string) - The image to launch, specified by the image ID or a name such as `ubuntu:12.04`. - `git_repo` (string) - The URL of a git repository to build the image from. Supports pulling specific tags, branches and revision, consult the [docker documentation](https://docs.docker.com/engine/reference/commandline/build/#/git-repositories) for more information. ## Optional General settings: - `build_args` (array of strings) - Extra arguments to pass to `docker build` when `build_dir` is in use. - `cmd` (array of strings) - Custom command to run on the container. Example: `["ls", "/app"]`. - `compose` (boolean) - If true, Vagrant will use `docker-compose` to manage the lifecycle and configuration of containers. This defaults to false. - `compose_configuration` (Hash) - Configuration values used for populating the `docker-compose.yml` file. The value of this Hash is directly merged and written to the `docker-compose.yml` file allowing customization of non-services items like networks and volumes. - `create_args` (array of strings) - Additional arguments to pass to `docker run` when the container is started. This can be used to set parameters that are not exposed via the Vagrantfile. - `dockerfile` (string) - Name of the Dockerfile in the build directory. This defaults to "Dockerfile" - `env` (hash) - Environmental variables to expose into the container. - `expose` (array of integers) - Ports to expose from the container but not to the host machine. Useful for links. - `link` (method, string argument) - Link this container to another by name. The argument should be in the format of `(name:alias)`. Example: `docker.link("db:db")`. Note, if you are linking to another container in the same Vagrantfile, make sure you call `vagrant up` with the `--no-parallel` flag. - `force_host_vm` (boolean) - If true, then a host VM will be spun up even if the computer running Vagrant supports Linux containers. This is useful to enforce a consistent environment to run Docker. This value defaults to "false" on Linux, Mac, and Windows hosts and defaults to "true" on other hosts. Users on other hosts who choose to use a different Docker provider or opt-in to the native Docker builds can explicitly set this value to false to disable the behavior. - `has_ssh` (boolean) - If true, then Vagrant will support SSH with the container. This allows `vagrant ssh` to work, provisioners, etc. This defaults to false. - `host_vm_build_dir_options` (hash) - Synced folder options for the `build_dir`, since the build directory is synced using a synced folder if a host VM is in use. - `name` (string) - Name of the container. Note that this has to be unique across all containers on the host VM. By default Vagrant will generate some random name. - `pull` (bool) - If true, the image will be pulled on every `up` and `reload`. Defaults to false. - `ports` (array of strings) - Ports to expose from the container to the host. These should be in the format of `host:container`. - `remains_running` (boolean) - If true, Vagrant expects this container to remain running and will make sure that it does for a certain amount of time. If false, then Vagrant expects that this container will automatically stop at some point, and will not error if it sees it do that. - `stop_timeout` (integer) - The amount of time to wait when stopping a container before sending a SIGTERM to the process. - `vagrant_machine` (string) - The name of the Vagrant machine in the `vagrant_vagrantfile` to use as the host machine. This defaults to "default". - `vagrant_vagrantfile` (string) - Path to a Vagrantfile that contains the `vagrant_machine` to use as the host VM if needed. - `volumes` (array of strings) - List of directories to mount as volumes into the container. These directories must exist in the host where Docker is running. If you want to sync folders from the host Vagrant is running, just use synced folders. Below, we have settings related to auth. If these are set, then Vagrant will `docker login` prior to starting containers, allowing you to pull images from private repositories. - `email` (string) - Email address for logging in. - `username` (string) - Username for logging in. - `password` (string) - Password for logging in. - `auth_server` (string) - The server to use for authentication. If not set, the Docker Hub will be used. ================================================ FILE: website/content/docs/providers/docker/index.mdx ================================================ --- layout: docs page_title: Docker Provider description: |- Vagrant comes with support out of the box for using Docker as a provider. This allows for your development environments to be backed by Docker containers rather than virtual machines. Additionally, it provides for a good workflow for developing Dockerfiles. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Docker Vagrant comes with support out of the box for using Docker as a provider. This allows for your development environments to be backed by Docker containers rather than virtual machines. Additionally, it provides for a good workflow for developing Dockerfiles. ~> **Warning: Docker knowledge assumed.** We assume that you know what Docker is and that you are comfortable with the basics of Docker. If not, we recommend starting with another provider such as [VirtualBox](/vagrant/docs/providers/virtualbox). Use the navigation to the left to find a specific Docker topic to read more about. ================================================ FILE: website/content/docs/providers/docker/networking.mdx ================================================ --- layout: docs page_title: Networking - Docker Provider description: |- The Vagrant Docker provider supports using the private network using the `docker network` commands. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Networking Vagrant uses the `docker network` command under the hood to create and manage networks for containers. Vagrant will do its best to create and manage networks for any containers configured inside the Vagrantfile. Each docker network is grouped by the subnet used for a requested ip address. For each newly unique network, Vagrant will run the `docker network create` subcommand with the provided options from the network config inside your Vagrantfile. If multiple networks share the same subnet, Vagrant will reuse that existing network for multiple containers. Once these networks have been created, Vagrant will attach these networks to the requested containers using the `docker network connect` for each network. Vagrant names the networks inside docker as `vagrant_network` or `vagrant_network_` where `` is the subnet for the network if defined by the user. An example of these networks is shown later in this page. If no subnet is requested for the network, Vagrant will connect the `vagrant_network` to the container. When destroying containers through Vagrant, Vagrant will clean up the network if there are no more containers using the network. ## Docker Network Options Most of the options work similar to other Vagrant providers. Defining either an ip or using `type: 'dhcp'` will give you a network on your container. ```ruby docker.vm.network :private_network, type: "dhcp" docker.vm.network :private_network, ip: "172.20.128.2" ``` If you want to set something specific with a new network you can use scoped options which align with the command line flags for the [docker network create](https://docs.docker.com/engine/reference/commandline/network_create/) command. If there are any specific options you want to enable from the `docker network create` command, you can specify them like this: ```ruby docker.vm.network :private_network, type: "dhcp", docker_network__internal: true ``` This will enable the `internal` option for the network when created with `docker network create`. Where `option` corresponds to the given flag that will be provided to the `docker network create` command. Similarly, if there is a value you wish to enable when connecting a container to a given network, you can use the following value in your network config: ```ruby docker_connect__option: "value" ``` When the docker provider creates a new network a netmask is required. If the netmask is not provided, Vagrant will default to a `/24` for IPv4 and `/64` for IPv6. To provide a different mask, set it using the `netmask` option: ```ruby docker.vm.network :private_network, ip: "172.20.128.2", netmask: 16 ``` For networks which set the type to "dhcp", it is also possible to specify a specific subnet for the network connection. This allows containers to connect to networks other than the default `vagrant_network` network. The docker provider supports specifying the desired subnet in two ways. The first is by using the `ip` and `netmask` options: ```ruby docker.vm.network :private_network, type: "dhcp", ip: "172.20.128.0", netmask: 24 ``` The second is by using the `subnet` option: ```ruby docker.vm.network :private_network, type: "dhcp", subnet: "172.20.128.0/24" ``` ### Public Networks The Vagrant docker provider also supports defining public networks. The easiest way to define a public network is by setting the `type` option to "dhcp": ```ruby docker.vm.network :public_network, type: "dhcp" ``` A bridge interface is required when setting up a public network. When no bridge device name is provided, Vagrant will prompt for the appropriate device to use. This can also be set using the `bridge` option: ```ruby docker.vm.network :public_network, type: "dhcp", bridge: "eth0" ``` The `bridge` option also supports a list of interfaces which can be used for setting up the network. Vagrant will inspect the defined interfaces and use the first active interface when setting up the network: ```ruby docker.vm.network :public_network, type: "dhcp", bridge: ["eth0", "wlan0"] ``` The available IP range for the bridge interface must be known when setting up the docker network. Even though a DHCP service may be available on the public network, docker will manage IP addresses provided to containers. This means that the subnet provided when defining the available IP range for the network should not be included within the subnet managed by the DHCP service. Vagrant will prompt for the available IP range information, however, it can also be provided in the Vagrantfile using the `docker_network__ip_range` option: ```ruby docker.vm.network :public_network, type: "dhcp", bridge: "eth0", docker_network__ip_range: "192.168.1.252/30" ``` Finally, the gateway for the interface is required during setup. The docker provider will default the gateway address to the first address available for the subnet of the bridge device. Vagrant will prompt for confirmation to use the default address. The address can also be manually set in the Vagrantfile using the `docker_network__gateway` option: ```ruby docker.vm.network :public_network, type: "dhcp", bridge: "eth0", docker_network__gateway: "192.168.1.2" ``` More examples are shared below which demonstrate creating a few common network interfaces. ## Docker Network Example The following Vagrantfile will generate these networks for a container: 1. A IPv4 IP address assigned by DHCP 2. A IPv4 IP address 172.20.128.2 on a network with subnet 172.20.0.0/16 3. A IPv6 IP address assigned by DHCP on subnet 2a02:6b8:b010:9020:1::/80 ```ruby Vagrant.configure("2") do |config| config.vm.define "docker" do |docker| docker.vm.network :private_network, type: "dhcp", docker_network__internal: true docker.vm.network :private_network, ip: "172.20.128.2", netmask: "16" docker.vm.network :private_network, type: "dhcp", subnet: "2a02:6b8:b010:9020:1::/80" docker.vm.provider "docker" do |d| d.build_dir = "docker_build_dir" end end end ``` You can test that your container has the proper configured networks by looking at the result of running `ip addr`, for example: ``` brian@localghost:vagrant-sandbox % docker ps ±[●][master] CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 370f4e5d2217 196a06ef12f5 "tail -f /dev/null" 5 seconds ago Up 3 seconds 80/tcp, 443/tcp vagrant-sandbox_docker-1_1551810440 brian@localghost:vagrant-sandbox % docker exec 370f4e5d2217 ip addr ±[●][master] 1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 24: eth0@if25: mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever 27: eth1@if28: mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.19.0.2/16 brd 172.19.255.255 scope global eth1 valid_lft forever preferred_lft forever 30: eth2@if31: mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:14:80:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.20.128.2/16 brd 172.20.255.255 scope global eth2 valid_lft forever preferred_lft forever 33: eth3@if34: mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:15:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.21.0.2/16 brd 172.21.255.255 scope global eth3 valid_lft forever preferred_lft forever inet6 2a02:6b8:b010:9020:1::2/80 scope global nodad valid_lft forever preferred_lft forever inet6 fe80::42:acff:fe15:2/64 scope link valid_lft forever preferred_lft forever ``` You can also connect your containers to a docker network that was created outside of Vagrant: ```shell-session $ docker network create my-custom-network --subnet=172.20.0.0/16 ``` ```ruby Vagrant.configure("2") do |config| config.vm.define "docker" do |docker| docker.vm.network :private_network, type: "dhcp", name: "my-custom-network" docker.vm.provider "docker" do |d| d.build_dir = "docker_build_dir" end end end ``` Vagrant will not delete or modify these outside networks when deleting the container, however. ## Useful Debugging Tips The `docker network` command provides some helpful insights to what might be going on with the networks Vagrant creates. For example, if you want to know what networks you currently have running on your machine, you can run the `docker network ls` command: ``` brian@localghost:vagrant-sandbox % docker network ls ±[●][master] NETWORK ID NAME DRIVER SCOPE a2bfc26bd876 bridge bridge local 2a2845e77550 host host local f36682aeba68 none null local 00d4986c7dc2 vagrant_network bridge local d02420ff4c39 vagrant_network_2a02:6b8:b010:9020:1::/80 bridge local 799ae9dbaf98 vagrant_network_172.20.0.0/16 bridge local ``` You can also inspect any network for more information: ``` brian@localghost:vagrant-sandbox % docker network inspect vagrant_network ±[●][master] [ { "Name": "vagrant_network", "Id": "00d4986c7dc2ed7bf1961989ae1cfe98504c711f9de2f547e5dfffe2bb819fc2", "Created": "2019-03-05T10:27:21.558824922-08:00", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "172.19.0.0/16", "Gateway": "172.19.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "370f4e5d2217e698b16376583fbf051dd34018e5fd18958b604017def92fea63": { "Name": "vagrant-sandbox_docker-1_1551810440", "EndpointID": "166b7ca8960a9f20a150bb75a68d07e27e674781ed9f916e9aa58c8bc2539a61", "MacAddress": "02:42:ac:13:00:02", "IPv4Address": "172.19.0.2/16", "IPv6Address": "" } }, "Options": {}, "Labels": {} } ] ``` ## Caveats For now, Vagrant only looks at the subnet when figuring out if it should create a new network for a guest container. If you bring up a container with a network, and then change or add some new options (but leave the subnet the same), it will not apply those changes or create a new network. Because the `--link` flag for the `docker network connect` command is considered legacy, Vagrant does not support that option when creating containers and connecting networks. ## More Information For more information on how docker manages its networks, please refer to their documentation: - https://docs.docker.com/network/ - https://docs.docker.com/engine/reference/commandline/network/ ================================================ FILE: website/content/docs/providers/hyperv/boxes.mdx ================================================ --- layout: docs page_title: Creating a Base Box - Hyper-V Provider description: |- As with every Vagrant provider, the Vagrant Hyper-V provider has a custom box format that affects how base boxes are made. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Creating a Base Box As with [every Vagrant provider](/vagrant/docs/providers/basic_usage), the Vagrant Hyper-V provider has a custom box format that affects how base boxes are made. Prior to reading this, you should read the [general guide to creating base boxes](/vagrant/docs/boxes/base). Actually, it would probably be most useful to keep this open in a separate tab as you may be referencing it frequently while creating a base box. That page contains important information about common software to install on the box. Additionally, it is helpful to understand the [basics of the box file format](/vagrant/docs/boxes/format). ~> **Advanced topic!** This is a reasonably advanced topic that a beginning user of Vagrant does not need to understand. If you are just getting started with Vagrant, skip this and use an available box. If you are an experienced user of Vagrant and want to create your own custom boxes, this is for you. ## Additional Software In addition to the software that should be installed based on the [general guide to creating base boxes](/vagrant/docs/boxes/base), Hyper-V base boxes require some additional software. ### Hyper-V Kernel Modules You will need to install Hyper-V kernel modules. While this improves performance, it also enables necessary features such as reporting its IP address so that Vagrant can access it. You can verify Hyper-V kernel modules are properly installed by running `lsmod` on Linux machines and looking for modules prefixed with `hv_`. Additionally, you will need to verify that the "Network" tab for your virtual machine in the Hyper-V manager is reporting an IP address. If it is not reporting an IP address, Vagrant will not be able to access it. For most newer Linux distributions, the Hyper-V modules will be available out of the box. Ubuntu 12.04 requires some special steps to make networking work. These are reproduced here in case similar steps are needed with other distributions. Without these commands, Ubuntu 12.04 will not report an IP address to Hyper-V: ```shell-session $ sudo apt-get install linux-tools-3.11.0-15-generic $ sudo apt-get install hv-kvp-daemon-init $ sudo cp /usr/lib/linux-tools/3.11.0-15/hv_* /usr/sbin/ ``` ## Packaging the Box To package a Hyper-V box, export the virtual machine from the Hyper-V Manager using the "Export" feature. This will create a directory with a structure similar to the following: ``` . |-- Snapshots |-- Virtual Hard drives |-- Virtual Machines ``` Delete the "Snapshots" folder. It is of no use to the Vagrant Hyper-V provider and can only add to the size of the box if there are snapshots in that folder. Then, create the "metadata.json" file necessary for the box, as documented in [basics of the box file format](/vagrant/docs/boxes/format). The proper provider value to use for the metadata is "hyperv". Finally, create an archive of those contents (but _not_ the parent folder) using a tool such as `tar`: ```shell-session $ tar cvzf ~/custom.box ./* ``` A common mistake is to also package the parent folder by accident. Vagrant will not work in this case. To verify you've packaged it properly, add the box to Vagrant and try to bring up the machine. ## Additional Help There is also some less structured help available from the experience of other users. These are not official documentation but if you are running into trouble they may help you: - [Ubuntu 14.04.2 without secure boot](https://github.com/hashicorp/vagrant/issues/5419#issuecomment-86235427) ================================================ FILE: website/content/docs/providers/hyperv/configuration.mdx ================================================ --- layout: docs page_title: Configuration- Hyper-V Provider description: |- The Vagrant Hyper-V provider has some provider-specific configuration options you may set. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Configuration The Vagrant Hyper-V provider has some provider-specific configuration options you may set. A complete reference is shown below: - `auto_start_action` (Nothing, StartIfRunning, Start) - Automatic start action for VM on host startup. Default: Nothing. - `auto_stop_action` (ShutDown, TurnOff, Save) - Automatic stop action for VM on host shutdown. Default: ShutDown. - `cpus` (integer) - Number of virtual CPUs allocated to VM at startup. - `differencing_disk` (boolean) - **Deprecated** Use differencing disk instead of cloning entire VHD (use `linked_clone` instead) Default: false. - `enable_virtualization_extensions` (boolean) - Enable virtualization extensions for the virtual CPUs. Default: false - `enable_checkpoints` (boolean) Enable checkpoints of the VM. Default: true - `enable_automatic_checkpoints` (boolean) Enable automatic checkpoints of the VM. Default: false - `enable_enhanced_session_mode` (boolean) - Enable enhanced session transport type for the VM. Default: false - `ip_address_timeout` (integer) - Number of seconds to wait for the VM to report an IP address. Default: 120. - `linked_clone` (boolean) - Use differencing disk instead of cloning entire VHD. Default: false - `mac` (string) - MAC address for the guest network interface - `maxmemory` (integer) - Maximum number of megabytes allowed to be allocated for the VM. When set Dynamic Memory Allocation will be enabled. Set to `nil` to disable Dynamic Memory. - `memory` (integer) - Number of megabytes allocated to VM at startup. If `maxmemory` is set, this will be amount of memory allocated at startup. - `vlan_id` (integer) - VLAN ID for the guest network interface. - `vmname` (string) - Name of virtual machine as shown in Hyper-V manager. Default: Generated name. - `vm_integration_services` (Hash) - Hash to set the state of integration services. (Note: Unknown key values will be passed directly.) - `guest_service_interface` (boolean) - `heartbeat` (boolean) - `key_value_pair_exchange` (boolean) - `shutdown` (boolean) - `time_synchronization` (boolean) - `vss` (boolean) ## VM Integration Services The `vm_integration_services` configuration option consists of a simple Hash. The key values are the names of VM integration services to enable or disable for the VM. Vagrant includes an internal mapping of known services which allows them to be provided in a "snake case" format. When a provided key is unknown, the key value is used "as-is" without any modifications. For example, if a new `CustomVMSRV` VM integration service was added and Vagrant is not aware of this new service name, it can be provided as the key value explicitly: ```ruby config.vm.provider "hyperv" do |h| h.vm_integration_services = { guest_service_interface: true, CustomVMSRV: true } end ``` This example would enable the `GuestServiceInterface` (which Vagrant is aware) and `CustomVMSRV` (which Vagrant is _not_ aware) VM integration services. ================================================ FILE: website/content/docs/providers/hyperv/index.mdx ================================================ --- layout: docs page_title: Hyper-V Provider description: |- Vagrant comes with support out of the box for Hyper-V, a native hypervisor written by Microsoft. Hyper-V is available by default for almost all Windows 8.1 and later installs. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Hyper-V Vagrant comes with support out of the box for [Hyper-V](https://en.wikipedia.org/wiki/Hyper-V), a native hypervisor written by Microsoft. Hyper-V is available by default for almost all Windows 8.1 and later installs. The Hyper-V provider is compatible with Windows 8.1 and later only. Prior versions of Hyper-V do not include the necessary APIs for Vagrant to work. Hyper-V must be enabled prior to using the provider. Most Windows installations will not have Hyper-V enabled by default. Hyper-V is available by default for almost all Windows Enterprise, Professional, or Education 8.1 and later installs. To enable Hyper-V, go to "Programs and Features", click on "Turn Windows features on or off" and check the box next to "Hyper-V". Or install via PowerShell with: ```powershell Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All ``` See official documentation [here](https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v). ================================================ FILE: website/content/docs/providers/hyperv/limitations.mdx ================================================ --- layout: docs page_title: Limitations - Hyper-V Provider description: |- The Hyper-V provider works in almost every way like the VirtualBox or VMware provider would, but has some limitations that are inherent to Hyper-V itself. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Limitations The Vagrant Hyper-V provider works in almost every way like the VirtualBox or VMware provider would, but has some limitations that are inherent to Hyper-V itself. ## Limited Networking Vagrant does not yet know how to create and configure new networks for Hyper-V. When launching a machine with Hyper-V, Vagrant will prompt you asking what virtual switch you want to connect the virtual machine to. A result of this is that networking configurations in the Vagrantfile are completely ignored with Hyper-V. Vagrant cannot enforce a static IP or automatically configure a NAT. However, the IP address of the machine will be reported as part of the `vagrant up`, and you can use that IP address as if it were a host only network. ## Snapshots Restoring snapshot VMs using `vagrant snapshot pop` or `vagrant snapshot restore` will sometimes raise errors when mounting SMB shared folders, however these mounts will still work inside the guest. ================================================ FILE: website/content/docs/providers/hyperv/usage.mdx ================================================ --- layout: docs page_title: Usage - Hyper-V Provider description: |- The Hyper-V provider is used just like any other provider. Please read the general basic usage page for providers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Usage The Vagrant Hyper-V provider is used just like any other provider. Please read the general [basic usage](/vagrant/docs/providers/basic_usage) page for providers. The value to use for the `--provider` flag is `hyperv`. Hyper-V also requires that you execute Vagrant with administrative privileges. Creating and managing virtual machines with Hyper-V requires admin rights. Vagrant will show you an error if it does not have the proper permissions. Boxes for Hyper-V can be easily found on [HashiCorp's Vagrant Cloud](https://vagrantcloud.com/boxes/search). To get started, you might want to try the `hashicorp/bionic64` box. ================================================ FILE: website/content/docs/providers/index.mdx ================================================ --- layout: docs page_title: Providers description: |- While Vagrant ships out of the box with support for VirtualBox, Hyper-V, and Docker. Vagrant has the ability to manage other types of machines as well. This is done by using other providers with Vagrant. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Providers While Vagrant ships out of the box with support for [VirtualBox](https://www.virtualbox.org), [Hyper-V](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/about/), and [Docker](https://www.docker.io), Vagrant has the ability to manage other types of machines as well. This is done by using other _providers_ with Vagrant. Alternate providers can offer different features that make more sense in your use case. For example, if you are using Vagrant for any real work, [VMware](https://www.vmware.com) providers are recommended since they're well supported and generally more stable and performant than VirtualBox. Before you can use another provider, you must install it. Installation of other providers is done via the Vagrant plugin system. Once the provider is installed, usage is straightforward and simple, as you would expect with Vagrant. Read into the relevant subsections found in the navigation to the left for more information. ================================================ FILE: website/content/docs/providers/installation.mdx ================================================ --- layout: docs page_title: Installation - Providers description: |- Providers are distributed as Vagrant plugins, and are therefore installed using standard plugin installation steps. After installing a plugin which contains a provider, the provider should immediately be available. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Provider Installation Providers are distributed as Vagrant plugins, and are therefore installed using [standard plugin installation steps](/vagrant/docs/plugins/usage). After installing a plugin which contains a provider, the provider should immediately be available. ================================================ FILE: website/content/docs/providers/virtualbox/boxes.mdx ================================================ --- layout: docs page_title: Creating a Base Box - VirtualBox Provider description: |- As with every Vagrant provider, the Vagrant VirtualBox provider has a custom box format that affects how base boxes are made. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Creating a Base Box As with [every Vagrant provider](/vagrant/docs/providers/basic_usage), the Vagrant VirtualBox provider has a custom box format that affects how base boxes are made. Prior to reading this, you should read the [general guide to creating base boxes](/vagrant/docs/boxes/base). Actually, it would probably be most useful to keep this open in a separate tab as you may be referencing it frequently while creating a base box. That page contains important information about common software to install on the box. Additionally, it is helpful to understand the [basics of the box file format](/vagrant/docs/boxes/format). ~> **Advanced topic!** This is a reasonably advanced topic that a beginning user of Vagrant does not need to understand. If you are just getting started with Vagrant, skip this and use an available box. If you are an experienced user of Vagrant and want to create your own custom boxes, this is for you. ## Virtual Machine The virtual machine created in VirtualBox can use any configuration you would like, but Vagrant has some hard requirements: - The first network interface (adapter 1) _must_ be a NAT adapter. Vagrant uses this to connect the first time. - The MAC address of the first network interface (the NAT adapter) should be noted, since you will need to put it in a Vagrantfile later as the value for `config.vm.base_mac`. To get this value, use the VirtualBox GUI. Other than the above, you are free to customize the base virtual machine as you see fit. ## Additional Software In addition to the software that should be installed based on the [general guide to creating base boxes](/vagrant/docs/boxes/base), VirtualBox base boxes require some additional software. ### VirtualBox Guest Additions [VirtualBox Guest Additions](https://www.virtualbox.org/manual/ch04.html) must be installed so that things such as shared folders can function. Installing guest additions also usually improves performance since the guest OS can make some optimizations by knowing it is running within VirtualBox. Before installing the guest additions, you will need the Linux kernel headers and the basic developer tools. On Ubuntu, you can easily install these like so: ```shell-session $ sudo apt-get install linux-headers-$(uname -r) build-essential dkms ``` #### To install via the GUI: Next, make sure that the guest additions image is available by using the GUI and clicking on "Devices" followed by "Install Guest Additions". Then mount the CD-ROM to some location. On Ubuntu, this usually looks like this: ```shell-session $ sudo mount /dev/cdrom /media/cdrom ``` Finally, run the shell script that matches your system to install the guest additions. For example, for Linux on x86, it is the following: ```shell-session $ sudo sh /media/cdrom/VBoxLinuxAdditions.run ``` If the command succeeds, then the guest additions are now installed! #### To install via the command line: You can find the appropriate guest additions version to match your VirtualBox version by selecting the appropriate version [here](http://download.virtualbox.org/virtualbox/). The examples below use 4.3.8, which was the latest VirtualBox version at the time of writing. ```text wget http://download.virtualbox.org/virtualbox/4.3.8/VBoxGuestAdditions_4.3.8.iso sudo mkdir /media/VBoxGuestAdditions sudo mount -o loop,ro VBoxGuestAdditions_4.3.8.iso /media/VBoxGuestAdditions sudo sh /media/VBoxGuestAdditions/VBoxLinuxAdditions.run rm VBoxGuestAdditions_4.3.8.iso sudo umount /media/VBoxGuestAdditions sudo rmdir /media/VBoxGuestAdditions ``` If you did not install a Desktop environment when you installed the operating system, as recommended to reduce size, the install of the VirtualBox additions should warn you about the lack of OpenGL or Window System Drivers, but you can safely ignore this. If the commands succeed, then the guest additions are now installed! ## Packaging the Box Vagrant includes a simple way to package VirtualBox base boxes. Once you've installed all the software you want to install, you can run this command: ```shell-session $ vagrant package --base my-virtual-machine ``` Where "my-virtual-machine" is replaced by the name of the virtual machine in VirtualBox to package as a base box. It will take a few minutes, but after it is complete, a file "package.box" should be in your working directory which is the new base box. At this point, you've successfully created a base box! ## Raw Contents This section documents the actual raw contents of the box file. This is not as useful when creating a base box but can be useful in debugging issues if necessary. A VirtualBox base box is an archive of the resulting files of [exporting](https://www.virtualbox.org/manual/ch08.html#vboxmanage-export) a VirtualBox virtual machine. Here is an example of what is contained in such a box: ```shell-session $ tree . |-- Vagrantfile |-- box-disk1.vmdk |-- box.ovf |-- metadata.json 0 directories, 4 files ``` In addition to the files from exporting a VirtualBox VM, there is the "metadata.json" file used by Vagrant itself. Also, there is a "Vagrantfile." This contains some configuration to properly set the MAC address of the NAT network device, since VirtualBox requires this to be correct in order to function properly. If you are not using `vagrant package --base` above, you will have to set the `config.vm.base_mac` setting in this Vagrantfile to the MAC address of the NAT device without colons. When bringing up a VirtualBox backed machine, Vagrant [imports](https://www.virtualbox.org/manual/ch08.html#vboxmanage-import) the "box.ovf" file found in the box contents. ================================================ FILE: website/content/docs/providers/virtualbox/common-issues.mdx ================================================ --- layout: docs page_title: Common Issues - VirtualBox Provider description: |- This page lists some common issues people run into with Vagrant and VirtualBox as well as solutions for those issues. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Common Issues This page lists some common issues people run into with Vagrant and VirtualBox as well as solutions for those issues. ## Hanging on Windows If Vagrant commands are hanging on Windows because they're communicating to VirtualBox, this may be caused by a permissions issue with VirtualBox. This is easy to fix. Starting VirtualBox as a normal user or as an administrator will prevent you from using it in the opposite way. Please keep in mind that when Vagrant interacts with VirtualBox, it will interact with it with the same access level as the console running Vagrant. To fix this issue, completely shut down all VirtualBox machines and GUIs. Wait a few seconds. Then, launch VirtualBox only with the access level you wish to use. ## DNS Not Working If DNS is not working within your VM, then you may need to enable a DNS proxy (built-in to VirtualBox). Please [see the StackOverflow answers here](https://serverfault.com/questions/453185/vagrant-virtualbox-dns-10-0-2-3-not-working) for a guide on how to do that. ================================================ FILE: website/content/docs/providers/virtualbox/configuration.mdx ================================================ --- layout: docs page_title: Configuration - VirtualBox Provider description: |- The VirtualBox provider exposes some additional configuration options that allow you to more finely control your VirtualBox-powered Vagrant environments. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Configuration The VirtualBox provider exposes some additional configuration options that allow you to more finely control your VirtualBox-powered Vagrant environments. ## GUI vs. Headless By default, VirtualBox machines are started in headless mode, meaning there is no UI for the machines visible on the host machine. Sometimes, you want to have a UI. Common use cases include wanting to see a browser that may be running in the machine, or debugging a strange boot issue. You can easily tell the VirtualBox provider to boot with a GUI: ```ruby config.vm.provider "virtualbox" do |v| v.gui = true end ``` ## Virtual Machine Name You can customize the name that appears in the VirtualBox GUI by setting the `name` property. By default, Vagrant sets it to the containing folder of the Vagrantfile plus a timestamp of when the machine was created. By setting another name, your VM can be more easily identified. ```ruby config.vm.provider "virtualbox" do |v| v.name = "my_vm" end ``` ## Default NIC Type By default Vagrant will not set the NIC type for network interfaces. This allows VirtualBox to apply the default NIC type for the guest. If you would like to use a specific NIC type by default for guests, set the `default_nic_type` option: ```ruby config.vm.provider "virtualbox" do |v| v.default_nic_type = "82543GC" 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.linked_clone = true end ``` To have backward compatibility: ```ruby config.vm.provider "virtualbox" do |v| v.linked_clone = true if Gem::Version.new(Vagrant::VERSION) >= Gem::Version.new('1.8.0') end ``` If you do not want backward compatibility and want to force users to support linked cloning, you can use `Vagrant.require_version` with 1.8. -> **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 removed when there are no linked clones connected to it. ## Checking for Guest Additions By default Vagrant will check for the [VirtualBox Guest Additions](https://www.virtualbox.org/manual/ch04.html) when starting a machine, and will output a warning if the guest additions are missing or out-of-date. You can skip the guest additions check by setting the `check_guest_additions` option: ```ruby config.vm.provider "virtualbox" do |v| v.check_guest_additions = false end ``` ## VBoxManage Customizations [VBoxManage](https://www.virtualbox.org/manual/ch08.html) is a utility that can be used to make modifications to VirtualBox virtual machines from the command line. Vagrant exposes a way to call any command against VBoxManage just prior to booting the machine: ```ruby config.vm.provider "virtualbox" do |v| v.customize ["modifyvm", :id, "--cpuexecutioncap", "50"] end ``` In the example above, the VM is modified to have a host CPU execution cap of 50%, meaning that no matter how much CPU is used in the VM, no more than 50% would be used on your own host machine. Some details: - The `:id` special parameter is replaced with the ID of the virtual machine being created, so when a VBoxManage command requires an ID, you can pass this special parameter. - Multiple `customize` directives can be used. They will be executed in the order given. There are some convenience shortcuts for memory and CPU settings: ```ruby config.vm.provider "virtualbox" do |v| v.memory = 1024 v.cpus = 2 end ``` ================================================ FILE: website/content/docs/providers/virtualbox/index.mdx ================================================ --- layout: docs page_title: VirtualBox Provider description: |- Vagrant comes with support out of the box for VirtualBox, a free, cross-platform consumer virtualization product. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # VirtualBox Vagrant comes with support out of the box for [VirtualBox](https://www.virtualbox.org), a free, cross-platform consumer virtualization product. The VirtualBox provider is compatible with VirtualBox versions 4.0.x, 4.1.x, 4.2.x, 4.3.x, 5.0.x, 5.1.x, 5.2.x, 6.0.x, 6.1.x, 7.0.x, 7.1.x, and 7.2.x. Other versions are unsupported and the provider will display an error message. Please note that beta and pre-release versions VirtualBox are not supported and may not be well-behaved. VirtualBox must be installed on its own prior to using the provider, or the provider will display an error message asking you to install it. VirtualBox can be installed by [downloading](https://www.virtualbox.org/wiki/Downloads) a package or installer for your operating system and using standard procedures to install that package. Use the navigation to the left to find a specific VirtualBox topic to read more about. ================================================ FILE: website/content/docs/providers/virtualbox/networking.mdx ================================================ --- layout: docs page_title: Networking - VirtualBox Provider description: |- The Vagrant VirtualBox provider supports using the private network as a VirtualBox internal network. By default, private networks are host-only networks, because those are the easiest to work with. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Networking ## VirtualBox Host-Only Networks By default, private networks are [host-only networks](https://www.virtualbox.org/manual/ch06.html#network_hostonly), because those are the easiest to work with. In VirtualBox, since you can create multiple host-only networks, it is also possible to specify which host-only network you want the Vagrant VirtualBox provider to use for a given interface. To do this, use the `name` argument with the name of the host-only interface to use. ```ruby Vagrant.configure("2") do |config| config.vm.network "private_network", type: "dhcp", name: "vboxnet3" end ``` ## VirtualBox Internal Network The Vagrant VirtualBox provider supports using the private network as a VirtualBox [internal network](https://www.virtualbox.org/manual/ch06.html#network_internal). By default, private networks are host-only networks, because those are the easiest to work with. However, internal networks can be enabled as well. To specify a private network as an internal network for VirtualBox use the `virtualbox__intnet` option with the network. The `virtualbox__` (double underscore) prefix tells Vagrant that this option is only for the VirtualBox provider. ```ruby Vagrant.configure("2") do |config| config.vm.network "private_network", ip: "192.168.50.4", virtualbox__intnet: true end ``` Additionally, if you want to specify that the VirtualBox provider join a specific internal network, specify the name of the internal network: ```ruby Vagrant.configure("2") do |config| config.vm.network "private_network", ip: "192.168.50.4", virtualbox__intnet: "mynetwork" end ``` ## VirtualBox NIC Type You can specify a specific NIC type for the created network interface by using the `nic_type` parameter. This is not prefixed by `virtualbox__` for legacy reasons, but is VirtualBox-specific. This is an advanced option and should only be used if you know what you are using, since it can cause the network device to not work at all. Example: ```ruby Vagrant.configure("2") do |config| config.vm.network "private_network", ip: "192.168.50.4", nic_type: "virtio" end ``` ================================================ FILE: website/content/docs/providers/virtualbox/usage.mdx ================================================ --- layout: docs page_title: Usage - VirtualBox Provider description: |- The Vagrant VirtualBox provider is used just like any other provider. Please read the general basic usage page for providers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Usage The Vagrant VirtualBox provider is used just like any other provider. Please read the general [basic usage](/vagrant/docs/providers/basic_usage) page for providers. The value to use for the `--provider` flag is `virtualbox`. The Vagrant VirtualBox provider does not support parallel execution at this time. Specifying the `--parallel` option will have no effect. ================================================ FILE: website/content/docs/providers/vmware/boxes.mdx ================================================ --- layout: docs page_title: Box Format - VMware Provider description: |- As with every Vagrant provider, the Vagrant VMware providers have a custom box format. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Boxes As with [every Vagrant provider](/vagrant/docs/providers/basic_usage), the Vagrant VMware providers have a custom box format. This page documents the format so that you can create your own base boxes. Note that currently you must make these base boxes by hand. A future release of Vagrant will provide additional mechanisms for automatically creating such images. -> **Note:** This is a reasonably advanced topic that a beginning user of Vagrant does not need to understand. If you are just getting started with Vagrant, skip this and use an available box. If you are an experienced user of Vagrant and want to create your own custom boxes, this is for you. Prior to reading this page, please understand the [basics of the box file format](/vagrant/docs/boxes/format). ## Contents A VMware base box is a compressed archive of the necessary contents of a VMware "vmwarevm" file. Here is an example of what is contained in such a box: ```shell-session $ tree . |-- disk-s001.vmdk |-- disk-s002.vmdk |-- ... |-- disk.vmdk |-- metadata.json |-- bionic64.nvram |-- bionic64.vmsd |-- bionic64.vmx |-- bionic64.vmxf 0 directories, 17 files ``` The files that are strictly required for a VMware machine to function are: nvram, vmsd, vmx, vmxf, and vmdk files. There is also the "metadata.json" file used by Vagrant itself. This file contains nothing but the defaults which are documented on the [box format](/vagrant/docs/boxes/format) page. When bringing up a VMware backed machine, Vagrant copies all of the contents in the box into a privately managed "vmwarevm" folder, and uses the first "vmx" file found to control the machine. -> **Vagrant 1.8 and higher support linked clones**. Prior versions of Vagrant do not support linked clones. For more information on linked clones, please see the documentation. ## VMX Allowlisting Settings in the VMX file control the behavior of the VMware virtual machine when it is booted. In the past Vagrant has removed the configured network device when creating a new instance and inserted a new configuration. With the introduction of ["predictable network interface names"][iface-names] this approach can cause unexpected behaviors or errors with VMware Vagrant boxes. While some boxes that use the predictable network interface names are configured to handle the VMX modifications Vagrant makes, it is better if Vagrant does not make the modification at all. Vagrant will now warn if a allowlisted setting is detected within a Vagrant box VMX file. If it is detected, a warning will be shown alerting the user and providing a configuration snippet. The configuration snippet can be used in the Vagrantfile if Vagrant fails to start the virtual machine. ### Making compatible boxes These are the VMX settings the allowlisting applies to: - `ethernet*.pcislotnumber` If the newly created box does not depend on Vagrant's existing behavior of modifying this setting, it can disable Vagrant from applying the modification by adding a Vagrantfile to the box with the following content: ```ruby Vagrant.configure("2") do |config| config.vm.provider "vmware_desktop" do |vmware| vmware.allowlist_verified = true end end ``` This will prevent Vagrant from displaying a warning to the user as well as disable the VMX settings modifications. ## Installed Software Base boxes for VMware should have the following software installed, as a bare minimum: - SSH server with key-based authentication setup. If you want the box to work with default Vagrant settings, the SSH user must be set to accept the [insecure keypair](https://github.com/hashicorp/vagrant/blob/main/keys/vagrant.pub) that ships with Vagrant. - [VMware Tools](https://kb.vmware.com/kb/340) so that things such as shared folders can function. There are many other benefits to installing the tools, such as improved networking performance. ## Optimizing Box Size Prior to packaging up a box, you should shrink the hard drives as much as possible. This can be done with `vmware-vdiskmanager` which is usually found in `/Applications/VMware Fusion.app/Contents/Library` for VMware Fusion. You first want to defragment then shrink the drive. Usage shown below: ```shell-session $ vmware-vdiskmanager -d /path/to/main.vmdk ... $ vmware-vdiskmanager -k /path/to/main.vmdk ... ``` ## Packaging Remove any extraneous files from the "vmwarevm" folder and package it. Be sure to compress the tar with gzip (done below in a single command) since VMware hard disks are not compressed by default. ```shell-session $ cd /path/to/my/vm.vmwarevm $ tar cvzf custom.box ./* ``` [iface-names]: https://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames/ ================================================ FILE: website/content/docs/providers/vmware/configuration.mdx ================================================ --- layout: docs page_title: Configuration - VMware Provider description: |- While Vagrant VMware providers are a drop-in replacement for VirtualBox, there are some additional features that are exposed that allow you to more finely configure VMware-specific aspects of your machines. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Configuration While Vagrant VMware Desktop provider is a drop-in replacement for VirtualBox, there are some additional features that are exposed that allow you to more finely configure VMware-specific aspects of your machines. Configuration settings for the provider are set in the Vagrantfile: ```ruby Vagrant.configure("2") do |config| config.vm.box = "my-box" config.vm.provider "vmware_desktop" do |v| v.gui = true end end ``` ## Provider settings - `allowlist_verified` (bool, symbol) - Flag that VMware box has been properly configured for allow list VMX settings. `true` if verified, `false` if unverified, `:disable_warning` to silence allowlist warnings. - `base_address` (string) - Address to be reserved from the DHCP server. This option requires the `base_mac` option to be set as well. - `base_mac` (string) - Custom MAC address to be used for the default NAT interface device - `clone_directory` (string) - Path for storing VMware clones. This value can also be set using the `VAGRANT_VMWARE_CLONE_DIRECTORY` environment variable. This defaults to `./.vagrant` - `enable_vmrun_ip_lookup` (bool) - Use vmrun to discover guest IP address. This defaults to `true` - `functional_hgfs` (bool) - HGFS is functional within the guest. This defaults to detected capability of the guest - `gui` (bool) - Launch guest with a GUI. This defaults to `false` - `linked_clone` (bool) - Use linked clones instead of full copy clones. This defaults to `true` if linked clones are functional based on VMware installation. - `nat_device` (string) - The host vmnet device to use for the default NAT interface. By default this will be automatically detected with a fallback to `vmnet8`. - `port_forward_network_pause` - Number of seconds to pause after applying port forwarding configuration. This allows guest time to acquire DHCP address if previous address is dropped when VMware network services are restarted. This defaults to `0` - `ssh_info_public` (bool) - Use the public IP address for SSH connections to guest. This defaults to `false` - `unmount_default_hgfs` (bool) - Unmount the default HGFS mount point within the guest. This defaults to `false` - `utility_port` (integer) - Listen port of the Vagrant VMware Utility service. This defaults to `9922` - `utility_certificate_path` (string) - Path to the Vagrant VMware Utility service certificates directory. The default value is dependent on the host - `verify_vmnet` (bool) - Verify vmnet devices health before usage. This defaults to `true` - `vmx` (hash) - VMX key/value pairs to set or unset. If the value is `nil`, the key will be deleted. ### VM Clone Directory By default, the VMware provider will clone the VMware VM in the box to the ".vagrant" folder relative to the folder where the Vagrantfile is. Usually, this is fine. For some people, for example those who use a differential backup software such as Time Machine, this is very annoying because you cannot regularly ignore giant virtual machines as part of backups. The directory where the provider clones the virtual machine can be customized by setting the `VAGRANT_VMWARE_CLONE_DIRECTORY` environmental variable. This does not need to be unique per project. Each project will get a different sub-directory within this folder. Therefore, it is safe to set this systemwide. ### Linked Clones By default new machines are created using a linked clone to the base box. This reduces the time and required disk space incurred by directly importing the base box. 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. To disable linked clones: ```ruby config.vm.provider "vmware_desktop" do |v| v.linked_clone = false end ``` ### VMX Customization If you want to add or remove specific keys from the VMX file, you can do that: ```ruby config.vm.provider "vmware_desktop" do |v| v.vmx["custom-key"] = "value" v.vmx["another-key"] = nil end ``` In the example above, the "custom-key" key will be set to "value" and the "another-key" key will be removed from the VMX file. VMX customization is done as the final step before the VMware machine is booted, so you have the ability to possibly undo or misconfigure things that Vagrant has set up itself. VMX is an undocumented format and there is no official reference for the available keys and values. This customization option is exposed for people who have knowledge of exactly what they want. The most common keys people look for are setting memory and CPUs. The example below sets both: ```ruby config.vm.provider "vmware_desktop" do |v| v.vmx["memsize"] = "1024" v.vmx["numvcpus"] = "2" end ``` ================================================ FILE: website/content/docs/providers/vmware/faq.mdx ================================================ --- layout: docs page_title: Frequently Asked Questions - VMware Provider description: |- Frequently asked questions related to using Vagrant with VMware Workstation and VMware Fusion --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Frequently Asked Questions ## Q: How do I use the VMware Fusion Tech Preview? The Vagrant VMware utility expects VMware Fusion to be installed in a specific path on macOS. Since the VMware Fusion Tech Preview installs into a different path, the Vagrant VMware utility will fail to detect the installation and will not start. To resolve this you can create a symlink from the VMware Fusion Tech Preview installation path to the normal VMware Fusion installation path: ```shell-session sudo ln -s /Applications/VMware\ Fusion\ Tech\ Preview.app /Applications/VMware\ Fusion.app ``` ## Q: How do I upgrade my currently installed Vagrant VMware plugin? You can update the Vagrant VMware plugin to the latest version by re-running the install command: ```shell-session $ vagrant plugin install vagrant-vmware-desktop ``` To upgrade the Vagrant VMware utility, download the latest version from the [Vagrant VMware utility downloads page](/vagrant/downloads/vmware) and install the system package to your local system. ## Q: Do I need a license for the Vagrant VMware plugin? No, the Vagrant VMware plugin has been open sourced under the MPL. A license is no longer required for using the Vagrant VMware plugin. If the plugin you are using still requires a license, simply upgrade your plugin: ``` $ vagrant plugin update vagrant-vmware-desktop ``` ================================================ FILE: website/content/docs/providers/vmware/index.mdx ================================================ --- layout: docs page_title: VMware Provider description: |- HashiCorp develops an official VMware Fusion and VMware Workstation provider for Vagrant. This provider allows Vagrant to power VMware based machines and take advantage of the improved stability and performance that VMware software offers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # VMware [HashiCorp](https://www.hashicorp.com) develops an official [VMware Fusion](https://www.vmware.com/products/fusion/overview.html) and [VMware Workstation](https://www.vmware.com/products/workstation/)  [provider](/vagrant/docs/providers/) for Vagrant. This provider allows Vagrant to power VMware based machines and take advantage of the improved stability and performance that VMware software offers. The Vagrant VMware plugin is now open sourced under the MPL. The code repository for the Vagrant VMware plugin is available on [GitHub](https://github.com/hashicorp/vagrant-vmware-desktop). This provider is a drop-in replacement for VirtualBox. However, there are some VMware-specific things such as box formats, configurations, etc. that are documented here. Please note that VMware Fusion and VMware Workstation are third-party products that must be purchased and installed separately prior to using the provider. Use the navigation to the left to find a specific VMware topic to read more about. ================================================ FILE: website/content/docs/providers/vmware/installation.mdx ================================================ --- layout: docs page_title: Installation - VMware Provider description: |- The Vagrant VMware provider requires a two step installation process which includes a system package and a Vagrant plugin. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Installation If you are upgrading from the Vagrant VMware Workstation or Vagrant VMware Fusion plugins, please halt or destroy all VMware VMs currently being managed by Vagrant. Then continue with the instructions below. Installation of the Vagrant VMware provider requires two steps. First the Vagrant VMware Utility must be installed. This can be done by downloading and installing the correct system package from the [Vagrant VMware Utility downloads page](/vagrant/downloads/vmware). Next, install the Vagrant VMware provider plugin using the standard plugin installation procedure: ```shell-session $ vagrant plugin install vagrant-vmware-desktop ``` For more information on plugin installation, please see the [Vagrant plugin usage documentation](/vagrant/docs/plugins/usage). ## Upgrading to v1.x It is **extremely important** that the VMware plugin is upgraded to 1.0.0 or above. This release resolved critical security vulnerabilities. To learn more, please [read our release announcement](https://www.hashicorp.com/blog/introducing-the-vagrant-vmware-desktop-plugin). After upgrading, please verify that the following paths are empty. The upgrade process should remove these for you, but for security reasons it is important to double check. If you're a new user or installing the VMware provider on a new machine, you may skip this step. If you're a Windows user, you may skip this step as well. The path `~/.vagrant.d/gems/*/vagrant-vmware-{fusion,workstation}` should no longer exist. The gem `vagrant-vmware-desktop` may exist since this is the name of the new plugin. If the old directories exist, remove them. An example for a Unix-like shell is shown below: ```shell-session # Check if they exist and verify that they're the correct paths as shown below. $ ls ~/.vagrant.d/gems/*/vagrant-vmware-{fusion,workstation} ... # Remove them $ rm -rf ~/.vagrant.d/gems/*/vagrant-vmware-{fusion,workstation} ``` ## Updating the Vagrant VMware Desktop plugin The Vagrant VMware Desktop plugin can be updated directly from Vagrant. Run the following command to update Vagrant to the latest version of the Vagrant VMware Desktop plugin: ```shell-session $ vagrant plugin update vagrant-vmware-desktop ``` ================================================ FILE: website/content/docs/providers/vmware/known-issues.mdx ================================================ --- layout: docs page_title: Known Issues - VMware Provider description: |- This page tracks some known issues or limitations of the VMware provider. Note that none of these are generally blockers to using the provider, but are good to know. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Known Issues This page tracks some known issues or limitations of the VMware provider. Note that none of these are generally blockers to using the provider, but are good to know. ## Network disconnect When Vagrant applies port forwarding rules while bring up a guest instance, other running VMware VMs may experience a loss of network connectivity. The cause of this connectivity issue is the restarting of the VMware NAT service to apply new port forwarding rules. Since new rules cannot be applied to the NAT service while it is running, it is required to restart the service, which results in the loss of connectivity. ## Forwarded Ports Failing in Workstation on Windows VMware Workstation has a bug on Windows where forwarded ports do not work properly. Vagrant actually works around this bug and makes them work. However, if you run the virtual network editor on Windows, the forwarded ports will suddenly stop working. In this case, run `vagrant reload` and things will begin working again. This issue has been reported to VMware, but a fix has not been released yet. ## Big Sur Related Issues ### Creating Network Devices Starting with the Big Sur release VMware Fusion is no longer able to create network devices with custom subnet and mask settings to attach to guests. This means custom IP addresses are not valid when creating a private network. When creating a private network device to attach to a guest, simply define it with no options: ```ruby Vagrant.configure("2") do |config| config.vm.box = "hashicorp/bionic64" config.vm.network :private_network end ``` ### Port Forwarding VMware Fusion currently does not support port forwarding to the localhost. To work around this issue, the vagrant-vmware-utility provides functionality to simulate port forwarding behavior available within VMware Fusion prior to Big Sur. The port forward management is contained to the vagrant-vmware-utility and is not accessible via the VMware Fusion networking UI. ### DHCP Reservations Due VMware Fusion no longer utilizing its own service for DHCP, defining DHCP reservations is currently not working with VMware Fusion on Big Sur. ================================================ FILE: website/content/docs/providers/vmware/usage.mdx ================================================ --- layout: docs page_title: Usage - VMware Provider description: |- The Vagrant VMware providers are used just like any other provider. Please read the general basic usage page for providers. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Usage The Vagrant VMware provider is used just like any other provider. Please read the general [basic usage](/vagrant/docs/providers/basic_usage) page for providers. The value to use for the `--provider` flag is `vmware_desktop`. For compatibility with older versions of the plugin, `vmware_fusion` can be used for VMware Fusion, and `vmware_workstation` for VMware Workstation. The Vagrant VMware provider does not support parallel execution at this time. Specifying the `--parallel` option will have no effect. To get started, create a new `Vagrantfile` that points to a VMware box: ```ruby # vagrant init hashicorp/bionic64 Vagrant.configure("2") do |config| config.vm.box = "hashicorp/bionic64" end ``` Then run: ```shell-session $ vagrant up --provider vmware_desktop ``` This will download and bring up a new VMware Fusion/Workstation virtual machine in Vagrant. ================================================ FILE: website/content/docs/providers/vmware/vagrant-vmware-utility.mdx ================================================ --- layout: docs page_title: Installation - VMware Provider description: |- The Vagrant VMware Utility works with the Vagrant VMware Provider to interact with the system VMware installation. --- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ > [!IMPORTANT] > **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ # Vagrant VMware Utility Installation ## System Packages The Vagrant VMware Utility is provided as a system package. To install the utility, download and install the correct system package from the downloads page.