diff --git a/.gitignore b/.gitignore index 225e014d6..b1274f4d0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ pkg/* tags /Gemfile.lock test/tmp/ +vendor/ # Documentation _site/* @@ -31,6 +32,7 @@ doc/ # IDE junk .idea/* *.iml +.project # Ruby Managers .rbenv @@ -47,3 +49,4 @@ website/docs/Rakefile website/www/.sass-cache website/www/build website/www/Rakefile +exec/ diff --git a/.travis.yml b/.travis.yml index 9baf9785e..6fb40430b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: ruby before_install: - sudo apt-get update -qq - sudo apt-get install -qq -y bsdtar + - rvm @global do gem uninstall bundler --all --executables + - gem uninstall bundler --all --executables + - gem install bundler --version '< 1.7.0' rvm: - 2.0.0 env: diff --git a/CHANGELOG.md b/CHANGELOG.md index b9be106c2..dcb3f8ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,230 @@ +## 1.7.3 (unreleased) + +IMPROVEMENTS: + + - guests/darwin: Support inserting generated key. [GH-5204] + - guests/fedora: Support Fedora 21. [GH-5277] + - guests/redhat: Support Scientific Linux 7 [GH-5303] + - guests/solaris,solaris11: Support inserting generated key. [GH-5182] + [GH-5290] + - providers/virtualbox: regexp supported for bridge configuration. [GH-5320] + +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/cli: fix box checksum validation [GH-4665, GH-5221] + - communicators/winrm: improve error handling significantly and improve + the error messages shown to be more human-friendly. [GH-4943] + - hosts/nfs: allow colons (`:`) in NFS IDs [GH-5222] + - guests/debian: Halt works properly on Debian 8. [GH-5369] + - guests/funtoo: fix incorrect path in configure networks [GH-4812] + - 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] + - 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/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] + - 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/docker: Only add docker user to group if exists. [GH-5315] + - 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/docker: use docker.com instead of docker.io [GH-5216] + - pushes/atlas: send additional box metadata [GH-5283] + - pushes/ftp: improve check for remote directory existence [GH-5549] + - synced\_folders/rsync: add `IdentitiesOnly=yes` to the rsync command. [GH-5175] + - 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 file 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: @@ -8,6 +235,8 @@ BUG FIXES: - 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc5d3c576..bdad0b6df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ The following guidelines for contribution should be followed if you want to subm ## How to prepare * You need a [GitHub account](https://github.com/signup/free) -* Submit an [issue ticket](https://github.com/mitchellh/vagrant/issues) for your issue if the is no one yet. +* Submit an [issue ticket](https://github.com/mitchellh/vagrant/issues) for your issue if there is not one yet. * Describe the issue and include steps to reproduce when it's a bug. * Ensure to mention the earliest version that you know is affected. * If you plan on submitting a bug report, please submit debug-level logs along diff --git a/Gemfile b/Gemfile index 3ddf8ced5..15f245819 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ -source "http://rubygems.org" +source "https://rubygems.org" gemspec diff --git a/LICENSE b/LICENSE index 661398b6b..91ca85a81 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2010-2014 Mitchell Hashimoto +Copyright (c) 2010-2015 Mitchell Hashimoto Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7e1325dbf..a361da9b0 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Ruby 2.0 is needed. ### Dependencies and Unit Tests -To hack on vagrant, you'll need [bundler](http://github.com/carlhuda/bundler) which can +To hack on Vagrant, you'll need [bundler](http://github.com/carlhuda/bundler) which can be installed with a simple `gem install bundler`. Afterwards, do the following: bundle install @@ -68,7 +68,7 @@ like so: bundle exec vagrant help -**NOTE:** By default running Vagrant in via `bundle` will disable plugins. +**NOTE:** By default running Vagrant via `bundle` will disable plugins. This is necessary because Vagrant creates its own private Bundler context (it does not respect your Gemfile), because it uses Bundler to manage plugin dependencies. diff --git a/Vagrantfile b/Vagrantfile index 0f5f34c66..e928b3c6b 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,35 +1,62 @@ +# 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. -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" +Vagrant.configure("2") do |config| + config.vm.box = "hashicorp/precise64" -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - config.vm.box = "centos7" - - config.vm.define :centos7 do |centos7| - config.vm.network "private_network", ip: "192.168.11.3" - config.vm.hostname = "centos7" - # config.vm.network "public_network" - # config.ssh.forward_agent = true - config.vm.synced_folder "../puppet", "/puppet" - - config.vm.provider "virtualbox" do |vb| - vb.gui = false - # Use VBoxManage to customize the VM. For example to change memory: - vb.customize ["modifyvm", :id, "--memory", "2048"] + ["vmware_fusion", "vmware_workstation", "virtualbox"].each do |provider| + config.vm.provider provider do |v, override| + v.memory = "1024" end + end - config.vm.provision "puppet" do |puppet| - puppet.environment_path = "../puppet/environments" - puppet.environment = "testenv" - # puppet.manifests_path = "../puppet/manifests" - # puppet.manifest_file = "site.pp" - puppet.module_path = [ "../puppet/modules/public", "../puppet/modules/private" ] - # puppet.options = "--debug --verbose" - end + config.vm.provision "shell", inline: $shell - # Deprecated method: - #puppet apply --debug --verbose --modulepath '/puppet/modules/private:/puppet/modules/public:/etc/puppet/modules' - #--manifestdir /tmp/vagrant-puppet-1/manifests --detailed-exitcodes /tmp/vagrant-puppet-1/manifests/site.pp + 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 + +$shell = <<-CONTENTS +MARKER_FILE="/usr/local/etc/vagrant_provision_marker" + +# Only provision once +if [ -f "${MARKER_FILE}" ]; then + exit 0 +fi + +# Update apt +apt-get update + +# Install basic dependencies +apt-get install -y build-essential bsdtar curl + +# Install RVM +su -l -c 'curl -L https://get.rvm.io | bash -s stable' vagrant + +# Add the vagrant user to the RVM group +#usermod -a -G rvm vagrant + +# Install some Rubies +su -l -c 'rvm install 2.1.1' vagrant +su -l -c 'rvm --default use 2.1.1' vagrant + +# Output the Ruby version (for sanity) +su -l -c 'ruby --version' vagrant + +# Install Git +apt-get install -y git + +# Automatically move into the shared folder, but only add the command +# if it's not already there. +grep -q 'cd /vagrant' /home/vagrant/.bash_profile || echo 'cd /vagrant' >> /home/vagrant/.bash_profile + +# Touch the marker file so we don't do this again +touch ${MARKER_FILE} +CONTENTS diff --git a/bin/vagrant b/bin/vagrant index 21630e1fc..4fc6b96fa 100755 --- a/bin/vagrant +++ b/bin/vagrant @@ -15,7 +15,7 @@ end # Fast path the version of Vagrant if argv.include?("-v") || argv.include?("--version") - require "vagrant/version" + require_relative "../lib/vagrant/version" puts "Vagrant #{Vagrant::VERSION}" exit 0 end diff --git a/contrib/bash/completion.sh b/contrib/bash/completion.sh index 636795744..e21dc8dcf 100644 --- a/contrib/bash/completion.sh +++ b/contrib/bash/completion.sh @@ -65,7 +65,7 @@ _vagrant() { then case "$prev" in "init") - local box_list=$(find $HOME/.vagrant.d/boxes -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + local box_list=$(find "${VAGRANT_HOME:-${HOME}/.vagrant.d}/boxes" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) COMPREPLY=($(compgen -W "${box_list}" -- ${cur})) return 0 ;; @@ -111,7 +111,7 @@ _vagrant() { then case "$prev" in "remove"|"repackage") - local box_list=$(find $HOME/.vagrant.d/boxes -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + local box_list=$(find "${VAGRANT_HOME:-${HOME}/.vagrant.d}/boxes" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) COMPREPLY=($(compgen -W "${box_list}" -- ${cur})) return 0 ;; diff --git a/contrib/sudoers/linux-fedora b/contrib/sudoers/linux-fedora new file mode 100644 index 000000000..f2d64b66d --- /dev/null +++ b/contrib/sudoers/linux-fedora @@ -0,0 +1,6 @@ +Cmnd_Alias VAGRANT_EXPORTS_ADD = /usr/bin/tee -a /etc/exports +Cmnd_Alias VAGRANT_NFSD_CHECK = /usr/bin/systemctl status nfs-server.service +Cmnd_Alias VAGRANT_NFSD_START = /usr/bin/systemctl start nfs-server.service +Cmnd_Alias VAGRANT_NFSD_APPLY = /usr/sbin/exportfs -ar +Cmnd_Alias VAGRANT_EXPORTS_REMOVE = /bin/sed -r -e * d -ibak /etc/exports +%vagrant ALL=(root) NOPASSWD: VAGRANT_EXPORTS_ADD, VAGRANT_NFSD_CHECK, VAGRANT_NFSD_START, VAGRANT_NFSD_APPLY, VAGRANT_EXPORTS_REMOVE diff --git a/contrib/sudoers/linux b/contrib/sudoers/linux-ubuntu similarity index 100% rename from contrib/sudoers/linux rename to contrib/sudoers/linux-ubuntu diff --git a/lib/vagrant.rb b/lib/vagrant.rb index 26ed01820..1fb9c49d6 100644 --- a/lib/vagrant.rb +++ b/lib/vagrant.rb @@ -123,6 +123,7 @@ module Vagrant 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 } end @@ -141,13 +142,22 @@ module Vagrant # This checks if a plugin with the given name is installed. This can # be used from the Vagrantfile to easily branch based on plugin # availability. - def self.has_plugin?(name) - # We check the plugin names first because those are cheaper to check - return true if plugin("2").manager.registered.any? { |p| p.name == name } + def self.has_plugin?(name, version=nil) + 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 + + # Make the requirement object + version = Gem::Requirement.new([version]) if version # Now check the plugin gem names require "vagrant/plugin/manager" - Plugin::Manager.instance.installed_specs.any? { |s| s.name == name } + Plugin::Manager.instance.installed_specs.any? do |s| + match = s.name == name + next match if !version + next match && version.satisfied_by?(s.version) + end end # Returns a superclass to use when creating a plugin for Vagrant. diff --git a/lib/vagrant/action/builtin/box_add.rb b/lib/vagrant/action/builtin/box_add.rb index fb2109326..deed8b9ef 100644 --- a/lib/vagrant/action/builtin/box_add.rb +++ b/lib/vagrant/action/builtin/box_add.rb @@ -147,7 +147,7 @@ module Vagrant # element is an authenticated URL. # @param [Hash] env # @param [Bool] expanded True if the metadata URL was expanded with - # a Vagrant Cloud server URL. + # a Atlas server URL. def add_from_metadata(url, env, expanded) original_url = env[:box_url] provider = env[:box_provider] @@ -369,7 +369,7 @@ module Vagrant # # @return [Hash] def downloader(url, env, **opts) - opts[:ui] = true if !opts.has_key?(:ui) + 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}") @@ -401,7 +401,7 @@ module Vagrant 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_client_cert] + 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] @@ -409,7 +409,7 @@ module Vagrant end def download(url, env, **opts) - opts[:ui] = true if !opts.has_key?(:ui) + opts[:ui] = true if !opts.key?(:ui) d = downloader(url, env, **opts) @@ -483,7 +483,7 @@ module Vagrant output = d.head match = output.scan(/^Content-Type: (.+?)$/i).last return false if !match - match.last.chomp == "application/json" + !!(match.last.chomp =~ /application\/json/) end def validate_checksum(checksum_type, checksum, path) diff --git a/lib/vagrant/action/builtin/config_validate.rb b/lib/vagrant/action/builtin/config_validate.rb index e4fd44cdd..3c4461a10 100644 --- a/lib/vagrant/action/builtin/config_validate.rb +++ b/lib/vagrant/action/builtin/config_validate.rb @@ -11,7 +11,7 @@ module Vagrant end def call(env) - if !env.has_key?(:config_validate) || env[:config_validate] + if !env.key?(:config_validate) || env[:config_validate] errors = env[:machine].config.validate(env[:machine]) if errors && !errors.empty? diff --git a/lib/vagrant/action/builtin/graceful_halt.rb b/lib/vagrant/action/builtin/graceful_halt.rb index 393c81398..33f6cfb9b 100644 --- a/lib/vagrant/action/builtin/graceful_halt.rb +++ b/lib/vagrant/action/builtin/graceful_halt.rb @@ -24,7 +24,7 @@ module Vagrant def call(env) graceful = true - graceful = !env[:force_halt] if env.has_key?(:force_halt) + graceful = !env[:force_halt] if env.key?(:force_halt) # By default, we didn't succeed. env[:result] = false @@ -59,6 +59,9 @@ module Vagrant 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 diff --git a/lib/vagrant/action/builtin/handle_box.rb b/lib/vagrant/action/builtin/handle_box.rb index 0ecaece67..8cbdd413c 100644 --- a/lib/vagrant/action/builtin/handle_box.rb +++ b/lib/vagrant/action/builtin/handle_box.rb @@ -64,6 +64,8 @@ module Vagrant 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_formats = machine.provider_options[:box_format] || machine.provider_name @@ -79,12 +81,15 @@ module Vagrant 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_server_url: machine.config.vm.box_server_url, box_provider: box_formats, box_version: machine.config.vm.box_version, - box_client_cert: box_download_client_cert, + 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, })) rescue Errors::BoxAlreadyExists # Just ignore this, since it means the next part will succeed! diff --git a/lib/vagrant/action/builtin/mixin_provisioners.rb b/lib/vagrant/action/builtin/mixin_provisioners.rb index 9ee465aa5..3d712c954 100644 --- a/lib/vagrant/action/builtin/mixin_provisioners.rb +++ b/lib/vagrant/action/builtin/mixin_provisioners.rb @@ -15,7 +15,7 @@ module Vagrant # 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.name] + klass = Vagrant.plugin("2").manager.provisioners[provisioner.type] # This can happen in the case the configuration isn't validated. next nil if !klass @@ -23,11 +23,12 @@ module Vagrant result = klass.new(env[:machine], provisioner.config) # Store in the type map so that --provision-with works properly - @_provisioner_types[result] = provisioner.name + @_provisioner_types[result] = provisioner.type # Build up the options options = { - run: provisioner.run, + name: provisioner.name, + run: provisioner.run, } # Return the result diff --git a/lib/vagrant/action/builtin/mixin_synced_folders.rb b/lib/vagrant/action/builtin/mixin_synced_folders.rb index 42e3a290f..5a44c5ac8 100644 --- a/lib/vagrant/action/builtin/mixin_synced_folders.rb +++ b/lib/vagrant/action/builtin/mixin_synced_folders.rb @@ -93,11 +93,12 @@ module Vagrant config = opts[:config] config ||= machine.config.vm + config_folders = config.synced_folders folders = {} # Determine all the synced folders as well as the implementation # they're going to use. - config.synced_folders.each do |id, data| + config_folders.each do |id, data| # Ignore disabled synced folders next if data[:disabled] @@ -112,10 +113,12 @@ module Vagrant raise "Internal error. Report this as a bug. Invalid: #{data[:type]}" end - 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 + 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 @@ -126,7 +129,7 @@ module Vagrant # If we have folders with the "default" key, then determine the # most appropriate implementation for this. - if folders.has_key?("") && !folders[""].empty? + 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(", ") diff --git a/lib/vagrant/action/builtin/provision.rb b/lib/vagrant/action/builtin/provision.rb index 0795a2ddc..02868c735 100644 --- a/lib/vagrant/action/builtin/provision.rb +++ b/lib/vagrant/action/builtin/provision.rb @@ -24,13 +24,13 @@ module Vagrant # Tracks whether we were configured to provision config_enabled = true - config_enabled = env[:provision_enabled] if env.has_key?(:provision_enabled) + 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.has_key?(:provision_ignore_sentinel) + if env.key?(:provision_ignore_sentinel) ignore_sentinel = env[:provision_ignore_sentinel] end if ignore_sentinel @@ -69,7 +69,7 @@ module Vagrant end # Store the value so that other actions can use it - env[:provision_enabled] = provision_enabled if !env.has_key?(:provision_enabled) + 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, _| @@ -103,14 +103,20 @@ module Vagrant provisioner_instances(env).each do |p, options| type_name = type_map[p] next if env[:provision_types] && \ - !env[:provision_types].include?(type_name) + !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 + name = type_name + if options[:name] + name = "#{options[:name]} (#{type_name})" + end + env[:ui].info(I18n.t( "vagrant.actions.vm.provision.beginning", - provisioner: type_name)) + provisioner: name)) env[:hook].call(:provisioner_run, env.merge( callable: method(:run_provisioner), diff --git a/lib/vagrant/action/builtin/ssh_exec.rb b/lib/vagrant/action/builtin/ssh_exec.rb index 23b209066..81ae53cd6 100644 --- a/lib/vagrant/action/builtin/ssh_exec.rb +++ b/lib/vagrant/action/builtin/ssh_exec.rb @@ -31,11 +31,6 @@ module Vagrant info[:private_key_path] ||= [] - # Check SSH key permissions - info[:private_key_path].each do |path| - SSH.check_key_permissions(Pathname.new(path)) - end - if info[:private_key_path].empty? && info[:password] env[:ui].warn(I18n.t("vagrant.ssh_exec_password")) end diff --git a/lib/vagrant/action/builtin/ssh_run.rb b/lib/vagrant/action/builtin/ssh_run.rb index db2a0d79e..d4058e6e4 100644 --- a/lib/vagrant/action/builtin/ssh_run.rb +++ b/lib/vagrant/action/builtin/ssh_run.rb @@ -29,11 +29,6 @@ module Vagrant info[:private_key_path] ||= [] - # Check SSH key permissions - info[:private_key_path].each do |path| - SSH.check_key_permissions(Pathname.new(path)) - end - if info[:private_key_path].empty? raise Errors::SSHRunRequiresKeys end diff --git a/lib/vagrant/action/general/package.rb b/lib/vagrant/action/general/package.rb index ee7e30ec3..99c0002ec 100644 --- a/lib/vagrant/action/general/package.rb +++ b/lib/vagrant/action/general/package.rb @@ -1,4 +1,5 @@ require 'fileutils' +require "pathname" require 'vagrant/util/safe_chdir' require 'vagrant/util/subprocess' @@ -36,6 +37,9 @@ module Vagrant @app.call(env) + @env[:ui].info I18n.t("vagrant.actions.general.package.compressing", tar_path: tar_path) + copy_include_files + setup_private_key compress end @@ -81,11 +85,6 @@ module Vagrant # Compress the exported file into a package def compress - @env[:ui].info I18n.t("vagrant.actions.general.package.compressing", tar_path: tar_path) - - # Copy over the included files - copy_include_files - # Get the output path. We have to do this up here so that the # pwd returns the proper thing. output_path = tar_path.to_s @@ -100,6 +99,39 @@ module Vagrant end 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") + 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 + # Path to the final box output file def tar_path File.expand_path(@env["package.output"], FileUtils.pwd) diff --git a/lib/vagrant/action/runner.rb b/lib/vagrant/action/runner.rb index 6d4a2e409..8799a8d7f 100644 --- a/lib/vagrant/action/runner.rb +++ b/lib/vagrant/action/runner.rb @@ -49,7 +49,7 @@ module Vagrant # 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.has_key?(:ui) + ui = environment[:ui] if environment.key?(:ui) int_callback = lambda do if environment[:interrupted] ui.error I18n.t("vagrant.actions.runner.exit_immediately") if ui @@ -62,7 +62,7 @@ module Vagrant end # We place a process lock around every action that is called - @logger.info("Running action: #{callable_id}") + @logger.info("Running action: #{environment[:action_name]} #{callable_id}") Util::Busy.busy(int_callback) { callable.call(environment) } # Return the environment in case there are things in there that diff --git a/lib/vagrant/box_collection.rb b/lib/vagrant/box_collection.rb index cfefbf030..925f62a87 100644 --- a/lib/vagrant/box_collection.rb +++ b/lib/vagrant/box_collection.rb @@ -29,7 +29,7 @@ module Vagrant # * 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.). + # VMware, 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. diff --git a/lib/vagrant/capability_host.rb b/lib/vagrant/capability_host.rb index e3b579a49..47491deef 100644 --- a/lib/vagrant/capability_host.rb +++ b/lib/vagrant/capability_host.rb @@ -165,7 +165,7 @@ module Vagrant @cap_logger.debug("Checking in: #{host_name}") caps = @cap_caps[host_name] - if caps && caps.has_key?(cap_name) + if caps && caps.key?(cap_name) @cap_logger.debug("Found cap: #{cap_name} in #{host_name}") return caps[cap_name] end diff --git a/lib/vagrant/config/loader.rb b/lib/vagrant/config/loader.rb index f57b5d31e..35b0d0828 100644 --- a/lib/vagrant/config/loader.rb +++ b/lib/vagrant/config/loader.rb @@ -49,7 +49,7 @@ module Vagrant # Gather the procs for every source, since that is what we care about. procs = [] sources.each do |source| - if !@proc_cache.has_key?(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. @@ -92,10 +92,10 @@ module Vagrant errors = [] order.each do |key| - next if !@sources.has_key?(key) + next if !@sources.key?(key) @sources[key].each do |version, proc| - if !@config_cache.has_key?(proc) + if !@config_cache.key?(proc) @logger.debug("Loading from: #{key} (evaluating)") # Get the proper version loader for this version and load diff --git a/lib/vagrant/config/v1/loader.rb b/lib/vagrant/config/v1/loader.rb index 910ae3dda..e26c88e44 100644 --- a/lib/vagrant/config/v1/loader.rb +++ b/lib/vagrant/config/v1/loader.rb @@ -60,7 +60,7 @@ module Vagrant new_keys = new_state["keys"] keys = {} old_keys.each do |key, old_value| - if new_keys.has_key?(key) + 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]) @@ -72,7 +72,7 @@ module Vagrant new_keys.each do |key, new_value| # Add in the keys that the new class has that we haven't merged. - if !keys.has_key?(key) + if !keys.key?(key) keys[key] = new_value.dup end end diff --git a/lib/vagrant/config/v1/root.rb b/lib/vagrant/config/v1/root.rb index 1f7db9763..975a9e4d3 100644 --- a/lib/vagrant/config/v1/root.rb +++ b/lib/vagrant/config/v1/root.rb @@ -20,7 +20,7 @@ module Vagrant # used for Vagrant and load the proper configuration classes for # each. def method_missing(name, *args) - return @keys[name] if @keys.has_key?(name) + return @keys[name] if @keys.key?(name) config_klass = @config_map[name.to_sym] if config_klass diff --git a/lib/vagrant/config/v2/loader.rb b/lib/vagrant/config/v2/loader.rb index 1c3bc0224..2f5b6cbac 100644 --- a/lib/vagrant/config/v2/loader.rb +++ b/lib/vagrant/config/v2/loader.rb @@ -70,7 +70,7 @@ module Vagrant new_keys = new_state["keys"] keys = {} old_keys.each do |key, old_value| - if new_keys.has_key?(key) + 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]) @@ -82,7 +82,7 @@ module Vagrant new_keys.each do |key, new_value| # Add in the keys that the new class has that we haven't merged. - if !keys.has_key?(key) + if !keys.key?(key) keys[key] = new_value.dup end end diff --git a/lib/vagrant/config/v2/root.rb b/lib/vagrant/config/v2/root.rb index f64129dad..f4a18c10b 100644 --- a/lib/vagrant/config/v2/root.rb +++ b/lib/vagrant/config/v2/root.rb @@ -22,7 +22,7 @@ module Vagrant # used for Vagrant and load the proper configuration classes for # each. def method_missing(name, *args) - return @keys[name] if @keys.has_key?(name) + return @keys[name] if @keys.key?(name) config_klass = @config_map[name.to_sym] if config_klass @@ -41,7 +41,7 @@ module Vagrant # mutate itself. def finalize! @config_map.each do |key, klass| - if !@keys.has_key?(key) + if !@keys.key?(key) @keys[key] = klass.new end end @@ -102,9 +102,9 @@ module Vagrant # 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.has_key?("config_map") - @keys = state["keys"] if state.has_key?("keys") - @missing_key_calls = state["missing_key_calls"] if state.has_key?("missing_key_calls") + @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 diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 571ae75ab..b57092b03 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -80,7 +80,7 @@ module Vagrant }.merge(opts || {}) # Set the default working directory to look for the vagrantfile - opts[:cwd] ||= ENV["VAGRANT_CWD"] if ENV.has_key?("VAGRANT_CWD") + opts[:cwd] ||= ENV["VAGRANT_CWD"] if ENV.key?("VAGRANT_CWD") opts[:cwd] ||= Dir.pwd opts[:cwd] = Pathname.new(opts[:cwd]) if !opts[:cwd].directory? @@ -94,7 +94,7 @@ module Vagrant # 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.has_key?("VAGRANT_VAGRANTFILE") + ENV.key?("VAGRANT_VAGRANTFILE") opts[:vagrantfile_name] = [opts[:vagrantfile_name]] if \ opts[:vagrantfile_name] && !opts[:vagrantfile_name].is_a?(Array) @@ -164,6 +164,15 @@ module Vagrant @local_data_path = Pathname.new(File.expand_path(opts[:local_data_path], @cwd)) end + # 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 @@ -298,7 +307,7 @@ module Vagrant # @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.has_key?(:force_default) + opts[:force_default] = true if !opts.key?(:force_default) default = ENV["VAGRANT_DEFAULT_PROVIDER"] default = nil if default == "" @@ -308,6 +317,30 @@ module Vagrant # that (the default behavior) return default if default && opts[:force_default] + # 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) + root_config = machine_info[:config] + end + + # Get the list of providers within our configuration and assign + # a priority to each in the order they exist so that we try these first. + config = {} + root_config.vm.__providers.reverse.each_with_index do |key, idx| + config[key] = idx + end + + # Determine the max priority so that we can add the config priority + # to that to make sure it is highest. + max_priority = 0 + Vagrant.plugin("2").manager.providers.each do |key, data| + priority = data[1][:priority] + max_priority = priority if priority > max_priority + end + ordered = [] Vagrant.plugin("2").manager.providers.each do |key, data| impl = data[0] @@ -316,10 +349,19 @@ module Vagrant # Skip excluded providers next if opts[:exclude] && opts[:exclude].include?(key) - # Skip providers that can't be defaulted - next if popts.has_key?(:defaultable) && !popts[:defaultable] + # Skip providers that can't be defaulted, unless they're in our + # config, in which case someone made our decision for us. + if !config.key?(key) + next if popts.key?(:defaultable) && !popts[:defaultable] + end - ordered << [popts[:priority], key, impl, popts] + # The priority is higher if it is in our config. Otherwise, it is + # the priority it set PLUS the length of the config to make sure it + # is never higher than the configuration keys. + priority = popts[:priority] + priority = config[key] + max_priority if config.key?(key) + + ordered << [priority, key, impl, popts] end # Order the providers by priority. Higher values are tried first. @@ -336,8 +378,8 @@ module Vagrant return key if impl.usable?(false) end - # If all else fails, return VirtualBox - return :virtualbox + # No providers available is a critical error for Vagrant. + raise Errors::NoDefaultProvider end # Returns the collection of boxes for the environment. @@ -497,6 +539,41 @@ module Vagrant end end + # This executes the push with the given name, raising any exceptions that + # occur. + # + # Precondition: the push is not nil and exists. + def push(name) + @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 = Vagrant.plugin("2").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. # @@ -519,7 +596,7 @@ module Vagrant @machines.delete(cache_key) end - if @machines.has_key?(cache_key) + if @machines.key?(cache_key) @logger.info("Returning cached machine: #{name} (#{provider})") return @machines[cache_key] end @@ -782,7 +859,10 @@ module Vagrant # 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 - @ui.ask(I18n.t("vagrant.upgrading_home_path_v1_5")) + 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 diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 8995f6505..de002ffed 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -476,6 +476,10 @@ module Vagrant 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 @@ -552,6 +556,22 @@ module Vagrant error_key(:plugin_uninstall_system) 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 RSyncError < VagrantError error_key(:rsync_error) end @@ -616,6 +636,10 @@ module Vagrant 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 @@ -728,6 +752,14 @@ module Vagrant error_key(:virtualbox_no_name) end + class VirtualBoxNameExists < VagrantError + error_key(:virtualbox_name_exists) + end + + class VirtualBoxVersionEmpty < VagrantError + error_key(:virtualbox_version_empty) + end + class VMBaseMacNotSpecified < VagrantError error_key(:no_base_mac, "vagrant.actions.vm.match_mac") end diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 9d2c0d15c..03ca19f07 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -1,3 +1,5 @@ +require_relative "util/ssh" + require "digest/md5" require "thread" @@ -6,7 +8,7 @@ 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.). + # is backed by any sort of provider (VirtualBox, VMware, etc.). class Machine # The box that is backing this machine. # @@ -133,6 +135,12 @@ module Vagrant @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 end # This calls an action on the provider. The provider may or may not @@ -142,9 +150,18 @@ module Vagrant # @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, extra_env=nil) + 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 + # Create a deterministic ID for this machine vf = nil vf = @env.vagrantfile_name[0] if @env.vagrantfile_name @@ -155,7 +172,7 @@ module Vagrant # 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 !name.to_s.start_with?("ssh") + locker = @env.method(:lock) if lock && !name.to_s.start_with?("ssh") # Lock this machine for the duration of this action locker.call("machine-action-#{id}") do @@ -170,6 +187,7 @@ module Vagrant provider: @provider.to_s end + # Call the action action_raw(name, callable, extra_env) end rescue Errors::EnvironmentLockedError @@ -342,6 +360,7 @@ module Vagrant # This reloads the ID of the underlying machine. def reload + old_id = @id @id = nil if @data_dir @@ -350,6 +369,13 @@ module Vagrant id_file = @data_dir.join("id") @id = id_file.read.chomp if id_file.file? 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 @@ -434,6 +460,14 @@ module Vagrant 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 @@ -446,12 +480,6 @@ module Vagrant result = @provider.state raise Errors::MachineStateInvalid if !result.is_a?(MachineState) - # If the ID is the special not created ID, then set our ID to - # nil so that we destroy all our data. - if result.id == MachineState::NOT_CREATED_ID - self.id = nil - end - # Update our state cache if we have a UUID and an entry in the # master index. uuid = index_uuid diff --git a/lib/vagrant/plugin/manager.rb b/lib/vagrant/plugin/manager.rb index 0cd71c8ba..dfaab2ecf 100644 --- a/lib/vagrant/plugin/manager.rb +++ b/lib/vagrant/plugin/manager.rb @@ -143,7 +143,7 @@ module Vagrant # If we already have a newer version in our list of installed, # then ignore it - next if installed_map.has_key?(spec.name) && + next if installed_map.key?(spec.name) && installed_map[spec.name].version >= spec.version installed_map[spec.name] = spec diff --git a/lib/vagrant/plugin/state_file.rb b/lib/vagrant/plugin/state_file.rb index 4a39a52bc..f41da1b86 100644 --- a/lib/vagrant/plugin/state_file.rb +++ b/lib/vagrant/plugin/state_file.rb @@ -61,7 +61,7 @@ module Vagrant # # @return [Boolean] def has_plugin?(name) - @data["installed"].has_key?(name) + @data["installed"].key?(name) end # Remove a plugin that is installed from the state file. diff --git a/lib/vagrant/plugin/v2.rb b/lib/vagrant/plugin/v2.rb index 1539667bd..953f73ff6 100644 --- a/lib/vagrant/plugin/v2.rb +++ b/lib/vagrant/plugin/v2.rb @@ -16,6 +16,7 @@ module Vagrant 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" end diff --git a/lib/vagrant/plugin/v2/command.rb b/lib/vagrant/plugin/v2/command.rb index a702b767c..24b2230d4 100644 --- a/lib/vagrant/plugin/v2/command.rb +++ b/lib/vagrant/plugin/v2/command.rb @@ -162,7 +162,7 @@ module Vagrant end # Use the default provider if nothing else - provider_to_use ||= @env.default_provider + provider_to_use ||= @env.default_provider(machine: name) # Get the right machine with the right provider @env.machine(name, provider_to_use) @@ -224,6 +224,15 @@ module Vagrant @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 diff --git a/lib/vagrant/plugin/v2/components.rb b/lib/vagrant/plugin/v2/components.rb index 7bae6c29a..d7c64d370 100644 --- a/lib/vagrant/plugin/v2/components.rb +++ b/lib/vagrant/plugin/v2/components.rb @@ -54,6 +54,11 @@ module Vagrant # @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>] @@ -71,6 +76,7 @@ module Vagrant @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 end end diff --git a/lib/vagrant/plugin/v2/manager.rb b/lib/vagrant/plugin/v2/manager.rb index 62a23f82a..ce76d1fc1 100644 --- a/lib/vagrant/plugin/v2/manager.rb +++ b/lib/vagrant/plugin/v2/manager.rb @@ -172,6 +172,28 @@ module Vagrant 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] diff --git a/lib/vagrant/plugin/v2/plugin.rb b/lib/vagrant/plugin/v2/plugin.rb index 9a7f6177d..025032fd9 100644 --- a/lib/vagrant/plugin/v2/plugin.rb +++ b/lib/vagrant/plugin/v2/plugin.rb @@ -88,7 +88,7 @@ module Vagrant end # By default, the command is primary - opts[:primary] = true if !opts.has_key?(:primary) + opts[:primary] = true if !opts.key?(:primary) # Register the command components.commands.register(name.to_sym) do @@ -221,6 +221,18 @@ module Vagrant 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. diff --git a/lib/vagrant/plugin/v2/push.rb b/lib/vagrant/plugin/v2/push.rb new file mode 100644 index 000000000..f8bc15d53 --- /dev/null +++ b/lib/vagrant/plugin/v2/push.rb @@ -0,0 +1,27 @@ +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 diff --git a/lib/vagrant/registry.rb b/lib/vagrant/registry.rb index 5095f2097..d84a745a5 100644 --- a/lib/vagrant/registry.rb +++ b/lib/vagrant/registry.rb @@ -22,8 +22,8 @@ module Vagrant # This will evaluate the block given to `register` and return the # resulting value. def get(key) - return nil if !@items.has_key?(key) - return @results_cache[key] if @results_cache.has_key?(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 @@ -31,10 +31,11 @@ module Vagrant # Checks if the given key is registered with the registry. # # @return [Boolean] - def has_key?(key) - @items.has_key?(key) + def key?(key) + @items.key?(key) end - + alias_method :has_key?, :key? + # Returns an array populated with the keys of this object. # # @return [Array] @@ -49,6 +50,21 @@ module Vagrant end end + # Return the number of elements in this registry. + # + # @return [Fixnum] + 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. diff --git a/lib/vagrant/shared_helpers.rb b/lib/vagrant/shared_helpers.rb index c2c52ae10..12545ae4f 100644 --- a/lib/vagrant/shared_helpers.rb +++ b/lib/vagrant/shared_helpers.rb @@ -5,12 +5,12 @@ require "thread" module Vagrant @@global_lock = Mutex.new - # This is the default endpoint of the Vagrant Cloud in + # This is the default endpoint of the Atlas 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" + DEFAULT_SERVER_URL = "https://atlas.hashicorp.com" # This holds a global lock for the duration of the block. This should # be invoked around anything that is modifying process state (such as @@ -52,6 +52,13 @@ module Vagrant !!ENV["VAGRANT_I_KNOW_WHAT_IM_DOING_PLEASE_BE_QUIET"] end + # The current log level for Vagrant + # + # @return [String] + def self.log_level + ENV["VAGRANT_LOG"] + end + # Returns the URL prefix to the server. # # @return [String] diff --git a/lib/vagrant/ui.rb b/lib/vagrant/ui.rb index 4b47c3e1f..a84448a1c 100644 --- a/lib/vagrant/ui.rb +++ b/lib/vagrant/ui.rb @@ -136,9 +136,9 @@ module Vagrant # Setup the options so that the new line is suppressed opts ||= {} - opts[:echo] = true if !opts.has_key?(:echo) - opts[:new_line] = false if !opts.has_key?(:new_line) - opts[:prefix] = false if !opts.has_key?(:prefix) + 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) @@ -249,7 +249,7 @@ module Vagrant class_eval <<-CODE def #{method}(message, *args, **opts) super(message) - if !@ui.opts.has_key?(:bold) && !opts.has_key?(:bold) + if !@ui.opts.key?(:bold) && !opts.key?(:bold) opts[:bold] = #{method.inspect} != :detail && \ #{method.inspect} != :ask end @@ -284,7 +284,7 @@ module Vagrant opts = self.opts.merge(opts) prefix = "" - if !opts.has_key?(:prefix) || opts[:prefix] + if !opts.key?(:prefix) || opts[:prefix] prefix = OUTPUT_PREFIX prefix = " " * OUTPUT_PREFIX.length if \ type == :detail || type == :ask || opts[:prefix_spaces] @@ -294,7 +294,7 @@ module Vagrant return message if prefix.empty? target = @prefix - target = opts[:target] if opts.has_key?(:target) + target = opts[:target] if opts.key?(:target) # Get the lines. The first default is because if the message # is an empty string, then we want to still use the empty string. diff --git a/lib/vagrant/util.rb b/lib/vagrant/util.rb index db729bfdc..896b5d7e0 100644 --- a/lib/vagrant/util.rb +++ b/lib/vagrant/util.rb @@ -2,6 +2,7 @@ module Vagrant module Util autoload :Busy, 'vagrant/util/busy' autoload :Counter, 'vagrant/util/counter' + autoload :Env, 'vagrant/util/env' autoload :HashWithIndifferentAccess, 'vagrant/util/hash_with_indifferent_access' autoload :Platform, 'vagrant/util/platform' autoload :Retryable, 'vagrant/util/retryable' diff --git a/lib/vagrant/util/downloader.rb b/lib/vagrant/util/downloader.rb index 8756fc30f..dd3abaa72 100644 --- a/lib/vagrant/util/downloader.rb +++ b/lib/vagrant/util/downloader.rb @@ -58,18 +58,15 @@ module Vagrant # If this method returns without an exception, the download # succeeded. An exception will be raised if the download failed. def download! - options, subprocess_options = self.options - options += ["--output", @destination] - options << @source - # 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. - subprocess_options[:notify] = :stderr + extra_subprocess_opts[:notify] = :stderr progress_data = "" progress_regexp = /(\r(.+?))\r/ @@ -121,8 +118,31 @@ module Vagrant @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 + + # If its any error other than 33, it is an error. + raise if e.extra_data[:exit_code].to_i != 33 + + # Exit code 33 means that the server doesn't support ranges. + # In this case, try again without resume. + @continue = false + retried = true + retry ensure # If we're outputting to the UI, clear the output to # avoid lingering progress meters. @@ -177,7 +197,9 @@ module Vagrant @logger.warn("Downloader exit code: #{result.exit_code}") parts = result.stderr.split(/\n*curl:\s+\(\d+\)\s*/, 2) parts[1] ||= "" - raise Errors::DownloaderError, message: parts[1].chomp + raise Errors::DownloaderError, + code: result.exit_code, + message: parts[1].chomp end result diff --git a/lib/vagrant/util/env.rb b/lib/vagrant/util/env.rb new file mode 100644 index 000000000..3d3200f17 --- /dev/null +++ b/lib/vagrant/util/env.rb @@ -0,0 +1,43 @@ +module Vagrant + module Util + class Env + # + # 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(&block) + original = ENV.to_hash + + ENV.delete('_ORIGINAL_GEM_PATH') + ENV.delete_if { |k,_| k.start_with?('BUNDLE_') } + ENV.delete_if { |k,_| k.start_with?('GEM_') } + ENV.delete_if { |k,_| k.start_with?('RUBY') } + + yield + ensure + ENV.replace(original.to_hash) + end + end + end +end diff --git a/lib/vagrant/util/io.rb b/lib/vagrant/util/io.rb index b38bc3eef..692ece4f9 100644 --- a/lib/vagrant/util/io.rb +++ b/lib/vagrant/util/io.rb @@ -29,7 +29,7 @@ module Vagrant break if !results || results[0].empty? # Read! - data << io.readpartial(READ_CHUNK_SIZE) + data << io.readpartial(READ_CHUNK_SIZE).encode("UTF-8", Encoding.default_external) else # Do a simple non-blocking read on the IO object data << io.read_nonblock(READ_CHUNK_SIZE) diff --git a/lib/vagrant/util/is_port_open.rb b/lib/vagrant/util/is_port_open.rb index e98dd4c24..63652ee28 100644 --- a/lib/vagrant/util/is_port_open.rb +++ b/lib/vagrant/util/is_port_open.rb @@ -29,7 +29,8 @@ module Vagrant # to connect. return true end - rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH + rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, \ + Errno::ENETUNREACH, Errno::EACCES # Any of the above exceptions signal that the port is closed. return false end diff --git a/lib/vagrant/util/keypair.rb b/lib/vagrant/util/keypair.rb new file mode 100644 index 000000000..7fec84b2a --- /dev/null +++ b/lib/vagrant/util/keypair.rb @@ -0,0 +1,46 @@ +require "base64" +require "openssl" + +module Vagrant + module Util + class Keypair + # 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) + rsa_key = OpenSSL::PKey::RSA.new(2048) + public_key = rsa_key.public_key + private_key = rsa_key.to_pem + + if password + cipher = OpenSSL::Cipher::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 + end +end diff --git a/lib/vagrant/util/platform.rb b/lib/vagrant/util/platform.rb index d2b399616..c452eb295 100644 --- a/lib/vagrant/util/platform.rb +++ b/lib/vagrant/util/platform.rb @@ -148,7 +148,7 @@ module Vagrant # output. def terminal_supports_colors? if windows? - return true if ENV.has_key?("ANSICON") + return true if ENV.key?("ANSICON") return true if cygwin? return true if ENV["TERM"] == "cygwin" return false diff --git a/lib/vagrant/util/ssh.rb b/lib/vagrant/util/ssh.rb index 44e0403aa..0a71613c7 100644 --- a/lib/vagrant/util/ssh.rb +++ b/lib/vagrant/util/ssh.rb @@ -102,12 +102,14 @@ module Vagrant 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", "Compression=yes", "-o", "DSAAuthentication=yes", - "-o", "LogLevel=FATAL", + "-o", "LogLevel=#{log_level}", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"] diff --git a/lib/vagrant/util/subprocess.rb b/lib/vagrant/util/subprocess.rb index 92669d45a..278d68d01 100644 --- a/lib/vagrant/util/subprocess.rb +++ b/lib/vagrant/util/subprocess.rb @@ -25,6 +25,7 @@ module Vagrant def initialize(*command) @options = command.last.is_a?(Hash) ? command.pop : {} @command = command.dup + @command.each { |s| s.encode(Encoding.default_external) } @command[0] = Which.which(@command[0]) if !File.file?(@command[0]) if !@command[0] raise Errors::CommandUnavailableWindows, file: command[0] if Platform.windows? diff --git a/lib/vagrant/vagrantfile.rb b/lib/vagrant/vagrantfile.rb index 511d1443e..011d16351 100644 --- a/lib/vagrant/vagrantfile.rb +++ b/lib/vagrant/vagrantfile.rb @@ -114,24 +114,30 @@ module Vagrant name: name, provider: provider end - provider_plugin = Vagrant.plugin("2").manager.providers[provider] - if !provider_plugin - raise Errors::ProviderNotFound, - machine: 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 + raise Errors::ProviderNotFound, + machine: name, provider: provider + end - provider_cls = provider_plugin[0] - provider_options = provider_plugin[1] - box_formats = provider_options[:box_format] || 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 + # 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 end # Add the sub-machine configuration to the loader and keys @@ -153,7 +159,7 @@ module Vagrant local_keys = keys.dup # Load the box Vagrantfile, if there is one - if config.vm.box + if config.vm.box && boxes box = boxes.find(config.vm.box, box_formats, config.vm.box_version) if box box_vagrantfile = find_vagrantfile(box.directory) diff --git a/plugins/commands/box/command/add.rb b/plugins/commands/box/command/add.rb index 0356ee298..d82d441a8 100644 --- a/plugins/commands/box/command/add.rb +++ b/plugins/commands/box/command/add.rb @@ -47,7 +47,7 @@ module VagrantPlugins end o.separator "" - o.separator "The box descriptor can be the name of a box on Vagrant Cloud," + o.separator "The box descriptor can be the name of a box on HashiCorp's Atlas," o.separator "or a URL, or a local .box file, or a local .json file containing" o.separator "the catalog metadata." o.separator "" diff --git a/plugins/commands/destroy/command.rb b/plugins/commands/destroy/command.rb index e7a2f603f..141bfd38b 100644 --- a/plugins/commands/destroy/command.rb +++ b/plugins/commands/destroy/command.rb @@ -32,7 +32,7 @@ module VagrantPlugins :destroy, force_confirm_destroy: options[:force]) total += 1 - declined += 1 if action_env.has_key?(:force_confirm_destroy_result) && + declined += 1 if action_env.key?(:force_confirm_destroy_result) && action_env[:force_confirm_destroy_result] == false end diff --git a/plugins/commands/login/client.rb b/plugins/commands/login/client.rb new file mode 100644 index 000000000..63b01f335 --- /dev/null +++ b/plugins/commands/login/client.rb @@ -0,0 +1,117 @@ +require "rest_client" + +module VagrantPlugins + module LoginCommand + class Client + # Initializes a login client with the given Vagrant::Environment. + # + # @param [Vagrant::Environment] env + def initialize(env) + @logger = Log4r::Logger.new("vagrant::login::client") + @env = env + 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? + token = self.token + return false if !token + + with_error_handling do + url = "#{Vagrant.server_url}/api/v1/authenticate" + + "?access_token=#{token}" + RestClient.get(url, content_type: :json) + true + end + 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] user + # @param [String] pass + # @return [String] token The access token, or nil if auth failed. + def login(user, pass) + @logger.info("Logging in '#{user}'") + + with_error_handling do + url = "#{Vagrant.server_url}/api/v1/authenticate" + request = { "user" => { "login" => user, "password" => pass } } + response = RestClient.post( + url, JSON.dump(request), content_type: :json) + data = JSON.load(response.to_s) + data["token"] + end + end + + # Stores the given token locally, removing any previous tokens. + # + # @param [String] token + def store_token(token) + @logger.info("Storing token in #{token_path}") + + token_path.open("w") do |f| + f.write(token) + end + + nil + end + + # Reads the access token if there is one. This will first read the + # `ATLAS_TOKEN` environment variable and then fallback to the stored + # access token on disk. + # + # @return [String] + def token + if ENV["ATLAS_TOKEN"] && !ENV["ATLAS_TOKEN"].empty? + @logger.debug("Using authentication token from environment variable") + return ENV["ATLAS_TOKEN"] + end + + if token_path.exist? + @logger.debug("Using authentication token from disk at #{token_path}") + return token_path.read.strip + end + + @logger.debug("No authentication token in environment or #{token_path}") + + nil + end + + protected + + def with_error_handling(&block) + yield + rescue RestClient::Unauthorized + @logger.debug("Unauthorized!") + false + rescue RestClient::NotAcceptable => e + @logger.debug("Got unacceptable response:") + @logger.debug(e.message) + @logger.debug(e.backtrace.join("\n")) + + begin + errors = JSON.parse(e.response)["errors"].join("\n") + raise Errors::ServerError, errors: errors + rescue JSON::ParserError; end + + raise "An unexpected error occurred: #{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 + end + end +end diff --git a/plugins/commands/login/command.rb b/plugins/commands/login/command.rb new file mode 100644 index 000000000..203b09009 --- /dev/null +++ b/plugins/commands/login/command.rb @@ -0,0 +1,102 @@ +module VagrantPlugins + module LoginCommand + class Command < Vagrant.plugin("2", "command") + def self.synopsis + "log in to HashiCorp's Atlas" + end + + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant login" + o.separator "" + o.on("-c", "--check", "Only checks if you're logged in") do |c| + options[:check] = c + end + + o.on("-k", "--logout", "Logs you out if you're logged in") do |k| + options[:logout] = k + end + + o.on("-t", "--token TOKEN", String, "Set the Atlas token") do |t| + options[:token] = t + end + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + @client = Client.new(@env) + + # Determine what task we're actually taking based on flags + if options[:check] + return execute_check + elsif options[:logout] + return execute_logout + elsif options[:token] + return execute_token(options[:token]) + end + + # Let the user know what is going on. + @env.ui.output(I18n.t("login_command.command_header") + "\n") + + # If it is a private cloud installation, show that + if Vagrant.server_url != Vagrant::DEFAULT_SERVER_URL + @env.ui.output("Atlas URL: #{Vagrant.server_url}") + end + + # Ask for the username + login = nil + password = nil + while !login + login = @env.ui.ask("Atlas Username: ") + end + + while !password + password = @env.ui.ask("Password (will be hidden): ", echo: false) + end + + token = @client.login(login, password) + if !token + @env.ui.error(I18n.t("login_command.invalid_login")) + return 1 + end + + @client.store_token(token) + @env.ui.success(I18n.t("login_command.logged_in")) + 0 + end + + def execute_check + if @client.logged_in? + @env.ui.success(I18n.t("login_command.check_logged_in")) + return 0 + else + @env.ui.error(I18n.t("login_command.check_not_logged_in")) + return 1 + end + end + + def execute_logout + @client.clear_token + @env.ui.success(I18n.t("login_command.logged_out")) + return 0 + end + + def execute_token(token) + @client.store_token(token) + @env.ui.success(I18n.t("login_command.token_saved")) + + if @client.logged_in? + @env.ui.success(I18n.t("login_command.check_logged_in")) + return 0 + else + @env.ui.error(I18n.t("login_command.invalid_token")) + return 1 + end + end + end + end +end diff --git a/plugins/commands/login/errors.rb b/plugins/commands/login/errors.rb new file mode 100644 index 000000000..614c37cf6 --- /dev/null +++ b/plugins/commands/login/errors.rb @@ -0,0 +1,17 @@ +module VagrantPlugins + module LoginCommand + module Errors + class Error < Vagrant::Errors::VagrantError + error_namespace("login_command.errors") + end + + class ServerError < Error + error_key(:server_error) + end + + class ServerUnreachable < Error + error_key(:server_unreachable) + end + end + end +end diff --git a/plugins/commands/login/locales/en.yml b/plugins/commands/login/locales/en.yml new file mode 100644 index 000000000..dba4d52d7 --- /dev/null +++ b/plugins/commands/login/locales/en.yml @@ -0,0 +1,34 @@ +en: + login_command: + errors: + server_error: |- + The Atlas server responded with an not-OK response: + + %{errors} + server_unreachable: |- + The Atlas server is not currently accepting connections. Please check + your network connection and try again later. + + 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 + Atlas. 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 Atlas account, sign up at + https://atlas.hashicorp.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. diff --git a/plugins/commands/login/middleware/add_authentication.rb b/plugins/commands/login/middleware/add_authentication.rb new file mode 100644 index 000000000..faaa3cfe7 --- /dev/null +++ b/plugins/commands/login/middleware/add_authentication.rb @@ -0,0 +1,45 @@ +require "uri" + +require_relative "../client" + +module VagrantPlugins + module LoginCommand + class AddAuthentication + def initialize(app, env) + @app = app + end + + def call(env) + client = Client.new(env[:env]) + token = client.token + + if token && Vagrant.server_url + server_uri = URI.parse(Vagrant.server_url) + + env[:box_urls].map! do |url| + u = URI.parse(url) + replace = u.host == server_uri.host + if !replace + # We need this in here for the transition we made from + # Vagrant Cloud to Atlas. This preserves access tokens + # appending to both without leaking access tokens to + # unsavory URLs. + replace = u.host == "vagrantcloud.com" && + server_uri.host == "atlas.hashicorp.com" + end + + if replace + u.query ||= "" + u.query += "&" if u.query != "" + u.query += "access_token=#{token}" + end + + u.to_s + end + end + + @app.call(env) + end + end + end +end diff --git a/plugins/commands/login/plugin.rb b/plugins/commands/login/plugin.rb new file mode 100644 index 000000000..efb84a556 --- /dev/null +++ b/plugins/commands/login/plugin.rb @@ -0,0 +1,35 @@ +require "vagrant" + +module VagrantPlugins + module LoginCommand + autoload :Client, File.expand_path("../client", __FILE__) + autoload :Errors, File.expand_path("../errors", __FILE__) + + class Plugin < Vagrant.plugin("2") + name "vagrant-login" + description <<-DESC + Provides the login command and internal API access to Atlas. + DESC + + command(:login) do + require_relative "command" + init! + Command + end + + action_hook(:cloud_authenticated_boxes, :authenticate_box_url) do |hook| + require_relative "middleware/add_authentication" + hook.prepend(AddAuthentication) + 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 diff --git a/plugins/commands/plugin/action/list_plugins.rb b/plugins/commands/plugin/action/list_plugins.rb index b7620ab84..37b168867 100644 --- a/plugins/commands/plugin/action/list_plugins.rb +++ b/plugins/commands/plugin/action/list_plugins.rb @@ -34,20 +34,29 @@ module VagrantPlugins system = ", system" if plugin && plugin["system"] env[:ui].info "#{spec.name} (#{spec.version}#{system})" env[:ui].machine("plugin-name", spec.name) - env[:ui].machine("plugin-version", "#{spec.version}#{system}") + env[:ui].machine( + "plugin-version", + "#{spec.version}#{system}", + 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"]) + 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"]) + env[:ui].machine( + "plugin-custom-entrypoint", + plugin["require"], + target: spec.name) end end diff --git a/plugins/commands/plugin/action/plugin_exists_check.rb b/plugins/commands/plugin/action/plugin_exists_check.rb index 7a369976e..b7b7b9623 100644 --- a/plugins/commands/plugin/action/plugin_exists_check.rb +++ b/plugins/commands/plugin/action/plugin_exists_check.rb @@ -12,7 +12,7 @@ module VagrantPlugins def call(env) installed = Vagrant::Plugin::Manager.instance.installed_plugins - if !installed.has_key?(env[:plugin_name]) + if !installed.key?(env[:plugin_name]) raise Vagrant::Errors::PluginNotInstalled, name: env[:plugin_name] end diff --git a/plugins/commands/push/command.rb b/plugins/commands/push/command.rb new file mode 100644 index 000000000..902a0c946 --- /dev/null +++ b/plugins/commands/push/command.rb @@ -0,0 +1,75 @@ +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 diff --git a/plugins/commands/push/plugin.rb b/plugins/commands/push/plugin.rb new file mode 100644 index 000000000..ecd24dd7a --- /dev/null +++ b/plugins/commands/push/plugin.rb @@ -0,0 +1,17 @@ +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 diff --git a/plugins/commands/up/command.rb b/plugins/commands/up/command.rb index 7fdb6eb3c..826bea0ae 100644 --- a/plugins/commands/up/command.rb +++ b/plugins/commands/up/command.rb @@ -58,24 +58,37 @@ module VagrantPlugins @env.batch(options[:parallel]) do |batch| names = argv if names.empty? + autostart = false @env.vagrantfile.machine_names_and_options.each do |n, o| - o[:autostart] = true if !o.has_key?(:autostart) + 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 - 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)) + if names + 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 + machines << machine - batch.action(machine, :up, options) + 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 diff --git a/plugins/commands/up/start_mixins.rb b/plugins/commands/up/start_mixins.rb index e0aeb6de2..548d889ca 100644 --- a/plugins/commands/up/start_mixins.rb +++ b/plugins/commands/up/start_mixins.rb @@ -19,6 +19,8 @@ module VagrantPlugins parser.on("--provision-with x,y,z", Array, "Enable only certain provisioners, by type.") do |list| options[:provision_types] = list.map { |type| type.to_sym } + options[:provision_enabled] = true + options[:provision_ignore_sentinel] = true end end diff --git a/plugins/communicators/ssh/communicator.rb b/plugins/communicators/ssh/communicator.rb index f4d152002..64455042b 100644 --- a/plugins/communicators/ssh/communicator.rb +++ b/plugins/communicators/ssh/communicator.rb @@ -11,9 +11,9 @@ 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' -require 'vagrant/util/ssh' module VagrantPlugins module CommunicatorSSH @@ -85,6 +85,12 @@ module VagrantPlugins raise rescue Vagrant::Errors::SSHKeyTypeNotSupported 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 @@ -139,20 +145,41 @@ module VagrantPlugins # If we used a password, then insert the insecure key ssh_info = @machine.ssh_info - if ssh_info[:password] && ssh_info[:private_key_path].empty? - @logger.info("Inserting insecure key to avoid password") - @machine.ui.info(I18n.t("vagrant.inserting_insecure_key")) - @machine.guest.capability( - :insert_public_key, - Vagrant.source_root.join("keys", "vagrant.pub").read.chomp) + 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 + + _pub, priv, openssh = Vagrant::Util::Keypair.create + + @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("w+") do |f| - f.write(Vagrant.source_root.join("keys", "vagrant").read) + f.write(priv) end - @machine.ui.info(I18n.t("vagrant.inserted_key")) + # 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 @@ -265,8 +292,13 @@ module VagrantPlugins # 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 - @connection.exec!("") + Timeout.timeout(5) do + @connection.exec!("") + end rescue Exception => e @logger.info("Connection errored, not re-using. Will reconnect.") @logger.debug(e.inspect) @@ -288,7 +320,7 @@ module VagrantPlugins raise Vagrant::Errors::SSHNotReady if ssh_info.nil? # Default some options - opts[:retries] = 5 if !opts.has_key?(:retries) + opts[:retries] = 5 if !opts.key?(:retries) # Build the options we'll use to initiate the connection via Net::SSH common_connect_opts = { @@ -305,11 +337,6 @@ module VagrantPlugins verbose: :debug, } - # Check that the private key permissions are valid - ssh_info[:private_key_path].each do |path| - Vagrant::Util::SSH.check_key_permissions(Pathname.new(path)) - end - # Connect to SSH, giving it a few tries connection = nil begin @@ -594,6 +621,16 @@ module VagrantPlugins # 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) + source_path = Vagrant.source_root.join("keys", "vagrant") + return File.read(path).chomp == source_path.read.chomp + end end end end diff --git a/plugins/communicators/winrm/command_filters/mkdir.rb b/plugins/communicators/winrm/command_filters/mkdir.rb old mode 100755 new mode 100644 diff --git a/plugins/communicators/winrm/command_filters/rm.rb b/plugins/communicators/winrm/command_filters/rm.rb index 251e26076..8ef79f148 100644 --- a/plugins/communicators/winrm/command_filters/rm.rb +++ b/plugins/communicators/winrm/command_filters/rm.rb @@ -20,7 +20,7 @@ module VagrantPlugins break end end - + # Figure out which argument is the path dir = cmd_parts.pop while !dir.nil? && dir.start_with?('-') diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb index 7609f16b3..7ce765298 100644 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -25,22 +25,89 @@ module VagrantPlugins @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 = Helper.winrm_info(@machine) + 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 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 retriable 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...") - Timeout.timeout(@machine.config.winrm.timeout) do + result = Timeout.timeout(@machine.config.winrm.timeout) do shell(true).powershell("hostname") end @logger.info("WinRM is ready!") return true - 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. + rescue Errors::TransientError => 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 ssh_info was not refreshing + # This resolves a problem when using vSphere where the winrm_info was not refreshing # thus never getting the correct hostname. @shell = nil return false @@ -79,12 +146,21 @@ module VagrantPlugins alias_method :sudo, :execute def test(command, opts=nil) - # If this is a *nix command with no Windows equivilant, assume failure + # If this is a *nix command (which we know about) with no Windows + # equivilant, assume failure command = @cmd_filter.filter(command) return false if command.empty? - opts = { error_check: false }.merge(opts || {}) - execute(command, opts) == 0 + 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 && flatten_stderr(output).length == 0 end def upload(from, to) @@ -106,11 +182,8 @@ module VagrantPlugins WinRMShell.new( winrm_info[:host], - @machine.config.winrm.username, - @machine.config.winrm.password, - port: winrm_info[:port], - timeout_in_seconds: @machine.config.winrm.timeout, - max_tries: @machine.config.winrm.max_tries, + winrm_info[:port], + @machine.config.winrm ) end @@ -156,8 +229,8 @@ module VagrantPlugins def raise_execution_error(output, opts) # WinRM can return multiple stderr and stdout entries error_opts = opts.merge( - stdout: output[:data].collect { |e| e[:stdout] }.join, - stderr: output[:data].collect { |e| e[:stderr] }.join + stdout: flatten_stdout(output), + stderr: flatten_stderr(output) ) # Use a different error message key if the caller gave us one, @@ -167,6 +240,20 @@ module VagrantPlugins # Raise the error, use the type the caller gave us or the comm default raise opts[:error_class], error_opts end + + + # TODO: Replace with WinRM Output class when WinRM 1.3 is released + def flatten_stderr(output) + output[:data].map do | line | + line[:stderr] + end.compact.join + end + + def flatten_stdout(output) + output[:data].map do | line | + line[:flatten_stdout] + end.compact.join + end end #WinRM class end end diff --git a/plugins/communicators/winrm/config.rb b/plugins/communicators/winrm/config.rb index 155ac0567..79901d503 100644 --- a/plugins/communicators/winrm/config.rb +++ b/plugins/communicators/winrm/config.rb @@ -7,37 +7,51 @@ module VagrantPlugins 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 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 + @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 end def finalize! - @username = "vagrant" if @username == UNSET_VALUE - @password = "vagrant" if @password == UNSET_VALUE + @username = "vagrant" if @username == UNSET_VALUE + @password = "vagrant" if @password == UNSET_VALUE + @transport = :plaintext if @transport == UNSET_VALUE @host = nil if @host == UNSET_VALUE - @port = 5985 if @port == UNSET_VALUE - @guest_port = 5985 if @guest_port == UNSET_VALUE - @max_tries = 20 if @max_tries == UNSET_VALUE - @timeout = 1800 if @timeout == 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 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.timeout cannot be nil." if @timeout.nil? + 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 @max_tries.nil? + errors << "winrm.timeout cannot be nil." if @timeout.nil? + unless @ssl_peer_verification == true || @ssl_peer_verification == false + errors << "winrm.ssl_peer_verification must be a boolean." + end { "WinRM" => errors } end diff --git a/plugins/communicators/winrm/errors.rb b/plugins/communicators/winrm/errors.rb index 552b8a300..6a5df1be1 100644 --- a/plugins/communicators/winrm/errors.rb +++ b/plugins/communicators/winrm/errors.rb @@ -6,8 +6,11 @@ module VagrantPlugins error_namespace("vagrant_winrm.errors") end - class AuthError < WinRMError - error_key(:auth_error) + class TransientError < WinRMError + end + + class AuthenticationFailed < WinRMError + error_key(:authentication_failed) end class ExecutionError < WinRMError @@ -29,6 +32,38 @@ module VagrantPlugins 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 diff --git a/plugins/communicators/winrm/file_manager.rb b/plugins/communicators/winrm/file_manager.rb deleted file mode 100644 index 71586b811..000000000 --- a/plugins/communicators/winrm/file_manager.rb +++ /dev/null @@ -1,163 +0,0 @@ -require "log4r" - -module VagrantPlugins - module CommunicatorWinRM - # Manages the file system on the remote guest allowing for file tranfer - # between the guest and host. - class FileManager - def initialize(shell) - @logger = Log4r::Logger.new("vagrant::communication::filemanager") - @shell = shell - end - - # Uploads the given file or directory from the host to the guest (recursively). - # - # @param [String] The source file or directory path on the host - # @param [String] The destination file or directory path on the host - def upload(host_src_file_path, guest_dest_file_path) - @logger.debug("Upload: #{host_src_file_path} -> #{guest_dest_file_path}") - if File.directory?(host_src_file_path) - upload_directory(host_src_file_path, guest_dest_file_path) - else - upload_file(host_src_file_path, guest_dest_file_path) - end - end - - # Downloads the given file from the guest to the host. - # NOTE: This currently only supports single file download - # - # @param [String] The source file path on the guest - # @param [String] The destination file path on the host - def download(guest_src_file_path, host_dest_file_path) - @logger.debug("#{guest_src_file_path} -> #{host_dest_file_path}") - - output = @shell.powershell("[System.convert]::ToBase64String([System.IO.File]::ReadAllBytes(\"#{guest_src_file_path}\"))") - contents = output[:data].map!{|line| line[:stdout]}.join.gsub("\\n\\r", '') - out = Base64.decode64(contents) - IO.binwrite(host_dest_file_path, out) - end - - private - - # Recursively uploads the given directory from the host to the guest - # - # @param [String] The source file or directory path on the host - # @param [String] The destination file or directory path on the host - def upload_directory(host_src_file_path, guest_dest_file_path) - glob_patt = File.join(host_src_file_path, '**/*') - Dir.glob(glob_patt).select { |f| !File.directory?(f) }.each do |host_file_path| - guest_file_path = guest_file_path(host_src_file_path, guest_dest_file_path, host_file_path) - upload_file(host_file_path, guest_file_path) - end - end - - # Uploads the given file, but only if the target file doesn't exist - # or its MD5 checksum doens't match the host's source checksum. - # - # @param [String] The source file path on the host - # @param [String] The destination file path on the guest - def upload_file(host_src_file_path, guest_dest_file_path) - if should_upload_file?(host_src_file_path, guest_dest_file_path) - tmp_file_path = upload_to_temp_file(host_src_file_path) - decode_temp_file(tmp_file_path, guest_dest_file_path) - else - @logger.debug("Up to date: #{guest_dest_file_path}") - end - end - - # Uploads the given file to a new temp file on the guest - # - # @param [String] The source file path on the host - # @return [String] The temp file path on the guest - def upload_to_temp_file(host_src_file_path) - tmp_file_path = File.join(guest_temp_dir, "winrm-upload-#{rand()}") - @logger.debug("Uploading '#{host_src_file_path}' to temp file '#{tmp_file_path}'") - - base64_host_file = Base64.encode64(IO.binread(host_src_file_path)).gsub("\n",'') - base64_host_file.chars.to_a.each_slice(8000-tmp_file_path.size) do |chunk| - out = @shell.cmd("echo #{chunk.join} >> \"#{tmp_file_path}\"") - raise_upload_error_if_failed(out, host_src_file_path, tmp_file_path) - end - - tmp_file_path - end - - # Moves and decodes the given file temp file on the guest to its - # permanent location - # - # @param [String] The source base64 encoded temp file path on the guest - # @param [String] The destination file path on the guest - def decode_temp_file(guest_tmp_file_path, guest_dest_file_path) - @logger.debug("Decoding temp file '#{guest_tmp_file_path}' to '#{guest_dest_file_path}'") - out = @shell.powershell <<-EOH - $tmp_file_path = [System.IO.Path]::GetFullPath('#{guest_tmp_file_path}') - $dest_file_path = [System.IO.Path]::GetFullPath('#{guest_dest_file_path}') - - if (Test-Path $dest_file_path) { - rm $dest_file_path - } - else { - $dest_dir = ([System.IO.Path]::GetDirectoryName($dest_file_path)) - New-Item -ItemType directory -Force -Path $dest_dir - } - - $base64_string = Get-Content $tmp_file_path - $bytes = [System.Convert]::FromBase64String($base64_string) - [System.IO.File]::WriteAllBytes($dest_file_path, $bytes) - EOH - raise_upload_error_if_failed(out, guest_tmp_file_path, guest_dest_file_path) - end - - # Checks to see if the target file on the guest is missing or out of date. - # - # @param [String] The source file path on the host - # @param [String] The destination file path on the guest - # @return [Boolean] True if the file is missing or out of date - def should_upload_file?(host_src_file_path, guest_dest_file_path) - local_md5 = Digest::MD5.file(host_src_file_path).hexdigest - cmd = <<-EOH - $dest_file_path = [System.IO.Path]::GetFullPath('#{guest_dest_file_path}') - - if (Test-Path $dest_file_path) { - $crypto_provider = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider - try { - $file = [System.IO.File]::Open($dest_file_path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read) - $guest_md5 = ([System.BitConverter]::ToString($crypto_provider.ComputeHash($file))).Replace("-","").ToLower() - } - finally { - $file.Dispose() - } - if ($guest_md5 -eq '#{local_md5}') { - exit 0 - } - } - Write-Host "should upload file $dest_file_path" - exit 1 - EOH - @shell.powershell(cmd)[:exitcode] == 1 - end - - # Creates a guest file path equivalent from a host file path - # - # @param [String] The base host directory we're going to copy from - # @param [String] The base guest directory we're going to copy to - # @param [String] A full path to a file on the host underneath host_base_dir - # @return [String] The guest file path equivalent - def guest_file_path(host_base_dir, guest_base_dir, host_file_path) - rel_path = File.dirname(host_file_path[host_base_dir.length, host_file_path.length]) - File.join(guest_base_dir, rel_path, File.basename(host_file_path)) - end - - def guest_temp_dir - @guest_temp ||= (@shell.cmd('echo %TEMP%'))[:data][0][:stdout].chomp - end - - def raise_upload_error_if_failed(out, from, to) - raise Errors::WinRMFileTransferError, - from: from, - to: to, - message: out.inspect if out[:exitcode] != 0 - end - end - end -end diff --git a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb index 454e5e874..923f034bb 100644 --- a/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb +++ b/plugins/communicators/winrm/scripts/elevated_shell.ps1.erb @@ -27,7 +27,7 @@ $task_xml = @' false false - true + false false true diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index 353c0e84d..d9d5f28ce 100644 --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -9,7 +9,7 @@ Vagrant::Util::SilenceWarnings.silence! do require "winrm" end -require_relative "file_manager" +require "winrm-fs/file_manager" module VagrantPlugins module CommunicatorWinRM @@ -22,6 +22,7 @@ module VagrantPlugins @@exceptions_to_retry_on = [ HTTPClient::KeepAliveDisconnected, WinRM::WinRMHTTPTransportError, + WinRM::WinRMAuthorizationError, Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED, @@ -32,23 +33,21 @@ module VagrantPlugins ] attr_reader :logger - attr_reader :username - attr_reader :password attr_reader :host attr_reader :port - attr_reader :timeout_in_seconds - attr_reader :max_tries + attr_reader :username + attr_reader :password + attr_reader :config - def initialize(host, username, password, options = {}) + def initialize(host, port, config) @logger = Log4r::Logger.new("vagrant::communication::winrmshell") @logger.debug("initializing WinRMShell") - @host = host - @port = options[:port] || 5985 - @username = username - @password = password - @timeout_in_seconds = options[:timeout_in_seconds] || 60 - @max_tries = options[:max_tries] || 20 + @host = host + @port = port + @username = config.username + @password = config.password + @config = config end def powershell(command, &block) @@ -67,11 +66,13 @@ module VagrantPlugins end def upload(from, to) - FileManager.new(self).upload(from, to) + file_manager = WinRM::FS::FileManager.new(session) + file_manager.upload(from, to) end def download(from, to) - FileManager.new(self).download(from, to) + file_manager = WinRM::FS::FileManager.new(session) + file_manager.download(from, to) end protected @@ -87,41 +88,80 @@ module VagrantPlugins end def execute_shell_with_retry(command, shell, &block) - retryable(tries: @max_tries, on: @@exceptions_to_retry_on, sleep: 10) do + retryable(tries: @config.max_tries, on: @@exceptions_to_retry_on, sleep: @config.retry_delay) do @logger.debug("#{shell} executing:\n#{command}") output = session.send(shell, 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 + (output[:data] || []).each do |data| + next if !data[:stderr] + if data[:stderr].include?("ParserError") + @logger.warn("Detected ParserError, setting exit code to 1") + output[:exitcode] = 1 + break + end + end + end + return output end end - def raise_winrm_exception(winrm_exception, shell, command) - # If the error is a 401, we can return a more specific error message - if winrm_exception.message.include?("401") - raise Errors::AuthError, - user: @username, - password: @password, - endpoint: endpoint, - message: winrm_exception.message + 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 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 - - raise Errors::ExecutionError, - shell: shell, - command: command, - message: winrm_exception.message end def new_session @logger.info("Attempting to connect to WinRM...") @logger.info(" - Host: #{@host}") @logger.info(" - Port: #{@port}") - @logger.info(" - Username: #{@username}") + @logger.info(" - Username: #{@config.username}") + @logger.info(" - Transport: #{@config.transport}") - client = ::WinRM::WinRMWebService.new(endpoint, :plaintext, endpoint_options) - client.set_timeout(@timeout_in_seconds) + client = ::WinRM::WinRMWebService.new(endpoint, @config.transport.to_sym, endpoint_options) + client.set_timeout(@config.timeout) client.toggle_nori_type_casting(:off) #we don't want coersion of types client end @@ -131,7 +171,14 @@ module VagrantPlugins end def endpoint - "http://#{@host}:#{@port}/wsman" + case @config.transport.to_sym + when :ssl + "https://#{@host}:#{@port}/wsman" + when :plaintext + "http://#{@host}:#{@port}/wsman" + else + raise Errors::WinRMInvalidTransport, transport: @config.transport + end end def endpoint_options @@ -139,8 +186,8 @@ module VagrantPlugins pass: @password, host: @host, port: @port, - operation_timeout: @timeout_in_seconds, - basic_auth_only: true } + basic_auth_only: true, + no_ssl_peer_verification: !@config.ssl_peer_verification } end end #WinShell class end diff --git a/plugins/guests/arch/cap/configure_networks.rb b/plugins/guests/arch/cap/configure_networks.rb index 5d2bc2aa6..fd06c24ea 100644 --- a/plugins/guests/arch/cap/configure_networks.rb +++ b/plugins/guests/arch/cap/configure_networks.rb @@ -10,9 +10,15 @@ module VagrantPlugins include Vagrant::Util def self.configure_networks(machine, networks) + interfaces = Array.new + machine.communicate.sudo("ip -o -0 addr | grep -v LOOPBACK | awk '{print $2}' | sed 's/://'") do |_, result| + interfaces = result.split("\n") + end + networks.each do |network| - entry = TemplateRenderer.render("guests/arch/network_#{network[:type]}", - options: network) + network[:device] = interfaces[network[:interface]] + + entry = TemplateRenderer.render("guests/arch/network_#{network[:type]}", options: network) temp = Tempfile.new("vagrant") temp.binmode @@ -20,26 +26,8 @@ module VagrantPlugins temp.close machine.communicate.upload(temp.path, "/tmp/vagrant_network") - machine.communicate.sudo("ln -sf /dev/null /etc/udev/rules.d/80-net-name-slot.rules") - machine.communicate.sudo("udevadm control --reload") - machine.communicate.sudo("mv /tmp/vagrant_network /etc/netctl/eth#{network[:interface]}") - - # Only consider nth line of sed's output below. There's always an - # offset of two lines in the below sed command given the current - # interface number -> 1: lo, 2: nat device, - snth = network[:interface] + 2 - - # A hack not to rely on udev rule 80-net-name-slot.rules masking - # (ln -sf /dev/null /etc/udev/80-net-name-slot.rules). - # I assume this to be the most portable solution because - # otherwise we would need to rely on the Virtual Machine implementation - # to provide details on the configured interfaces, e.g mac address - # to write a custom udev rule. Templating the netcfg files and - # replacing the correct interface name within ruby seems more - # complicted too (I'm far from being a ruby expert though). - machine.communicate.sudo("sed -i \"s/eth#{network[:interface]}/`ip link | sed -n 's/.*:\\s\\(.*\\): <.*/\\1/p' | sed -n #{snth}p`/g\" /etc/netctl/eth#{network[:interface]}") - machine.communicate.sudo("ip link set eth#{network[:interface]} down") - machine.communicate.sudo("netctl start eth#{network[:interface]}") + machine.communicate.sudo("mv /tmp/vagrant_network /etc/netctl/#{network[:device]}") + machine.communicate.sudo("ip link set #{network[:device]} down && netctl start #{network[:device]}") end end end diff --git a/plugins/guests/darwin/cap/change_host_name.rb b/plugins/guests/darwin/cap/change_host_name.rb index 80df9870d..ad7242c91 100644 --- a/plugins/guests/darwin/cap/change_host_name.rb +++ b/plugins/guests/darwin/cap/change_host_name.rb @@ -4,7 +4,9 @@ module VagrantPlugins class ChangeHostName def self.change_host_name(machine, name) if !machine.communicate.test("hostname -f | grep '^#{name}$' || hostname -s | grep '^#{name}$'") + machine.communicate.sudo("scutil --set ComputerName #{name}") machine.communicate.sudo("scutil --set HostName #{name}") + machine.communicate.sudo("scutil --set LocalHostName #{name}") machine.communicate.sudo("hostname #{name}") end end diff --git a/plugins/guests/darwin/cap/insert_public_key.rb b/plugins/guests/darwin/cap/insert_public_key.rb new file mode 100644 index 000000000..59e5dd58c --- /dev/null +++ b/plugins/guests/darwin/cap/insert_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestDarwin + 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 ~/.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 diff --git a/plugins/guests/darwin/cap/remove_public_key.rb b/plugins/guests/darwin/cap/remove_public_key.rb new file mode 100644 index 000000000..1e0fe415a --- /dev/null +++ b/plugins/guests/darwin/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestDarwin + 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 ~/.ssh/authorized_keys") + comm.execute( + "sed -i '' '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/darwin/cap/rsync.rb b/plugins/guests/darwin/cap/rsync.rb index e98d6e5ae..7e5d6d9a5 100644 --- a/plugins/guests/darwin/cap/rsync.rb +++ b/plugins/guests/darwin/cap/rsync.rb @@ -17,7 +17,7 @@ module VagrantPlugins end def self.rsync_post(machine, opts) - if opts.has_key?(:chown) && !opts[:chown] + if opts.key?(:chown) && !opts[:chown] return end diff --git a/plugins/guests/darwin/plugin.rb b/plugins/guests/darwin/plugin.rb index 9736500bd..305154fde 100644 --- a/plugins/guests/darwin/plugin.rb +++ b/plugins/guests/darwin/plugin.rb @@ -26,6 +26,11 @@ module VagrantPlugins Cap::Halt end + guest_capability("darwin", "insert_public_key") do + require_relative "cap/insert_public_key" + Cap::InsertPublicKey + end + guest_capability("darwin", "mount_nfs_folder") do require_relative "cap/mount_nfs_folder" Cap::MountNFSFolder @@ -36,6 +41,11 @@ module VagrantPlugins Cap::MountVmwareSharedFolder end + guest_capability("darwin", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end + guest_capability("darwin", "rsync_installed") do require_relative "cap/rsync" Cap::RSync diff --git a/plugins/guests/debian/cap/configure_networks.rb b/plugins/guests/debian/cap/configure_networks.rb index 49da980bf..3cb896b15 100644 --- a/plugins/guests/debian/cap/configure_networks.rb +++ b/plugins/guests/debian/cap/configure_networks.rb @@ -13,9 +13,8 @@ module VagrantPlugins machine.communicate.tap do |comm| # First, remove any previous network modifications # from the interface file. - comm.sudo("sed -e '/^#VAGRANT-BEGIN/,/^#VAGRANT-END/ d' /etc/network/interfaces > /tmp/vagrant-network-interfaces") - comm.sudo("su -c 'cat /tmp/vagrant-network-interfaces > /etc/network/interfaces'") - comm.sudo("rm -f /tmp/vagrant-network-interfaces") + 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 @@ -47,8 +46,8 @@ module VagrantPlugins comm.sudo("/sbin/ip addr flush dev eth#{interface} 2> /dev/null") end - comm.sudo("cat /tmp/vagrant-network-entry >> /etc/network/interfaces") - comm.sudo("rm -f /tmp/vagrant-network-entry") + 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| diff --git a/plugins/guests/debian/guest.rb b/plugins/guests/debian/guest.rb index a7f3d65de..be08b3605 100644 --- a/plugins/guests/debian/guest.rb +++ b/plugins/guests/debian/guest.rb @@ -2,7 +2,7 @@ module VagrantPlugins module GuestDebian class Guest < Vagrant.plugin("2", :guest) def detect?(machine) - machine.communicate.test("cat /etc/issue | grep 'Debian'") + machine.communicate.test("cat /etc/issue | grep 'Debian' | grep -v '8'") end end end diff --git a/plugins/guests/debian8/cap/halt.rb b/plugins/guests/debian8/cap/halt.rb new file mode 100644 index 000000000..932281347 --- /dev/null +++ b/plugins/guests/debian8/cap/halt.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module GuestDebian8 + module Cap + class Halt + def self.halt(machine) + begin + machine.communicate.sudo("shutdown -h -H") + rescue IOError + # Do nothing, because it probably means the machine shut down + # and SSH connection was lost. + end + end + end + end + end +end diff --git a/plugins/guests/debian8/guest.rb b/plugins/guests/debian8/guest.rb new file mode 100644 index 000000000..ec95e9e85 --- /dev/null +++ b/plugins/guests/debian8/guest.rb @@ -0,0 +1,9 @@ +module VagrantPlugins + module GuestDebian8 + class Guest < Vagrant.plugin("2", :guest) + def detect?(machine) + machine.communicate.test("cat /etc/issue | grep 'Debian' | grep '8'") + end + end + end +end diff --git a/plugins/guests/debian8/plugin.rb b/plugins/guests/debian8/plugin.rb new file mode 100644 index 000000000..1f566f5fa --- /dev/null +++ b/plugins/guests/debian8/plugin.rb @@ -0,0 +1,21 @@ +require "vagrant" + +module VagrantPlugins + module GuestDebian8 + class Plugin < Vagrant.plugin("2") + name "Debian Jessie guest" + description "Debian Jessie guest support." + + guest("debian8", "debian") do + require File.expand_path("../guest", __FILE__) + Guest + end + + guest_capability("debian8", "halt") do + require_relative "cap/halt" + Cap::Halt + end + + end + end +end diff --git a/plugins/guests/fedora/cap/configure_networks.rb b/plugins/guests/fedora/cap/configure_networks.rb index 549a99dff..2f67099cd 100644 --- a/plugins/guests/fedora/cap/configure_networks.rb +++ b/plugins/guests/fedora/cap/configure_networks.rb @@ -26,7 +26,7 @@ module VagrantPlugins end interface_names = networks.map do |network| - "eth#{network[:interface]}" + "#{interface_names[network[:interface]]}" end else machine.communicate.sudo("/usr/sbin/biosdevname -d | grep Kernel | cut -f2 -d: | sed -e 's/ //;'") do |_, result| diff --git a/plugins/guests/fedora/cap/network_scripts_dir.rb b/plugins/guests/fedora/cap/network_scripts_dir.rb index 3ce0e43c3..82a4abad7 100644 --- a/plugins/guests/fedora/cap/network_scripts_dir.rb +++ b/plugins/guests/fedora/cap/network_scripts_dir.rb @@ -4,7 +4,7 @@ module VagrantPlugins class NetworkScriptsDir # The path to the directory with the network configuration scripts. # This is pulled out into its own directory since there are other - # operating systems (SuSE) which behave similarly but with a different + # operating systems (SUSE) which behave similarly but with a different # path to the network scripts. def self.network_scripts_dir(machine) "/etc/sysconfig/network-scripts" diff --git a/plugins/guests/fedora/guest.rb b/plugins/guests/fedora/guest.rb index 5c74e7869..64a5e3d86 100644 --- a/plugins/guests/fedora/guest.rb +++ b/plugins/guests/fedora/guest.rb @@ -4,7 +4,7 @@ module VagrantPlugins module GuestFedora class Guest < Vagrant.plugin("2", :guest) def detect?(machine) - machine.communicate.test("grep 'Fedora release [12][67890]' /etc/redhat-release") + machine.communicate.test("grep 'Fedora release [12][678901]' /etc/redhat-release") end end end diff --git a/plugins/guests/freebsd/cap/mount_nfs_folder.rb b/plugins/guests/freebsd/cap/mount_nfs_folder.rb index c36f928ca..58d8f785c 100644 --- a/plugins/guests/freebsd/cap/mount_nfs_folder.rb +++ b/plugins/guests/freebsd/cap/mount_nfs_folder.rb @@ -4,8 +4,15 @@ module VagrantPlugins class MountNFSFolder def self.mount_nfs_folder(machine, ip, folders) folders.each do |name, opts| + if opts[:nfs_version] + nfs_version_mount_option="-o nfsv#{opts[:nfs_version]}" + end + machine.communicate.sudo("mkdir -p #{opts[:guestpath]}", {shell: "sh"}) - machine.communicate.sudo("mount -t nfs '#{ip}:#{opts[:hostpath]}' '#{opts[:guestpath]}'", {shell: "sh"}) + + machine.communicate.sudo( + "mount -t nfs #{nfs_version_mount_option} " + + "'#{ip}:#{opts[:hostpath]}' '#{opts[:guestpath]}'", {shell: "sh"}) end end end diff --git a/plugins/guests/freebsd/cap/remove_public_key.rb b/plugins/guests/freebsd/cap/remove_public_key.rb new file mode 100644 index 000000000..13abc864b --- /dev/null +++ b/plugins/guests/freebsd/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestFreeBSD + 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 ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/freebsd/cap/rsync.rb b/plugins/guests/freebsd/cap/rsync.rb index 2876b5943..c8fb36006 100644 --- a/plugins/guests/freebsd/cap/rsync.rb +++ b/plugins/guests/freebsd/cap/rsync.rb @@ -31,7 +31,7 @@ module VagrantPlugins end def self.rsync_post(machine, opts) - if opts.has_key?(:chown) && !opts[:chown] + if opts.key?(:chown) && !opts[:chown] return end diff --git a/plugins/guests/freebsd/plugin.rb b/plugins/guests/freebsd/plugin.rb index 49aa3298b..a4ece8de2 100644 --- a/plugins/guests/freebsd/plugin.rb +++ b/plugins/guests/freebsd/plugin.rb @@ -36,6 +36,11 @@ module VagrantPlugins Cap::MountNFSFolder end + guest_capability("freebsd", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end + guest_capability("freebsd", "rsync_install") do require_relative "cap/rsync" Cap::RSync diff --git a/plugins/guests/funtoo/cap/configure_networks.rb b/plugins/guests/funtoo/cap/configure_networks.rb index 466e3a57b..c52182b6f 100644 --- a/plugins/guests/funtoo/cap/configure_networks.rb +++ b/plugins/guests/funtoo/cap/configure_networks.rb @@ -28,7 +28,7 @@ module VagrantPlugins temp.binmode temp.write(entry) temp.close - comm.upload(temp.path, "/tmp/vagrant-network-entry-#{ifFile}") + comm.upload(temp.path, "/tmp/vagrant-#{ifFile}") 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}") diff --git a/plugins/guests/linux/cap/mount_smb_shared_folder.rb b/plugins/guests/linux/cap/mount_smb_shared_folder.rb index d50aae686..424982520 100644 --- a/plugins/guests/linux/cap/mount_smb_shared_folder.rb +++ b/plugins/guests/linux/cap/mount_smb_shared_folder.rb @@ -51,10 +51,15 @@ module VagrantPlugins while true success = true + stderr = "" mount_commands.each do |command| no_such_device = false + stderr = "" status = machine.communicate.sudo(command, error_check: false) do |type, data| - no_such_device = true if type == :stderr && data =~ /No such device/i + if type == :stderr + no_such_device = true if data =~ /No such device/i + stderr += data.to_s + end end success = status == 0 && !no_such_device @@ -69,7 +74,8 @@ module VagrantPlugins command.gsub!(smb_password, "PASSWORDHIDDEN") raise Vagrant::Errors::LinuxMountFailed, - command: command + command: command, + output: stderr end sleep 2 diff --git a/plugins/guests/linux/cap/remove_public_key.rb b/plugins/guests/linux/cap/remove_public_key.rb new file mode 100644 index 000000000..a8d773a40 --- /dev/null +++ b/plugins/guests/linux/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestLinux + 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 ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/linux/cap/rsync.rb b/plugins/guests/linux/cap/rsync.rb index 113b18dd0..424d20f46 100644 --- a/plugins/guests/linux/cap/rsync.rb +++ b/plugins/guests/linux/cap/rsync.rb @@ -17,7 +17,7 @@ module VagrantPlugins end def self.rsync_post(machine, opts) - if opts.has_key?(:chown) && !opts[:chown] + if opts.key?(:chown) && !opts[:chown] return end diff --git a/plugins/guests/linux/plugin.rb b/plugins/guests/linux/plugin.rb index a2bb1167c..cadd4aa95 100644 --- a/plugins/guests/linux/plugin.rb +++ b/plugins/guests/linux/plugin.rb @@ -62,6 +62,11 @@ module VagrantPlugins Cap::ReadIPAddress end + guest_capability("linux", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end + guest_capability("linux", "rsync_installed") do require_relative "cap/rsync" Cap::RSync diff --git a/plugins/guests/netbsd/cap/remove_public_key.rb b/plugins/guests/netbsd/cap/remove_public_key.rb new file mode 100644 index 000000000..d25e97679 --- /dev/null +++ b/plugins/guests/netbsd/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestNetBSD + 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 ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/netbsd/plugin.rb b/plugins/guests/netbsd/plugin.rb index 490c73644..9636c6630 100644 --- a/plugins/guests/netbsd/plugin.rb +++ b/plugins/guests/netbsd/plugin.rb @@ -36,6 +36,11 @@ module VagrantPlugins Cap::MountNFSFolder end + guest_capability("netbsd", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end + guest_capability("netbsd", "rsync_install") do require_relative "cap/rsync" Cap::RSync diff --git a/plugins/guests/openbsd/cap/remove_public_key.rb b/plugins/guests/openbsd/cap/remove_public_key.rb new file mode 100644 index 000000000..94cfce03b --- /dev/null +++ b/plugins/guests/openbsd/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestOpenBSD + 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 ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/openbsd/plugin.rb b/plugins/guests/openbsd/plugin.rb index 7a64b16d6..a1ddfe9c1 100644 --- a/plugins/guests/openbsd/plugin.rb +++ b/plugins/guests/openbsd/plugin.rb @@ -36,6 +36,11 @@ module VagrantPlugins Cap::MountNFSFolder end + guest_capability("openbsd", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end + guest_capability("openbsd", "rsync_install") do require_relative "cap/rsync" Cap::RSync diff --git a/plugins/guests/redhat/cap/change_host_name.rb b/plugins/guests/redhat/cap/change_host_name.rb index 72ad47e8d..2520904ec 100644 --- a/plugins/guests/redhat/cap/change_host_name.rb +++ b/plugins/guests/redhat/cap/change_host_name.rb @@ -3,16 +3,7 @@ module VagrantPlugins module Cap class ChangeHostName def self.change_host_name(machine, name) - case machine.guest.capability("flavor") - when :rhel_7 - change_host_name_rhel7(machine, name) - else - new(machine, name).change! - end - end - - def self.change_host_name_rhel7(machine, name) - machine.communicate.sudo("hostnamectl set-hostname #{name}") + new(machine, name).change! end attr_reader :machine, :new_hostname @@ -25,11 +16,17 @@ module VagrantPlugins def change! return unless should_change? - update_sysconfig - update_hostname - update_etc_hosts - update_dhcp_hostnames - restart_networking + case machine.guest.capability("flavor") + when :rhel_7 + update_hostname_rhel7 + update_etc_hosts + else + update_sysconfig + update_hostname + update_etc_hosts + update_dhcp_hostnames + restart_networking + end end def should_change? @@ -61,6 +58,10 @@ module VagrantPlugins sudo "hostname #{fqdn}" end + def update_hostname_rhel7 + sudo "hostnamectl set-hostname #{fqdn}" + end + # /etc/hosts should resemble: # 127.0.0.1 host.fqdn.com host localhost ... def update_etc_hosts diff --git a/plugins/guests/redhat/cap/flavor.rb b/plugins/guests/redhat/cap/flavor.rb index b85810a06..2ae87201a 100644 --- a/plugins/guests/redhat/cap/flavor.rb +++ b/plugins/guests/redhat/cap/flavor.rb @@ -11,7 +11,7 @@ module VagrantPlugins output.chomp! # Detect various flavors we care about - if output =~ /(CentOS|Red Hat Enterprise) Linux( .+)? release 7/i + if output =~ /(CentOS|Red Hat Enterprise|Scientific) Linux( .+)? release 7/i return :rhel_7 else return :rhel diff --git a/plugins/guests/redhat/cap/nfs_client.rb b/plugins/guests/redhat/cap/nfs_client.rb index 98858f70f..0e8023062 100644 --- a/plugins/guests/redhat/cap/nfs_client.rb +++ b/plugins/guests/redhat/cap/nfs_client.rb @@ -5,7 +5,12 @@ module VagrantPlugins def self.nfs_client_install(machine) machine.communicate.tap do |comm| comm.sudo("yum -y install nfs-utils nfs-utils-lib") - comm.sudo("/etc/init.d/rpcbind restart; /etc/init.d/nfs restart") + case machine.guest.capability("flavor") + when :rhel_7 + comm.sudo("/bin/systemctl restart rpcbind nfs") + else + comm.sudo("/etc/init.d/rpcbind restart; /etc/init.d/nfs restart") + end end end end diff --git a/plugins/guests/solaris/cap/insert_public_key.rb b/plugins/guests/solaris/cap/insert_public_key.rb new file mode 100644 index 000000000..b0b13d041 --- /dev/null +++ b/plugins/guests/solaris/cap/insert_public_key.rb @@ -0,0 +1,22 @@ +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 diff --git a/plugins/guests/solaris/cap/remove_public_key.rb b/plugins/guests/solaris/cap/remove_public_key.rb new file mode 100644 index 000000000..170d63870 --- /dev/null +++ b/plugins/guests/solaris/cap/remove_public_key.rb @@ -0,0 +1,22 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestSolaris + module Cap + class RemovePublicKey + def self.remove_public_key(machine, contents) + # TODO: code is identical to linux/cap/remove_public_key + contents = contents.chomp + contents = Vagrant::Util::ShellQuote.escape(contents, "'") + + machine.communicate.tap do |comm| + if comm.test("test -f ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/solaris/cap/rsync.rb b/plugins/guests/solaris/cap/rsync.rb index d060a8020..0d0220f40 100644 --- a/plugins/guests/solaris/cap/rsync.rb +++ b/plugins/guests/solaris/cap/rsync.rb @@ -17,10 +17,10 @@ module VagrantPlugins end def self.rsync_post(machine, opts) - su_cmd = machine.config.solaris.su_cmd + suexec_cmd = machine.config.solaris.suexec_cmd machine.communicate.execute( - "#{su_cmd} find '#{opts[:guestpath]}' '(' ! -user #{opts[:owner]} -or ! -group #{opts[:group]} ')' -print0 | " + - "xargs -0 -r chown #{opts[:owner]}:#{opts[:group]}") + "#{suexec_cmd} find '#{opts[:guestpath]}' '(' ! -user #{opts[:owner]} -or ! -group #{opts[:group]} ')' -print0 | " + + "xargs -0 chown #{opts[:owner]}:#{opts[:group]}") end end end diff --git a/plugins/guests/solaris/config.rb b/plugins/guests/solaris/config.rb index e0a6a486b..5983b6837 100644 --- a/plugins/guests/solaris/config.rb +++ b/plugins/guests/solaris/config.rb @@ -3,24 +3,28 @@ module VagrantPlugins 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 = 'sudo' - @device = "e1000g" + @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 diff --git a/plugins/guests/solaris/plugin.rb b/plugins/guests/solaris/plugin.rb index 31988453c..9f2b315ce 100644 --- a/plugins/guests/solaris/plugin.rb +++ b/plugins/guests/solaris/plugin.rb @@ -26,6 +26,11 @@ module VagrantPlugins 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 @@ -36,6 +41,11 @@ module VagrantPlugins 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 diff --git a/plugins/guests/solaris11/cap/configure_networks.rb b/plugins/guests/solaris11/cap/configure_networks.rb index 92e47703a..15aa62679 100644 --- a/plugins/guests/solaris11/cap/configure_networks.rb +++ b/plugins/guests/solaris11/cap/configure_networks.rb @@ -19,6 +19,9 @@ module VagrantPlugins #machine.communicate.execute("#{ifconfig_cmd} up") #machine.communicate.execute("#{su_cmd} sh -c \"echo '#{network[:ip]}' > /etc/hostname.#{device}\"") # ipadm create-addr -T static -a local=172.16.10.15/24 net2/v4 + 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 #machine.communicate.execute("#{ifconfig_cmd} dhcp start") diff --git a/plugins/guests/solaris11/cap/insert_public_key.rb b/plugins/guests/solaris11/cap/insert_public_key.rb new file mode 100644 index 000000000..55087c366 --- /dev/null +++ b/plugins/guests/solaris11/cap/insert_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestSolaris11 + 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 ~/.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 diff --git a/plugins/guests/solaris11/cap/remove_public_key.rb b/plugins/guests/solaris11/cap/remove_public_key.rb new file mode 100644 index 000000000..91924d3c5 --- /dev/null +++ b/plugins/guests/solaris11/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestSolaris11 + 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 ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/solaris11/cap/rsync.rb b/plugins/guests/solaris11/cap/rsync.rb index 039602172..95df046c9 100644 --- a/plugins/guests/solaris11/cap/rsync.rb +++ b/plugins/guests/solaris11/cap/rsync.rb @@ -17,10 +17,10 @@ module VagrantPlugins end def self.rsync_post(machine, opts) - su_cmd = machine.config.solaris11.su_cmd + suexec_cmd = machine.config.solaris11.suexec_cmd machine.communicate.execute( - "#{su_cmd} '#{opts[:guestpath]}' '(' ! -user #{opts[:owner]} -or ! -group #{opts[:group]} ')' -print0 | " + - "xargs -0 -r chown #{opts[:owner]}:#{opts[:group]}") + "#{suexec_cmd} '#{opts[:guestpath]}' '(' ! -user #{opts[:owner]} -or ! -group #{opts[:group]} ')' -print0 | " + + "xargs -0 chown #{opts[:owner]}:#{opts[:group]}") end end end diff --git a/plugins/guests/solaris11/plugin.rb b/plugins/guests/solaris11/plugin.rb index 707fd2bfc..1a5d07451 100644 --- a/plugins/guests/solaris11/plugin.rb +++ b/plugins/guests/solaris11/plugin.rb @@ -49,6 +49,16 @@ module VagrantPlugins require_relative "cap/rsync" Cap::RSync end + + guest_capability("solaris11", "insert_public_key") do + require_relative "cap/insert_public_key" + Cap::InsertPublicKey + end + + guest_capability("solaris11", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end end end end diff --git a/plugins/guests/suse/cap/change_host_name.rb b/plugins/guests/suse/cap/change_host_name.rb index a8ba242d7..616b92495 100644 --- a/plugins/guests/suse/cap/change_host_name.rb +++ b/plugins/guests/suse/cap/change_host_name.rb @@ -1,5 +1,5 @@ module VagrantPlugins - module GuestSuse + module GuestSUSE module Cap class ChangeHostName def self.change_host_name(machine, name) @@ -8,6 +8,7 @@ module VagrantPlugins if !comm.test("sudo hostname | grep '#{name}'") comm.sudo("echo #{name} > /etc/HOSTNAME") comm.sudo("hostname #{name}") + comm.sudo("sed -i 's@^\\(127[.]0[.]0[.]1[[:space:]]\\+\\)@\\1#{name} #{name.split('.')[0]} @' /etc/hosts") end end diff --git a/plugins/guests/suse/cap/configure_networks.rb b/plugins/guests/suse/cap/configure_networks.rb index d60bb8306..00902dbd0 100644 --- a/plugins/guests/suse/cap/configure_networks.rb +++ b/plugins/guests/suse/cap/configure_networks.rb @@ -5,7 +5,7 @@ require "vagrant/util/retryable" require "vagrant/util/template_renderer" module VagrantPlugins - module GuestSuse + module GuestSUSE module Cap class ConfigureNetworks extend Vagrant::Util::Retryable diff --git a/plugins/guests/suse/cap/halt.rb b/plugins/guests/suse/cap/halt.rb index ea1957594..e6b20818b 100644 --- a/plugins/guests/suse/cap/halt.rb +++ b/plugins/guests/suse/cap/halt.rb @@ -1,5 +1,5 @@ module VagrantPlugins - module GuestSuse + module GuestSUSE module Cap class Halt def self.halt(machine) diff --git a/plugins/guests/suse/cap/network_scripts_dir.rb b/plugins/guests/suse/cap/network_scripts_dir.rb index d8c34b290..82b2e7f4a 100644 --- a/plugins/guests/suse/cap/network_scripts_dir.rb +++ b/plugins/guests/suse/cap/network_scripts_dir.rb @@ -1,9 +1,9 @@ module VagrantPlugins - module GuestSuse + module GuestSUSE module Cap class NetworkScriptsDir def self.network_scripts_dir(machine) - "/etc/sysconfig/network/" + "/etc/sysconfig/network" end end end diff --git a/plugins/guests/suse/cap/nfs_client.rb b/plugins/guests/suse/cap/nfs_client.rb new file mode 100644 index 000000000..36a92d201 --- /dev/null +++ b/plugins/guests/suse/cap/nfs_client.rb @@ -0,0 +1,16 @@ +module VagrantPlugins + module GuestSUSE + module Cap + class NFSClient + def self.nfs_client_install(machine) + machine.communicate.tap do |comm| + comm.sudo("zypper -n install nfs-client") + + comm.sudo("/sbin/service rpcbind restart") + comm.sudo("/sbin/service nfs restart") + end + end + end + end + end +end diff --git a/plugins/guests/suse/cap/rsync.rb b/plugins/guests/suse/cap/rsync.rb new file mode 100644 index 000000000..aed989613 --- /dev/null +++ b/plugins/guests/suse/cap/rsync.rb @@ -0,0 +1,17 @@ +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.tap do |comm| + comm.sudo("zypper -n install rsync") + end + end + end + end + end +end diff --git a/plugins/guests/suse/guest.rb b/plugins/guests/suse/guest.rb index 340f5818d..643403f19 100644 --- a/plugins/guests/suse/guest.rb +++ b/plugins/guests/suse/guest.rb @@ -1,10 +1,10 @@ require "vagrant" module VagrantPlugins - module GuestSuse + module GuestSUSE class Guest < Vagrant.plugin("2", :guest) def detect?(machine) - machine.communicate.test("cat /etc/SuSE-release") + machine.communicate.test("test -f /etc/SuSE-release || grep -q SUSE /etc/os-release") end end end diff --git a/plugins/guests/suse/plugin.rb b/plugins/guests/suse/plugin.rb index 0aa9bd289..3850ae45c 100644 --- a/plugins/guests/suse/plugin.rb +++ b/plugins/guests/suse/plugin.rb @@ -1,12 +1,12 @@ require "vagrant" module VagrantPlugins - module GuestSuse + module GuestSUSE class Plugin < Vagrant.plugin("2") name "SUSE guest" description "SUSE guest support." - guest("suse", "redhat") do + guest("suse", "linux") do require File.expand_path("../guest", __FILE__) Guest end @@ -16,20 +16,35 @@ module VagrantPlugins Cap::ChangeHostName end - guest_capability("suse", "halt") do - require_relative "cap/halt" - Cap::Halt - 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 diff --git a/plugins/guests/tinycore/cap/change_host_name.rb b/plugins/guests/tinycore/cap/change_host_name.rb new file mode 100644 index 000000000..2c16af362 --- /dev/null +++ b/plugins/guests/tinycore/cap/change_host_name.rb @@ -0,0 +1,14 @@ +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("sh -c 'echo \"#{name}\" > /etc/hostname'") + machine.communicate.sudo("/usr/bin/sethostname #{name}") + end + end + end + end + end +end diff --git a/plugins/guests/tinycore/cap/configure_networks.rb b/plugins/guests/tinycore/cap/configure_networks.rb index cccc0e311..3e267748a 100644 --- a/plugins/guests/tinycore/cap/configure_networks.rb +++ b/plugins/guests/tinycore/cap/configure_networks.rb @@ -7,6 +7,11 @@ module VagrantPlugins 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") diff --git a/plugins/guests/ubuntu/guest.rb b/plugins/guests/ubuntu/guest.rb index 9e9a9a9ad..9295ede22 100644 --- a/plugins/guests/ubuntu/guest.rb +++ b/plugins/guests/ubuntu/guest.rb @@ -4,7 +4,7 @@ module VagrantPlugins module GuestUbuntu class Guest < Vagrant.plugin("2", :guest) def detect?(machine) - machine.communicate.test("cat /etc/issue | grep 'Ubuntu'") + machine.communicate.test("[ -x /usr/bin/lsb_release ] && /usr/bin/lsb_release -i 2>/dev/null | grep Ubuntu") end end end diff --git a/plugins/guests/windows/cap/choose_addressable_ip_addr.rb b/plugins/guests/windows/cap/choose_addressable_ip_addr.rb new file mode 100644 index 000000000..2304246ce --- /dev/null +++ b/plugins/guests/windows/cap/choose_addressable_ip_addr.rb @@ -0,0 +1,20 @@ +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 diff --git a/plugins/guests/windows/cap/mount_shared_folder.rb b/plugins/guests/windows/cap/mount_shared_folder.rb index 8f7866a1a..4329025f6 100644 --- a/plugins/guests/windows/cap/mount_shared_folder.rb +++ b/plugins/guests/windows/cap/mount_shared_folder.rb @@ -16,6 +16,11 @@ module VagrantPlugins mount_shared_folder(machine, name, guestpath, "\\\\psf\\") end + def self.mount_smb_shared_folder(machine, name, guestpath, options) + 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) diff --git a/plugins/guests/windows/cap/rsync.rb b/plugins/guests/windows/cap/rsync.rb new file mode 100644 index 000000000..e391b92db --- /dev/null +++ b/plugins/guests/windows/cap/rsync.rb @@ -0,0 +1,13 @@ +module VagrantPlugins + module GuestWindows + module Cap + class RSync + def self.rsync_pre(machine, opts) + machine.communicate.tap do |comm| + comm.execute("mkdir '#{opts[:guestpath]}'") + end + end + end + end + end +end diff --git a/plugins/guests/windows/plugin.rb b/plugins/guests/windows/plugin.rb index fa1408314..47ca67e3f 100644 --- a/plugins/guests/windows/plugin.rb +++ b/plugins/guests/windows/plugin.rb @@ -54,6 +54,21 @@ module VagrantPlugins 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_pre) do + require_relative "cap/rsync" + Cap::RSync + end + protected def self.init! diff --git a/plugins/hosts/bsd/cap/nfs.rb b/plugins/hosts/bsd/cap/nfs.rb index df37f559d..5d31c3cc1 100644 --- a/plugins/hosts/bsd/cap/nfs.rb +++ b/plugins/hosts/bsd/cap/nfs.rb @@ -102,10 +102,16 @@ module VagrantPlugins # 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 tee -a /etc/exports >/dev/null") + system( + "echo '#{line}' | " + + "#{sudo_command}tee -a /etc/exports >/dev/null") end # We run restart here instead of "update" just in case nfsd @@ -131,7 +137,7 @@ module VagrantPlugins user = Process.uid File.read("/etc/exports").lines.each do |line| - if id = line[/^# VAGRANT-BEGIN:( #{user})? ([A-Za-z0-9-]+?)$/, 2] + if id = line[/^# VAGRANT-BEGIN:( #{user})? ([\.\/A-Za-z0-9\-_:]+?)$/, 2] if valid_ids.include?(id) logger.debug("Valid ID: #{id}") else @@ -165,12 +171,19 @@ module VagrantPlugins 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( - "sudo", "sed", "-E", "-e", - "/^# VAGRANT-BEGIN:( #{user})? #{id}/,/^# VAGRANT-END:( #{user})? #{id}/ d", - "-ibak", "/etc/exports") + system(*command) end def self.nfs_checkexports! diff --git a/plugins/hosts/linux/cap/nfs.rb b/plugins/hosts/linux/cap/nfs.rb index 6b1c95c43..bf10aff9e 100644 --- a/plugins/hosts/linux/cap/nfs.rb +++ b/plugins/hosts/linux/cap/nfs.rb @@ -38,9 +38,13 @@ module VagrantPlugins 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.split("\n").each do |line| line = Vagrant::Util::ShellQuote.escape(line, "'") - system(%Q[echo '#{line}' | sudo tee -a /etc/exports >/dev/null]) + system(%Q[echo '#{line}' | #{sudo_command}tee -a /etc/exports >/dev/null]) end if nfs_running?(nfs_check_command) @@ -67,7 +71,7 @@ module VagrantPlugins user = Process.uid File.read("/etc/exports").lines.each do |line| - if id = line[/^# VAGRANT-BEGIN:( #{user})? ([A-Za-z0-9-]+?)$/, 2] + if id = line[/^# VAGRANT-BEGIN:( #{user})? ([\.\/A-Za-z0-9\-_:]+?)$/, 2] if valid_ids.include?(id) logger.debug("Valid ID: #{id}") else @@ -92,9 +96,13 @@ module VagrantPlugins user = Regexp.escape(Process.uid.to_s) id = Regexp.escape(id.to_s) + # Only use "sudo" if we can't write to /etc/exports directly + sudo_command = "" + sudo_command = "sudo " if !File.writable?("/etc/exports") + # Use sed to just strip out the block of code which was inserted # by Vagrant - system("sudo sed -r -e '\\\x01^# VAGRANT-BEGIN:( #{user})? #{id}\x01,\\\x01^# VAGRANT-END:( #{user})? #{id}\x01 d' -ibak /etc/exports") + system("#{sudo_command}sed -r -e '\\\x01^# VAGRANT-BEGIN:( #{user})? #{id}\x01,\\\x01^# VAGRANT-END:( #{user})? #{id}\x01 d' -ibak /etc/exports") end def self.nfs_opts_setup(folders) diff --git a/plugins/hosts/opensuse/host.rb b/plugins/hosts/opensuse/host.rb deleted file mode 100644 index 5e0240d27..000000000 --- a/plugins/hosts/opensuse/host.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "pathname" - -require "vagrant" - -module VagrantPlugins - module HostOpenSUSE - class Host < Vagrant.plugin("2", :host) - def detect?(env) - release_file = Pathname.new("/etc/SuSE-release") - - if release_file.exist? - release_file.open("r") do |f| - return true if f.gets =~ /^openSUSE/ - end - end - - false - end - end - end -end diff --git a/plugins/hosts/opensuse/plugin.rb b/plugins/hosts/opensuse/plugin.rb deleted file mode 100644 index 6ed81f8da..000000000 --- a/plugins/hosts/opensuse/plugin.rb +++ /dev/null @@ -1,27 +0,0 @@ -require "vagrant" - -module VagrantPlugins - module HostOpenSUSE - class Plugin < Vagrant.plugin("2") - name "OpenSUSE host" - description "OpenSUSE host support." - - host("opensuse", "linux") do - require_relative "host" - Host - end - - # Linux-specific helpers we need to determine paths that can - # be overriden. - host_capability("opensuse", "nfs_check_command") do - require_relative "cap/nfs" - Cap::NFS - end - - host_capability("opensuse", "nfs_start_command") do - require_relative "cap/nfs" - Cap::NFS - end - end - end -end diff --git a/plugins/hosts/opensuse/cap/nfs.rb b/plugins/hosts/suse/cap/nfs.rb similarity index 58% rename from plugins/hosts/opensuse/cap/nfs.rb rename to plugins/hosts/suse/cap/nfs.rb index d16bc9327..42bde6bb2 100644 --- a/plugins/hosts/opensuse/cap/nfs.rb +++ b/plugins/hosts/suse/cap/nfs.rb @@ -1,9 +1,13 @@ module VagrantPlugins - module HostOpenSUSE + 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) - "/sbin/service nfsserver status" + "pidof nfsd > /dev/null" end def self.nfs_start_command(env) diff --git a/plugins/hosts/suse/host.rb b/plugins/hosts/suse/host.rb new file mode 100644 index 000000000..2cc5c24c7 --- /dev/null +++ b/plugins/hosts/suse/host.rb @@ -0,0 +1,29 @@ +require "pathname" + +require "vagrant" + +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 diff --git a/plugins/hosts/suse/plugin.rb b/plugins/hosts/suse/plugin.rb new file mode 100644 index 000000000..3aa1b441f --- /dev/null +++ b/plugins/hosts/suse/plugin.rb @@ -0,0 +1,30 @@ +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 diff --git a/plugins/hosts/windows/cap/rdp.rb b/plugins/hosts/windows/cap/rdp.rb index 9976834de..8ddc67117 100644 --- a/plugins/hosts/windows/cap/rdp.rb +++ b/plugins/hosts/windows/cap/rdp.rb @@ -10,7 +10,6 @@ module VagrantPlugins def self.rdp_client(env, rdp_info) config = nil opts = { - "drivestoredirect:s" => "*", "full address:s" => "#{rdp_info[:host]}:#{rdp_info[:port]}", "prompt for credentials:i" => "1", "username:s" => rdp_info[:username], diff --git a/plugins/kernel_v2/config/push.rb b/plugins/kernel_v2/config/push.rb new file mode 100644 index 000000000..ccba3826d --- /dev/null +++ b/plugins/kernel_v2/config/push.rb @@ -0,0 +1,143 @@ +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, reseting 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! + + # 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 diff --git a/plugins/kernel_v2/config/vm.rb b/plugins/kernel_v2/config/vm.rb index 31af48df7..11947a30c 100644 --- a/plugins/kernel_v2/config/vm.rb +++ b/plugins/kernel_v2/config/vm.rb @@ -63,6 +63,7 @@ module VagrantPlugins @__finalized = false @__networks = {} @__providers = {} + @__provider_order = [] @__provider_overrides = {} @__synced_folders = {} end @@ -95,7 +96,7 @@ module VagrantPlugins end other_defined_vms.each do |key, subvm| - if !new_defined_vms.has_key?(key) + if !new_defined_vms.key?(key) new_defined_vms[key] = subvm.clone else new_defined_vms[key].config_procs.concat(subvm.config_procs) @@ -112,6 +113,12 @@ module VagrantPlugins 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 @@ -125,8 +132,8 @@ module VagrantPlugins new_provs = [] other_provs = other.provisioners.dup @provisioners.each do |p| - if p.id - other_p = other_provs.find { |o| p.id == o.id } + if p.name + other_p = other_provs.find { |o| p.name == o.name } if other_p # There is an override. Take it. other_p.config = p.config.merge(other_p.config) @@ -162,6 +169,7 @@ module VagrantPlugins 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 @@ -189,7 +197,7 @@ module VagrantPlugins options ||= {} options[:guestpath] = guestpath.to_s.gsub(/\/$/, '') options[:hostpath] = hostpath - options[:disabled] = false if !options.has_key?(:disabled) + options[:disabled] = false if !options.key?(:disabled) options = (@__synced_folders[options[:guestpath]] || {}). merge(options.dup) @@ -239,7 +247,7 @@ module VagrantPlugins id = "#{type}-#{id}" # Merge in the previous settings if we have them. - if @__networks.has_key?(id) + if @__networks.key?(id) options = @__networks[id][1].merge(options) end @@ -255,6 +263,9 @@ module VagrantPlugins @__providers[name] ||= [] @__provider_overrides[name] ||= [] + # Add the provider to the ordering list + @__provider_order << name + if block_given? @__providers[name] << block if block_given? @@ -267,21 +278,36 @@ module VagrantPlugins end def provision(name, **options, &block) - id = options.delete(:id).to_s if options.has_key?(:id) + 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 id - prov = @provisioners.find { |p| p.id == id } + if name + name = name.to_sym + prov = @provisioners.find { |p| p.name == name } end if !prov - prov = VagrantConfigProvisioner.new(id, name.to_sym) + prov = VagrantConfigProvisioner.new(name, type.to_sym) @provisioners << prov end prov.preserve_order = !!options.delete(:preserve_order) if \ - options.has_key?(:preserve_order) - prov.run = options.delete(:run) if options.has_key?(:run) + options.key?(:preserve_order) + prov.run = options.delete(:run) if options.key?(:run) prov.add_config(options, &block) nil end @@ -373,10 +399,15 @@ module VagrantPlugins host_ip: "127.0.0.1", id: "winrm", auto_correct: true - end - end - if !@__networks["forwarded_port-ssh"] + network :forwarded_port, + guest: 5986, + host: 55986, + host_ip: "127.0.0.1", + id: "winrm-ssl", + auto_correct: true + end + elsif !@__networks["forwarded_port-ssh"] network :forwarded_port, guest: 22, host: 2222, @@ -429,12 +460,7 @@ module VagrantPlugins p.run = p.run.to_sym if p.run end - # If we didn't share our current directory, then do it - # manually. - if !@__synced_folders["/vagrant"] - synced_folder(".", "/vagrant") - end - + current_dir_shared = false @__synced_folders.each do |id, options| if options[:nfs] options[:type] = :nfs @@ -444,6 +470,14 @@ module VagrantPlugins if options[:type] == :nfs && Vagrant::Util::Platform.windows? options.delete(:type) end + + if options[:hostpath] == '.' + current_dir_shared = true + end + end + + if !current_dir_shared && !@__synced_folders["/vagrant"] + synced_folder(".", "/vagrant") end # Flag that we finalized @@ -673,6 +707,10 @@ module VagrantPlugins errors end + + def __providers + @__provider_order + end end end end diff --git a/plugins/kernel_v2/config/vm_provisioner.rb b/plugins/kernel_v2/config/vm_provisioner.rb index 5aa3945b7..aedafe9fe 100644 --- a/plugins/kernel_v2/config/vm_provisioner.rb +++ b/plugins/kernel_v2/config/vm_provisioner.rb @@ -4,16 +4,16 @@ module VagrantPlugins module Kernel_V2 # Represents a single configured provisioner for a VM. class VagrantConfigProvisioner - # Unique ID name for this provisioner + # Unique name for this provisioner # # @return [String] - attr_reader :id + attr_reader :name - # The name of the provisioner that should be registered + # The type of the provisioner that should be registered # as a plugin. # # @return [Symbol] - attr_reader :name + attr_reader :type # The configuration associated with the provisioner, if there is any. # @@ -31,30 +31,30 @@ module VagrantPlugins # @return [Boolean] attr_accessor :preserve_order - def initialize(id, name) + def initialize(name, type) @logger = Log4r::Logger.new("vagrant::config::vm::provisioner") @logger.debug("Provisioner defined: #{name}") @config = nil - @id = id @invalid = false @name = name @preserve_order = false @run = nil + @type = type # Attempt to find the provisioner... - if !Vagrant.plugin("2").manager.provisioners[name] - @logger.warn("Provisioner '#{name}' not found.") + 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[@name] + provisioner_configs[@type] if !@config_class @logger.info( - "Provisioner config for '#{@name}' not found. Ignoring config.") + "Provisioner config for '#{@type}' not found. Ignoring config.") @config_class = Vagrant::Config::V2::DummyConfig end end diff --git a/plugins/kernel_v2/plugin.rb b/plugins/kernel_v2/plugin.rb index 0904481df..27737854f 100644 --- a/plugins/kernel_v2/plugin.rb +++ b/plugins/kernel_v2/plugin.rb @@ -25,6 +25,11 @@ module VagrantPlugins 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 diff --git a/plugins/providers/docker/action.rb b/plugins/providers/docker/action.rb index 3189d6bec..7ea2bae17 100644 --- a/plugins/providers/docker/action.rb +++ b/plugins/providers/docker/action.rb @@ -45,6 +45,12 @@ module VagrantPlugins 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| @@ -209,7 +215,7 @@ module VagrantPlugins def self.action_start Vagrant::Action::Builder.new.tap do |b| b.use Call, IsState, :running do |env, b2| - # If the container is running and we're doing a run, we're done + # If the container is running and we're not doing a run, we're done next if env[:result] && env[:machine_action] != :run_command if env[:machine_action] != :run_command @@ -225,7 +231,10 @@ module VagrantPlugins end b2.use Call, IsState, :not_created do |env2, b3| - if !env2[:result] + 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 @@ -235,12 +244,13 @@ module VagrantPlugins b2.use PrepareNFSValidIds b2.use SyncedFolderCleanup b2.use PrepareNFSSettings + 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, :not_created do |env2, b3| + b2.use Call, IsState, :preparing do |env2, b3| if env2[:result] b3.use EnvSet, port_collision_repair: true b3.use HostMachinePortWarning @@ -264,6 +274,7 @@ module VagrantPlugins end end else + # We're in a run command, so we do things a bit differently. b2.use SyncedFolders b2.use Create end @@ -287,8 +298,10 @@ module VagrantPlugins 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 :PrepareSSH, action_root.join("prepare_ssh") autoload :Stop, action_root.join("stop") autoload :PrepareNFSValidIds, action_root.join("prepare_nfs_valid_ids") diff --git a/plugins/providers/docker/action/build.rb b/plugins/providers/docker/action/build.rb index 61cbcb8a6..ddc96084c 100644 --- a/plugins/providers/docker/action/build.rb +++ b/plugins/providers/docker/action/build.rb @@ -1,9 +1,13 @@ 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") @@ -37,9 +41,13 @@ module VagrantPlugins machine.ui.output(I18n.t("docker_provider.building")) image = machine.provider.driver.build( build_dir, - extra_args: machine.provider_config.build_args, - ) - machine.ui.detail("Image: #{image}") + extra_args: machine.provider_config.build_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| diff --git a/plugins/providers/docker/action/create.rb b/plugins/providers/docker/action/create.rb index e960f10b0..89034b117 100644 --- a/plugins/providers/docker/action/create.rb +++ b/plugins/providers/docker/action/create.rb @@ -96,10 +96,10 @@ module VagrantPlugins image = @env[:create_image] image ||= @provider_config.image - links = {} + links = [] @provider_config._links.each do |link| parts = link.split(":", 2) - links[parts[0]] = parts[1] + links << parts end { @@ -143,7 +143,9 @@ module VagrantPlugins result += mappings.values.map do |fp| protocol = "" protocol = "/udp" if fp[:protocol].to_s == "udp" - "#{fp[:host]}:#{fp[:guest]}#{protocol}" + host_ip = "" + host_ip = "#{fp[:host_ip]}:" if fp[:host_ip] + "#{host_ip}#{fp[:host]}:#{fp[:guest]}#{protocol}" end.compact result diff --git a/plugins/providers/docker/action/forwarded_ports.rb b/plugins/providers/docker/action/forwarded_ports.rb index db78c33ac..ed51b086d 100644 --- a/plugins/providers/docker/action/forwarded_ports.rb +++ b/plugins/providers/docker/action/forwarded_ports.rb @@ -8,9 +8,19 @@ module VagrantPlugins 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: host.to_i, guest: guest.to_i, + host_ip: host_ip, + protocol: protocol end @app.call(env) diff --git a/plugins/providers/docker/action/host_machine.rb b/plugins/providers/docker/action/host_machine.rb index 2f38df5b9..56f4385bb 100644 --- a/plugins/providers/docker/action/host_machine.rb +++ b/plugins/providers/docker/action/host_machine.rb @@ -44,6 +44,10 @@ module VagrantPlugins 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")) diff --git a/plugins/providers/docker/action/host_machine_build_dir.rb b/plugins/providers/docker/action/host_machine_build_dir.rb index f43032631..4508e8c24 100644 --- a/plugins/providers/docker/action/host_machine_build_dir.rb +++ b/plugins/providers/docker/action/host_machine_build_dir.rb @@ -28,7 +28,7 @@ module VagrantPlugins # 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 = "/mnt/docker_build_#{Digest::MD5.hexdigest(build_dir)}" + new_build_dir = "/var/lib/docker/docker_build_#{Digest::MD5.hexdigest(build_dir)}" options = { docker__ignore: true, docker__exact: true, diff --git a/plugins/providers/docker/action/host_machine_sync_folders.rb b/plugins/providers/docker/action/host_machine_sync_folders.rb index ac5857364..9a6f03893 100644 --- a/plugins/providers/docker/action/host_machine_sync_folders.rb +++ b/plugins/providers/docker/action/host_machine_sync_folders.rb @@ -22,7 +22,7 @@ module VagrantPlugins def call(env) return @app.call(env) if !env[:machine].provider.host_vm? - if !env.has_key?(:host_machine_sync_folders) + if !env.key?(:host_machine_sync_folders) env[:host_machine_sync_folders] = true end @@ -46,7 +46,6 @@ module VagrantPlugins 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") - host_sfid = nil if !id_path.file? host_sfid = SecureRandom.uuid id_path.open("w") do |f| @@ -116,7 +115,7 @@ module VagrantPlugins # Add this synced folder onto the new config if we haven't # already shared it before. - if !existing_ids.has_key?(id) + 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. diff --git a/plugins/providers/docker/action/init_state.rb b/plugins/providers/docker/action/init_state.rb new file mode 100644 index 000000000..cbb444547 --- /dev/null +++ b/plugins/providers/docker/action/init_state.rb @@ -0,0 +1,20 @@ +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 diff --git a/plugins/providers/docker/action/login.rb b/plugins/providers/docker/action/login.rb new file mode 100644 index 000000000..a63c0fdc6 --- /dev/null +++ b/plugins/providers/docker/action/login.rb @@ -0,0 +1,39 @@ +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 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 == "" + + # 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[:ui].output(I18n.t("docker_provider.logging_in")) + driver.login( + config.email, config.username, + config.password, config.auth_server) + + # Continue, within the lock, so that the auth is protected + # from meddling. + @app.call(env) + + # Log out + driver.logout(config.auth_server) + end + end + end + end + end +end diff --git a/plugins/providers/docker/action/prepare_ssh.rb b/plugins/providers/docker/action/prepare_ssh.rb index 1a7298a66..450b139c6 100644 --- a/plugins/providers/docker/action/prepare_ssh.rb +++ b/plugins/providers/docker/action/prepare_ssh.rb @@ -19,14 +19,19 @@ module VagrantPlugins # Modify the SSH options for when we `vagrant ssh`... ssh_opts = env[:ssh_opts] || {} - # Build the command we'll execute within the host machine + # 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 diff --git a/plugins/providers/docker/action/stop.rb b/plugins/providers/docker/action/stop.rb index f743c8b6c..2a30cf4a3 100644 --- a/plugins/providers/docker/action/stop.rb +++ b/plugins/providers/docker/action/stop.rb @@ -11,7 +11,7 @@ module VagrantPlugins driver = machine.provider.driver if driver.running?(machine.id) env[:ui].info I18n.t("docker_provider.messages.stopping") - driver.stop(machine.id) + driver.stop(machine.id, machine.provider_config.stop_timeout) end @app.call(env) end diff --git a/plugins/providers/docker/cap/proxy_machine.rb b/plugins/providers/docker/cap/proxy_machine.rb new file mode 100644 index 000000000..20dd487cc --- /dev/null +++ b/plugins/providers/docker/cap/proxy_machine.rb @@ -0,0 +1,12 @@ +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 diff --git a/plugins/providers/docker/cap/public_address.rb b/plugins/providers/docker/cap/public_address.rb index 379b0452b..987cd668c 100644 --- a/plugins/providers/docker/cap/public_address.rb +++ b/plugins/providers/docker/cap/public_address.rb @@ -5,6 +5,14 @@ module VagrantPlugins 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] diff --git a/plugins/providers/docker/command/logs.rb b/plugins/providers/docker/command/logs.rb index db119284a..a2abab4b2 100644 --- a/plugins/providers/docker/command/logs.rb +++ b/plugins/providers/docker/command/logs.rb @@ -43,7 +43,7 @@ module VagrantPlugins next end - state = machine.state + state = machine.state.id if state == :host_state_unknown machine.ui.output(I18n.t("docker_provider.logs_host_state_unknown")) next diff --git a/plugins/providers/docker/communicator.rb b/plugins/providers/docker/communicator.rb index 657789575..fb6c9fe9e 100644 --- a/plugins/providers/docker/communicator.rb +++ b/plugins/providers/docker/communicator.rb @@ -137,18 +137,21 @@ module VagrantPlugins info[:port] ||= 22 # Make sure our private keys are synced over to the host VM - key_args = sync_private_keys(info).map do |path| + ssh_args = sync_private_keys(info).map do |path| "-i #{path}" - end.join(" ") + 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 #{key_args} " + - "-o Compression=yes " + - "-o ConnectTimeout=5 " + - "-o StrictHostKeyChecking=no " + - "-o UserKnownHostsFile=/dev/null " + - "-p#{info[:port]} " + - "#{info[:username]}@#{info[:host]}" + "ssh #{info[:username]}@#{info[:host]} -p#{info[:port]} #{ssh_args.join(" ")}" end protected diff --git a/plugins/providers/docker/config.rb b/plugins/providers/docker/config.rb index 139c6955e..7b36ab70c 100644 --- a/plugins/providers/docker/config.rb +++ b/plugins/providers/docker/config.rb @@ -61,6 +61,12 @@ module VagrantPlugins # @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" @@ -83,11 +89,42 @@ module VagrantPlugins # @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 @cmd = UNSET_VALUE - @create_args = [] + @create_args = UNSET_VALUE @env = {} @expose = [] @force_host_vm = UNSET_VALUE @@ -96,12 +133,18 @@ module VagrantPlugins @image = UNSET_VALUE @name = UNSET_VALUE @links = [] - @ports = [] + @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) @@ -148,11 +191,18 @@ module VagrantPlugins @has_ssh = false if @has_ssh == UNSET_VALUE @image = nil if @image == UNSET_VALUE @name = nil if @name == 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 diff --git a/plugins/providers/docker/driver.rb b/plugins/providers/docker/driver.rb index 5df5357e0..3ce34dc6e 100644 --- a/plugins/providers/docker/driver.rb +++ b/plugins/providers/docker/driver.rb @@ -14,19 +14,19 @@ module VagrantPlugins @executor = Executor::Local.new end - def build(dir, **opts) + def build(dir, **opts, &block) args = Array(opts[:extra_args]) args << dir - result = execute('docker', 'build', *args) - regexp = /Successfully built (.+)$/i - match = regexp.match(result) - if !match + result = execute('docker', 'build', *args, &block) + matches = result.scan(/Successfully built (.+)$/i) + if matches.empty? # This will cause a stack trace in Vagrant, but it is a bug # if this happens anyways. raise "UNKNOWN OUTPUT: #{result}" end - match[1] + # Return the last match, and the capture of it + matches[-1][0] end def create(params, **opts, &block) @@ -53,7 +53,7 @@ module VagrantPlugins run_cmd += params[:extra_args] if params[:extra_args] run_cmd += [image, cmd] - execute(*run_cmd.flatten, **opts, &block).chomp + execute(*run_cmd.flatten, **opts, &block).chomp.lines.last end def state(cid) @@ -86,6 +86,22 @@ module VagrantPlugins 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 start(cid) if !running?(cid) execute('docker', 'start', cid) @@ -95,9 +111,9 @@ module VagrantPlugins end end - def stop(cid) + def stop(cid, timeout) if running?(cid) - execute('docker', 'stop', '-t', '1', cid) + execute('docker', 'stop', '-t', timeout.to_s, cid) end end diff --git a/plugins/providers/docker/errors.rb b/plugins/providers/docker/errors.rb index 79e5a9767..64a8543ad 100644 --- a/plugins/providers/docker/errors.rb +++ b/plugins/providers/docker/errors.rb @@ -25,6 +25,10 @@ module VagrantPlugins error_key(:docker_provider_nfs_without_privileged) end + class PackageNotSupported < DockerError + error_key(:package_not_supported) + end + class StateNotRunning < DockerError error_key(:state_not_running) end diff --git a/plugins/providers/docker/hostmachine/Vagrantfile b/plugins/providers/docker/hostmachine/Vagrantfile index 752ba888d..79cd59f8c 100644 --- a/plugins/providers/docker/hostmachine/Vagrantfile +++ b/plugins/providers/docker/hostmachine/Vagrantfile @@ -18,4 +18,9 @@ Vagrant.configure("2") do |config| # b2d doesn't support NFS config.nfs.functional = false + + # b2d doesn't persist filesystem between reboots + if config.ssh.respond_to?(:insert_key) + config.ssh.insert_key = false + end end diff --git a/plugins/providers/docker/plugin.rb b/plugins/providers/docker/plugin.rb index 1558aa51d..36ab08687 100644 --- a/plugins/providers/docker/plugin.rb +++ b/plugins/providers/docker/plugin.rb @@ -56,6 +56,11 @@ module VagrantPlugins Cap::PublicAddress end + provider_capability("docker", "proxy_machine") do + require_relative "cap/proxy_machine" + Cap::ProxyMachine + end + protected def self.init! diff --git a/plugins/providers/docker/provider.rb b/plugins/providers/docker/provider.rb index 28d5b9967..79265b9d6 100644 --- a/plugins/providers/docker/provider.rb +++ b/plugins/providers/docker/provider.rb @@ -73,6 +73,10 @@ module VagrantPlugins 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) @@ -152,6 +156,11 @@ module VagrantPlugins 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}") diff --git a/plugins/providers/hyperv/action/import.rb b/plugins/providers/hyperv/action/import.rb index 0687503af..468a55c57 100644 --- a/plugins/providers/hyperv/action/import.rb +++ b/plugins/providers/hyperv/action/import.rb @@ -14,6 +14,15 @@ module VagrantPlugins def call(env) vm_dir = env[:machine].box.directory.join("Virtual Machines") hd_dir = env[:machine].box.directory.join("Virtual Hard Disks") + memory = env[:machine].provider_config.memory + maxmemory = env[:machine].provider_config.maxmemory + cpus = env[:machine].provider_config.cpus + vmname = env[:machine].provider_config.vmname + + env[:ui].output("Configured Dynamical memory allocation, maxmemory is #{maxmemory}") if maxmemory + env[:ui].output("Configured startup memory is #{memory}") if memory + env[:ui].output("Configured cpus number is #{cpus}") if cpus + env[:ui].output("Configured vmname is #{vmname}") if vmname if !vm_dir.directory? || !hd_dir.directory? raise Errors::BoxInvalid @@ -78,6 +87,10 @@ module VagrantPlugins image_path: image_path.to_s.gsub("/", "\\") } options[:switchname] = switch if switch + options[:memory] = memory if memory + options[:maxmemory] = maxmemory if maxmemory + options[:cpus] = cpus if cpus + options[:vmname] = vmname if vmname env[:ui].detail("Creating and registering the VM...") server = env[:machine].provider.driver.import(options) diff --git a/plugins/providers/hyperv/config.rb b/plugins/providers/hyperv/config.rb index ad69e3059..89b51c0a6 100644 --- a/plugins/providers/hyperv/config.rb +++ b/plugins/providers/hyperv/config.rb @@ -8,15 +8,27 @@ module VagrantPlugins # # @return [Integer] attr_accessor :ip_address_timeout + attr_accessor :memory + attr_accessor :maxmemory + attr_accessor :cpus + attr_accessor :vmname def initialize @ip_address_timeout = UNSET_VALUE + @memory = UNSET_VALUE + @maxmemory = UNSET_VALUE + @cpus = UNSET_VALUE + @vmname = UNSET_VALUE end def finalize! 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 end def validate(machine) diff --git a/plugins/providers/hyperv/scripts/import_vm.ps1 b/plugins/providers/hyperv/scripts/import_vm.ps1 index 0b1d74a7e..3e8664eb2 100644 --- a/plugins/providers/hyperv/scripts/import_vm.ps1 +++ b/plugins/providers/hyperv/scripts/import_vm.ps1 @@ -4,7 +4,11 @@ Param( [Parameter(Mandatory=$true)] [string]$image_path, - [string]$switchname=$null + [string]$switchname=$null, + [string]$memory=$null, + [string]$maxmemory=$null, + [string]$cpus=$null, + [string]$vmname=$null ) # Include the following modules @@ -13,10 +17,22 @@ $Dir = Split-Path $script:MyInvocation.MyCommand.Path [xml]$vmconfig = Get-Content -Path $vm_xml_config -$vm_name = $vmconfig.configuration.properties.name.'#text' -$processors = $vmconfig.configuration.settings.processors.count.'#text' $generation = [int]($vmconfig.configuration.properties.subtype.'#text')+1 +if (!$vmname) { + # Get the name of the vm + $vm_name = $vmconfig.configuration.properties.name.'#text' +}else { + $vm_name = $vmname +} + +if (!$cpus) { + # Get the name of the vm + $processors = $vmconfig.configuration.settings.processors.count.'#text' +}else { + $processors = $cpus +} + function GetUniqueName($name) { Get-VM | ForEach-Object -Process { if ($name -eq $_.Name) { @@ -31,18 +47,34 @@ do { $vm_name = GetUniqueName $name } while ($vm_name -ne $name) -$memory = (Select-Xml -xml $vmconfig -XPath "//memory").node.Bank -if ($memory.dynamic_memory_enabled."#text" -eq "True") { - $dynamicmemory = $True +if (!$memory) { + $xmlmemory = (Select-Xml -xml $vmconfig -XPath "//memory").node.Bank + if ($xmlmemory.dynamic_memory_enabled."#text" -eq "True") { + $dynamicmemory = $True + } + else { + $dynamicmemory = $False + } + # Memory values need to be in bytes + $MemoryMaximumBytes = ($xmlmemory.limit."#text" -as [int]) * 1MB + $MemoryStartupBytes = ($xmlmemory.size."#text" -as [int]) * 1MB + $MemoryMinimumBytes = ($xmlmemory.reservation."#text" -as [int]) * 1MB } else { - $dynamicmemory = $False + if (!$maxmemory){ + $dynamicmemory = $False + $MemoryMaximumBytes = ($memory -as [int]) * 1MB + $MemoryStartupBytes = ($memory -as [int]) * 1MB + $MemoryMinimumBytes = ($memory -as [int]) * 1MB + } + else { + $dynamicmemory = $True + $MemoryMaximumBytes = ($maxmemory -as [int]) * 1MB + $MemoryStartupBytes = ($memory -as [int]) * 1MB + $MemoryMinimumBytes = ($memory -as [int]) * 1MB + } } -# Memory values need to be in bytes -$MemoryMaximumBytes = ($memory.limit."#text" -as [int]) * 1MB -$MemoryStartupBytes = ($memory.size."#text" -as [int]) * 1MB -$MemoryMinimumBytes = ($memory.reservation."#text" -as [int]) * 1MB if (!$switchname) { # Get the name of the virtual switch @@ -58,6 +90,9 @@ Switch ((Select-Xml -xml $vmconfig -XPath "//boot").node.device0."#text") { "Default" { $bootdevice = "IDE" } } #switch +# Determine secure boot options +$secure_boot_enabled = (Select-Xml -xml $vmconfig -XPath "//secure_boot_enabled").Node."#text" + # Define a hash map of parameter values for New-VM $vm_params = @{ @@ -101,6 +136,16 @@ $vm | Set-VM @more_vm_params -Passthru # Add drives to the virtual machine $controllers = Select-Xml -xml $vmconfig -xpath "//*[starts-with(name(.),'controller')]" +# Only set EFI secure boot for Gen 2 machines, not gen 1 +if ($generation -ne 1) { + # Set EFI secure boot + if ($secure_boot_enabled -eq "True") { + Set-VMFirmware -VM $vm -EnableSecureBoot On + } else { + Set-VMFirmware -VM $vm -EnableSecureBoot Off + } +} + # A regular expression pattern to pull the number from controllers [regex]$rx="\d" diff --git a/plugins/providers/hyperv/scripts/start_vm.ps1 b/plugins/providers/hyperv/scripts/start_vm.ps1 index 937ce75ed..54025c9a4 100644 --- a/plugins/providers/hyperv/scripts/start_vm.ps1 +++ b/plugins/providers/hyperv/scripts/start_vm.ps1 @@ -10,7 +10,7 @@ forEach ($module in $modules) { . $module } try { $vm = Get-VM -Id $VmId -ErrorAction "stop" - Start-VM $vm + Start-VM $vm -ErrorAction "stop" $state = $vm.state $status = $vm.status $name = $vm.name @@ -24,4 +24,4 @@ try { } catch { Write-Error-Message "Failed to start a VM $_" -} +} \ No newline at end of file diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 668cbe0ef..83eb3aec9 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -292,6 +292,16 @@ module VagrantPlugins 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 diff --git a/plugins/providers/virtualbox/action/network.rb b/plugins/providers/virtualbox/action/network.rb index 0b006360d..f1beff00b 100644 --- a/plugins/providers/virtualbox/action/network.rb +++ b/plugins/providers/virtualbox/action/network.rb @@ -157,8 +157,10 @@ module VagrantPlugins @logger.debug("Bridge was directly specified in config, searching for: #{config[:bridge]}") # Search for a matching bridged interface + bridge = config[:bridge] + bridge = bridge.downcase if bridge.respond_to?(:downcase) bridgedifs.each do |interface| - if interface[:name].downcase == config[:bridge].downcase + if bridge === interface[:name].downcase @logger.debug("Specific bridge found as configured in the Vagrantfile. Using it.") chosen_bridge = interface[:name] break @@ -184,12 +186,15 @@ module VagrantPlugins else # More than one bridgable interface requires a user decision, so # show options to choose from. - @env[:ui].info I18n.t("vagrant.actions.vm.bridged_networking.available", - prefix: false) + @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) @@ -197,7 +202,8 @@ module VagrantPlugins # The choice that the user has chosen as the bridging interface choice = nil while !valid.include?(choice) - choice = @env[:ui].ask("What interface should the network bridge to? ") + choice = @env[:ui].ask( + "Which interface should the network bridge to? ") choice = choice.to_i end @@ -281,16 +287,16 @@ module VagrantPlugins # with the final octet + 2. So "172.28.0.0" turns into "172.28.0.2" dhcp_ip = ip_parts.dup dhcp_ip[3] += 2 - dhcp_options[:dhcp_ip] ||= dhcp_ip.join(".") + dhcp_options[:dhcp_ip] = options[:dhcp_ip] || dhcp_ip.join(".") # Calculate the lower and upper bound for the DHCP server dhcp_lower = ip_parts.dup dhcp_lower[3] += 3 - dhcp_options[:dhcp_lower] ||= dhcp_lower.join(".") + dhcp_options[:dhcp_lower] = options[:dhcp_lower] || dhcp_lower.join(".") dhcp_upper = ip_parts.dup dhcp_upper[3] = 254 - dhcp_options[:dhcp_upper] ||= dhcp_upper.join(".") + dhcp_options[:dhcp_upper] = options[:dhcp_upper] || dhcp_upper.join(".") end return { @@ -323,21 +329,7 @@ module VagrantPlugins end if config[:type] == :dhcp - # Check that if there is a DHCP server attached on our interface, - # then it is identical. Otherwise, we can't set it. - if interface[:dhcp] - valid = interface[:dhcp][:ip] == config[:dhcp_ip] && - interface[:dhcp][:lower] == config[:dhcp_lower] && - interface[:dhcp][:upper] == config[:dhcp_upper] - - raise Vagrant::Errors::NetworkDHCPAlreadyAttached if !valid - - @logger.debug("DHCP server already properly configured") - else - # Configure the DHCP server for the network. - @logger.debug("Creating a DHCP server...") - @env[:machine].provider.driver.create_dhcp_server(interface[:name], config) - end + create_dhcp_server_if_necessary(interface, config) end return { @@ -467,6 +459,70 @@ module VagrantPlugins nil 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 diff --git a/plugins/providers/virtualbox/action/prepare_nfs_settings.rb b/plugins/providers/virtualbox/action/prepare_nfs_settings.rb index b88cee99f..d3edf3e93 100644 --- a/plugins/providers/virtualbox/action/prepare_nfs_settings.rb +++ b/plugins/providers/virtualbox/action/prepare_nfs_settings.rb @@ -1,7 +1,10 @@ +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) @@ -14,19 +17,19 @@ module VagrantPlugins @app.call(env) - if using_nfs? + 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 - # 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 - # 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. diff --git a/plugins/providers/virtualbox/action/set_name.rb b/plugins/providers/virtualbox/action/set_name.rb index 3cbcf131a..0e80f4591 100644 --- a/plugins/providers/virtualbox/action/set_name.rb +++ b/plugins/providers/virtualbox/action/set_name.rb @@ -32,9 +32,9 @@ module VagrantPlugins # Verify the name is not taken vms = env[:machine].provider.driver.read_vms raise Vagrant::Errors::VMNameExists, name: name if \ - vms.has_key?(name) && vms[name] != env[:machine].id + vms.key?(name) && vms[name] != env[:machine].id - if vms.has_key?(name) + if vms.key?(name) @logger.info("Not setting the name because our name is already set.") else env[:ui].info(I18n.t( diff --git a/plugins/providers/virtualbox/config.rb b/plugins/providers/virtualbox/config.rb index aed1c692c..5bb1e754b 100644 --- a/plugins/providers/virtualbox/config.rb +++ b/plugins/providers/virtualbox/config.rb @@ -143,7 +143,7 @@ module VagrantPlugins def validate(machine) errors = _detected_errors - valid_events = ["pre-import", "pre-boot", "post-boot"] + valid_events = ["pre-import", "pre-boot", "post-boot", "post-comm"] @customizations.each do |event, _| if !valid_events.include?(event) errors << I18n.t( diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb index 2cb1a7093..da019eae8 100644 --- a/plugins/providers/virtualbox/driver/base.rb +++ b/plugins/providers/virtualbox/driver/base.rb @@ -30,8 +30,8 @@ module VagrantPlugins # On Windows, we use the VBOX_INSTALL_PATH environmental # variable to find VBoxManage. - if ENV.has_key?("VBOX_INSTALL_PATH") || - ENV.has_key?("VBOX_MSI_INSTALL_PATH") + if ENV.key?("VBOX_INSTALL_PATH") || + ENV.key?("VBOX_MSI_INSTALL_PATH") # Get the path. path = ENV["VBOX_INSTALL_PATH"] || ENV["VBOX_MSI_INSTALL_PATH"] @logger.debug("VBOX_INSTALL_PATH value: #{path}") @@ -180,6 +180,22 @@ module VagrantPlugins 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] @@ -196,7 +212,16 @@ module VagrantPlugins # Returns a list of available host only interfaces. # - # @return [Hash] + # 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" + # } + # + # @return [Array] See comment above for details def read_host_only_interfaces end @@ -238,6 +263,13 @@ module VagrantPlugins def read_vms 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. diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index 1da74d5d3..8fb8867f1 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -2,6 +2,8 @@ require "forwardable" require "log4r" +require "vagrant/util/retryable" + require File.expand_path("../base", __FILE__) module VagrantPlugins @@ -21,6 +23,8 @@ module VagrantPlugins # The version of virtualbox that is running. attr_reader :version + include Vagrant::Util::Retryable + def initialize(uuid=nil) # Setup the base super() @@ -92,6 +96,7 @@ module VagrantPlugins :import, :read_forwarded_ports, :read_bridged_interfaces, + :read_dhcp_servers, :read_guest_additions_version, :read_guest_ip, :read_guest_property, @@ -103,6 +108,7 @@ module VagrantPlugins :read_state, :read_used_ports, :read_vms, + :remove_dhcp_server, :resume, :set_mac_address, :set_name, @@ -130,16 +136,25 @@ module VagrantPlugins # Note: We split this into multiple lines because apparently "".split("_") # is [], so we have to check for an empty array in between. - 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 + 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 parts = output.split("_") diff --git a/plugins/providers/virtualbox/driver/version_4_0.rb b/plugins/providers/virtualbox/driver/version_4_0.rb index 670adfd0a..53361bbbb 100644 --- a/plugins/providers/virtualbox/driver/version_4_0.rb +++ b/plugins/providers/virtualbox/driver/version_4_0.rb @@ -255,6 +255,29 @@ module VagrantPlugins 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) @@ -269,7 +292,12 @@ module VagrantPlugins end def read_guest_ip(adapter_number) - read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") + 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) @@ -282,26 +310,6 @@ module VagrantPlugins end def read_host_only_interfaces - dhcp = {} - execute("list", "dhcpservers", retryable: true).split("\n\n").each do |block| - info = {} - - block.split("\n").each do |line| - if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] - info[:network] = network - elsif ip = line[/^IP:\s+(.+?)$/, 1] - info[:ip] = ip - elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] - info[:lower] = lower - elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] - info[:upper] = upper - end - end - - # Set the DHCP info - dhcp[info[:network]] = info - end - execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} @@ -317,9 +325,6 @@ module VagrantPlugins end end - # Set the DHCP info if it exists - info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] - info end end @@ -424,6 +429,10 @@ module VagrantPlugins 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 @@ -438,7 +447,7 @@ module VagrantPlugins folder[:name], "--hostpath", folder[:hostpath]] - args << "--transient" if folder.has_key?(:transient) && folder[:transient] + args << "--transient" if folder.key?(:transient) && folder[:transient] execute("sharedfolder", "add", @uuid, *args) end end @@ -500,6 +509,16 @@ module VagrantPlugins 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. diff --git a/plugins/providers/virtualbox/driver/version_4_1.rb b/plugins/providers/virtualbox/driver/version_4_1.rb index 60bbdadad..ae1830019 100644 --- a/plugins/providers/virtualbox/driver/version_4_1.rb +++ b/plugins/providers/virtualbox/driver/version_4_1.rb @@ -260,6 +260,29 @@ module VagrantPlugins 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) @@ -274,7 +297,12 @@ module VagrantPlugins end def read_guest_ip(adapter_number) - read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") + 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) @@ -287,26 +315,6 @@ module VagrantPlugins end def read_host_only_interfaces - dhcp = {} - execute("list", "dhcpservers", retryable: true).split("\n\n").each do |block| - info = {} - - block.split("\n").each do |line| - if network = line[/^NetworkName:\s+HostInterfaceNetworking-(.+?)$/, 1] - info[:network] = network - elsif ip = line[/^IP:\s+(.+?)$/, 1] - info[:ip] = ip - elsif lower = line[/^lowerIPAddress:\s+(.+?)$/, 1] - info[:lower] = lower - elsif upper = line[/^upperIPAddress:\s+(.+?)$/, 1] - info[:upper] = upper - end - end - - # Set the DHCP info - dhcp[info[:network]] = info - end - execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} @@ -322,9 +330,6 @@ module VagrantPlugins end end - # Set the DHCP info if it exists - info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] - info end end @@ -429,6 +434,10 @@ module VagrantPlugins 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 @@ -443,13 +452,13 @@ module VagrantPlugins folder[:name], "--hostpath", folder[:hostpath]] - args << "--transient" if folder.has_key?(:transient) && folder[:transient] - - # Add the shared folder - execute("sharedfolder", "add", @uuid, *args) + args << "--transient" if folder.key?(:transient) && folder[:transient] # Enable symlinks on the shared folder execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1") + + # Add the shared folder + execute("sharedfolder", "add", @uuid, *args) end end @@ -510,6 +519,16 @@ module VagrantPlugins 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. diff --git a/plugins/providers/virtualbox/driver/version_4_2.rb b/plugins/providers/virtualbox/driver/version_4_2.rb index cbc397f3e..dc7da430a 100644 --- a/plugins/providers/virtualbox/driver/version_4_2.rb +++ b/plugins/providers/virtualbox/driver/version_4_2.rb @@ -189,9 +189,9 @@ module VagrantPlugins # 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 + 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 + disk_params << path.reverse.sub("/#{suggested_name}/".reverse, "/#{specified_name}/".reverse).reverse # Replace only last occurrence end end @@ -283,6 +283,29 @@ module VagrantPlugins 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) @@ -305,7 +328,12 @@ module VagrantPlugins end def read_guest_ip(adapter_number) - read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") + 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) @@ -318,26 +346,6 @@ module VagrantPlugins end def read_host_only_interfaces - dhcp = {} - execute("list", "dhcpservers", retryable: true).split("\n\n").each do |block| - info = {} - - block.split("\n").each do |line| - if line =~ /^NetworkName:\s+HostInterfaceNetworking-(.+?)$/ - info[:network] = $1.to_s - elsif line =~ /^IP:\s+(.+?)$/ - info[:ip] = $1.to_s - elsif line =~ /^lowerIPAddress:\s+(.+?)$/ - info[:lower] = $1.to_s - elsif line =~ /^upperIPAddress:\s+(.+?)$/ - info[:upper] = $1.to_s - end - end - - # Set the DHCP info - dhcp[info[:network]] = info - end - execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} @@ -353,9 +361,6 @@ module VagrantPlugins end end - # Set the DHCP info if it exists - info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] - info end end @@ -460,6 +465,10 @@ module VagrantPlugins 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 @@ -474,13 +483,13 @@ module VagrantPlugins folder[:name], "--hostpath", folder[:hostpath]] - args << "--transient" if folder.has_key?(:transient) && folder[:transient] - - # Add the shared folder - execute("sharedfolder", "add", @uuid, *args) + args << "--transient" if folder.key?(:transient) && folder[:transient] # Enable symlinks on the shared folder execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1") + + # Add the shared folder + execute("sharedfolder", "add", @uuid, *args) end end @@ -541,6 +550,16 @@ module VagrantPlugins 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. diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index cc700c0fa..2d257a8c1 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -292,6 +292,29 @@ module VagrantPlugins 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) @@ -314,7 +337,13 @@ module VagrantPlugins end def read_guest_ip(adapter_number) - read_guest_property("/VirtualBox/GuestInfo/Net/#{adapter_number}/V4/IP") + 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) @@ -327,26 +356,6 @@ module VagrantPlugins end def read_host_only_interfaces - dhcp = {} - execute("list", "dhcpservers", retryable: true).split("\n\n").each do |block| - info = {} - - block.split("\n").each do |line| - if line =~ /^NetworkName:\s+HostInterfaceNetworking-(.+?)$/ - info[:network] = $1.to_s - elsif line =~ /^IP:\s+(.+?)$/ - info[:ip] = $1.to_s - elsif line =~ /^lowerIPAddress:\s+(.+?)$/ - info[:lower] = $1.to_s - elsif line =~ /^upperIPAddress:\s+(.+?)$/ - info[:upper] = $1.to_s - end - end - - # Set the DHCP info - dhcp[info[:network]] = info - end - execute("list", "hostonlyifs", retryable: true).split("\n\n").collect do |block| info = {} @@ -362,9 +371,6 @@ module VagrantPlugins end end - # Set the DHCP info if it exists - info[:dhcp] = dhcp[info[:name]] if dhcp[info[:name]] - info end end @@ -469,12 +475,23 @@ module VagrantPlugins 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) @@ -483,13 +500,13 @@ module VagrantPlugins folder[:name], "--hostpath", folder[:hostpath]] - args << "--transient" if folder.has_key?(:transient) && folder[:transient] - - # Add the shared folder - execute("sharedfolder", "add", @uuid, *args) + args << "--transient" if folder.key?(:transient) && folder[:transient] # Enable symlinks on the shared folder execute("setextradata", @uuid, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/#{folder[:name]}", "1") + + # Add the shared folder + execute("sharedfolder", "add", @uuid, *args) end end @@ -580,6 +597,18 @@ module VagrantPlugins 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 diff --git a/plugins/providers/virtualbox/model/forwarded_port.rb b/plugins/providers/virtualbox/model/forwarded_port.rb index 2229519f7..03107408e 100644 --- a/plugins/providers/virtualbox/model/forwarded_port.rb +++ b/plugins/providers/virtualbox/model/forwarded_port.rb @@ -51,7 +51,7 @@ module VagrantPlugins options ||= {} @auto_correct = false - @auto_correct = options[:auto_correct] if options.has_key?(:auto_correct) + @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 diff --git a/plugins/provisioners/ansible/provisioner.rb b/plugins/provisioners/ansible/provisioner.rb index 76f07ea65..9e2cb155b 100644 --- a/plugins/provisioners/ansible/provisioner.rb +++ b/plugins/provisioners/ansible/provisioner.rb @@ -1,3 +1,5 @@ +require "vagrant/util/platform" + module VagrantPlugins module Ansible class Provisioner < Vagrant.plugin("2", :provisioner) @@ -12,31 +14,30 @@ module VagrantPlugins @ssh_info = @machine.ssh_info # - # 1) Default Settings (lowest precedence) + # Ansible provisioner options # # Connect with Vagrant SSH identity options = %W[--private-key=#{@ssh_info[:private_key_path][0]} --user=#{@ssh_info[:username]}] - # Multiple SSH keys and/or SSH forwarding can be passed via - # ANSIBLE_SSH_ARGS environment variable, which requires 'ssh' mode. - # Note that multiple keys and ssh-forwarding settings are not supported - # by deprecated 'paramiko' mode. - options << "--connection=ssh" unless ansible_ssh_args.empty? + # Connect with native OpenSSH client + # Other modes (e.g. paramiko) are not officially supported, + # but can be enabled via raw_arguments option. + options << "--connection=ssh" - # By default we limit by the current machine. - # This can be overridden by the limit config option. - options << "--limit=#{@machine.name}" unless config.limit + # Increase the SSH connection timeout, as the Ansible default value (10 seconds) + # is a bit demanding for some overloaded developer boxes. This is particularly + # helpful when additional virtual networks are configured, as their availability + # is not controlled during vagrant boot process. + options << "--timeout=30" - # - # 2) Configuration Joker - # - - options.concat(self.as_array(config.raw_arguments)) if config.raw_arguments - - # - # 3) Append Provisioner options (highest precedence): - # + # By default we limit by the current machine, but + # this can be overridden by the `limit` option. + if config.limit + options << "--limit=#{as_list_argument(config.limit)}" + else + options << "--limit=#{@machine.name}" + end options << "--inventory-file=#{self.setup_inventory_file}" options << "--extra-vars=#{self.get_extra_vars_argument}" if config.extra_vars @@ -48,25 +49,32 @@ module VagrantPlugins options << "--vault-password-file=#{config.vault_password_file}" if config.vault_password_file options << "--tags=#{as_list_argument(config.tags)}" if config.tags options << "--skip-tags=#{as_list_argument(config.skip_tags)}" if config.skip_tags - options << "--limit=#{as_list_argument(config.limit)}" if config.limit options << "--start-at-task=#{config.start_at_task}" if config.start_at_task + # Finally, add the raw configuration options, which has the highest precedence + # and can therefore potentially override any other options of this provisioner. + options.concat(self.as_array(config.raw_arguments)) if config.raw_arguments + + # # Assemble the full ansible-playbook command + # + command = (%w(ansible-playbook) << options << config.playbook).flatten - # Some Ansible options must be passed as environment variables env = { - "ANSIBLE_FORCE_COLOR" => "true", - "ANSIBLE_HOST_KEY_CHECKING" => "#{config.host_key_checking}", - # Ensure Ansible output isn't buffered so that we receive output # on a task-by-task basis. - "PYTHONUNBUFFERED" => 1 + "PYTHONUNBUFFERED" => 1, + + # Some Ansible options must be passed as environment variables, + # as there is no equivalent command line arguments + "ANSIBLE_FORCE_COLOR" => "true", + "ANSIBLE_HOST_KEY_CHECKING" => "#{config.host_key_checking}", } - # Support Multiple SSH keys and SSH forwarding: + # ANSIBLE_SSH_ARGS is required for Multiple SSH keys, SSH forwarding and custom SSH settings env["ANSIBLE_SSH_ARGS"] = ansible_ssh_args unless ansible_ssh_args.empty? - show_ansible_playbook_command(env, command) if config.verbose + show_ansible_playbook_command(env, command) if (config.verbose || @logger.debug?) # Write stdout and stderr data, since it's the regular Ansible output command << { @@ -108,8 +116,9 @@ module VagrantPlugins @machine.env.active_machines.each do |am| begin m = @machine.env.machine(*am) - if !m.ssh_info.nil? - file.write("#{m.name} ansible_ssh_host=#{m.ssh_info[:host]} ansible_ssh_port=#{m.ssh_info[:port]}\n") + m_ssh_info = m.ssh_info + if !m_ssh_info.nil? + file.write("#{m.name} ansible_ssh_host=#{m_ssh_info[:host]} ansible_ssh_port=#{m_ssh_info[:port]}\n") inventory_machines[m.name] = m else @logger.error("Auto-generated inventory: Impossible to get SSH information for machine '#{m.name} (#{m.provider_name})'. This machine should be recreated.") @@ -182,9 +191,38 @@ module VagrantPlugins @ansible_ssh_args ||= get_ansible_ssh_args end + # Use ANSIBLE_SSH_ARGS to pass some OpenSSH options that are not wrapped by + # an ad-hoc Ansible option. Last update corresponds to Ansible 1.8 def get_ansible_ssh_args ssh_options = [] + # Use an SSH ProxyCommand when using the Docker provider with the intermediate host + if @machine.provider_name == :docker && machine.provider.host_vm? + docker_host_ssh_info = machine.provider.host_vm.ssh_info + + proxy_cmd = "ssh #{docker_host_ssh_info[:username]}@#{docker_host_ssh_info[:host]}" + + " -p #{docker_host_ssh_info[:port]} -i #{docker_host_ssh_info[:private_key_path][0]}" + + # Use same options than plugins/providers/docker/communicator.rb + # Note: this could be improved (DRY'ed) by sharing these settings. + proxy_cmd += " -o Compression=yes -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" + + proxy_cmd += " -o ForwardAgent=yes" if @ssh_info[:forward_agent] + + proxy_cmd += " exec nc %h %p 2>/dev/null" + + ssh_options << "-o ProxyCommand='#{ proxy_cmd }'" + end + + # Don't access user's known_hosts file, except when host_key_checking is enabled. + ssh_options << "-o UserKnownHostsFile=/dev/null" unless config.host_key_checking + + # Set IdentitiesOnly=yes to avoid authentication errors when the host has more than 5 ssh keys. + # Notes: + # - Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the IdentitiesOnly option. + # - this could be improved by sharing logic with lib/vagrant/util/ssh.rb + ssh_options << "-o IdentitiesOnly=yes" unless Vagrant::Util::Platform.solaris? + # Multiple Private Keys @ssh_info[:private_key_path].drop(1).each do |key| ssh_options << "-o IdentityFile=#{key}" diff --git a/plugins/provisioners/cfengine/cap/suse/cfengine_install.rb b/plugins/provisioners/cfengine/cap/suse/cfengine_install.rb index 278fe49a8..fd5ef1b30 100644 --- a/plugins/provisioners/cfengine/cap/suse/cfengine_install.rb +++ b/plugins/provisioners/cfengine/cap/suse/cfengine_install.rb @@ -1,12 +1,13 @@ module VagrantPlugins module CFEngine module Cap - module SuSE + module SUSE module CFEngineInstall def self.cfengine_install(machine, config) machine.communicate.tap do |comm| - comm.sudo("GPGFILE=$(mktemp) && wget -O $GPGFILE #{config.repo_gpg_key_url} && rpm --import $GPGFILE; rm -f $GPGFILE") - comm.sudo("zypper addrepo -t YUM #{config.yum_repo_url} cfengine-repository") + 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 diff --git a/plugins/provisioners/cfengine/plugin.rb b/plugins/provisioners/cfengine/plugin.rb index 4e1a7f093..ee5d75768 100644 --- a/plugins/provisioners/cfengine/plugin.rb +++ b/plugins/provisioners/cfengine/plugin.rb @@ -35,7 +35,7 @@ module VagrantPlugins guest_capability("suse", "cfengine_install") do require_relative "cap/suse/cfengine_install" - Cap::SuSE::CFEngineInstall + Cap::SUSE::CFEngineInstall end provisioner(:cfengine) do diff --git a/plugins/provisioners/chef/cap/debian/chef_install.rb b/plugins/provisioners/chef/cap/debian/chef_install.rb new file mode 100644 index 000000000..3d9302d80 --- /dev/null +++ b/plugins/provisioners/chef/cap/debian/chef_install.rb @@ -0,0 +1,19 @@ +require_relative "../../omnibus" + +module VagrantPlugins + module Chef + module Cap + module Debian + module ChefInstall + def self.chef_install(machine, version, prerelease, download_path) + machine.communicate.sudo("apt-get update -y -qq") + machine.communicate.sudo("apt-get install -y -qq curl") + + command = Omnibus.build_command(version, prerelease, download_path) + machine.communicate.sudo(command) + end + end + end + end + end +end diff --git a/plugins/provisioners/chef/cap/linux/chef_installed.rb b/plugins/provisioners/chef/cap/linux/chef_installed.rb new file mode 100644 index 000000000..aa891c541 --- /dev/null +++ b/plugins/provisioners/chef/cap/linux/chef_installed.rb @@ -0,0 +1,22 @@ +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, version) + knife = "/opt/chef/bin/knife" + command = "test -x #{knife}" + + if version != :latest + command << "&& #{knife} --version | grep 'Chef: #{version}'" + end + + machine.communicate.test(command, sudo: true) + end + end + end + end + end +end diff --git a/plugins/provisioners/chef/cap/omnios/chef_install.rb b/plugins/provisioners/chef/cap/omnios/chef_install.rb new file mode 100644 index 000000000..b20332638 --- /dev/null +++ b/plugins/provisioners/chef/cap/omnios/chef_install.rb @@ -0,0 +1,20 @@ +require_relative "../../omnibus" + +module VagrantPlugins + module Chef + module Cap + module OmniOS + module ChefInstall + def self.chef_install(machine, version, prerelease, download_path) + su_cmd = machine.config.solaris.suexec_cmd + + machine.communicate.execute("#{su_cmd} pkg list --no-refresh web/curl > /dev/null 2>&1 || pkg install -q --accept web/curl") + + command = VagrantPlugins::Chef::Omnibus.build_command(version, prerelease, download_path) + machine.communicate.execute(su_cmd + ' ' + command) + end + end + end + end + end +end diff --git a/plugins/provisioners/chef/cap/omnios/chef_installed.rb b/plugins/provisioners/chef/cap/omnios/chef_installed.rb new file mode 100644 index 000000000..2345d3b6a --- /dev/null +++ b/plugins/provisioners/chef/cap/omnios/chef_installed.rb @@ -0,0 +1,23 @@ +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, version) + knife = "/opt/chef/bin/knife" + command = "test -x #{knife}" + + if version != :latest + command << "&& #{knife} --version | grep 'Chef: #{version}'" + end + + machine.communicate.test(command, sudo: true) + end + end + end + end + end +end diff --git a/plugins/provisioners/chef/cap/redhat/chef_install.rb b/plugins/provisioners/chef/cap/redhat/chef_install.rb new file mode 100644 index 000000000..c6aff7bad --- /dev/null +++ b/plugins/provisioners/chef/cap/redhat/chef_install.rb @@ -0,0 +1,18 @@ +require_relative "../../omnibus" + +module VagrantPlugins + module Chef + module Cap + module Redhat + module ChefInstall + def self.chef_install(machine, version, prerelease, download_path) + machine.communicate.sudo("yum install -y -q curl") + + command = Omnibus.build_command(version, prerelease, download_path) + machine.communicate.sudo(command) + end + end + end + end + end +end diff --git a/plugins/provisioners/chef/config/base.rb b/plugins/provisioners/chef/config/base.rb index ae63a4522..370736fd6 100644 --- a/plugins/provisioners/chef/config/base.rb +++ b/plugins/provisioners/chef/config/base.rb @@ -1,139 +1,109 @@ -require "vagrant/util/counter" - module VagrantPlugins module Chef module Config class Base < Vagrant.plugin("2", :config) - extend Vagrant::Util::Counter - - attr_accessor :arguments - attr_accessor :attempts + # 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 - 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 + + # 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 - 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 + + # Install a prerelease version of Chef. + # @return [true, false] + attr_accessor :prerelease + + # 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 + + # 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 - @arguments = UNSET_VALUE - @attempts = UNSET_VALUE - @binary_path = UNSET_VALUE - @binary_env = UNSET_VALUE - @custom_config_path = UNSET_VALUE - @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 - @log_level = 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 - - @json = {} - @run_list = [] - end - - def encrypted_data_bag_secret=(value) - puts "DEPRECATION: Chef encrypted_data_bag_secret has no effect anymore." - puts "Remove this from your Vagrantfile since it'll be removed in the next" - puts "Vagrant version." + @binary_path = UNSET_VALUE + @binary_env = UNSET_VALUE + @install = UNSET_VALUE + @log_level = UNSET_VALUE + @prerelease = UNSET_VALUE + @version = UNSET_VALUE + @installer_download_path = UNSET_VALUE end def finalize! - @arguments = nil if @arguments == UNSET_VALUE - @attempts = 1 if @attempts == UNSET_VALUE - @binary_path = nil if @binary_path == UNSET_VALUE - @binary_env = nil if @binary_env == 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 - @log_level = :info if @log_level == 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 = "/var/chef/backup" if @file_backup_path == UNSET_VALUE - @file_cache_path = "/var/chef/cache" if @file_cache_path == UNSET_VALUE - @verbose_logging = false if @verbose_logging == UNSET_VALUE + @binary_path = nil if @binary_path == UNSET_VALUE + @binary_env = nil if @binary_env == UNSET_VALUE + @install = true if @install == UNSET_VALUE + @log_level = :info if @log_level == UNSET_VALUE + @prerelease = false if @prerelease == UNSET_VALUE + @version = :latest if @version == UNSET_VALUE + @installer_download_path = nil if @installer_download_path == UNSET_VALUE - if @encrypted_data_bag_secret_key_path == UNSET_VALUE - @encrypted_data_bag_secret_key_path = nil + # 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 - - # Set the default provisioning path to be a unique path in /tmp - if !@provisioning_path - counter = self.class.get_and_update_counter(:chef_config) - @provisioning_path = "/tmp/vagrant-chef-#{counter}" - 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. + # Like validate, but returns a list of errors to append. + # + # @return [Array] def validate_base(machine) errors = _detected_errors - 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 + if missing?(log_level) + errors << I18n.t("vagrant.provisioners.chef.log_level_empty") 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 + # Determine if the given string is "missing" (blank) + # @return [true, false] + def missing?(obj) + obj.to_s.strip.empty? end end end diff --git a/plugins/provisioners/chef/config/base_runner.rb b/plugins/provisioners/chef/config/base_runner.rb new file mode 100644 index 000000000..df0329589 --- /dev/null +++ b/plugins/provisioners/chef/config/base_runner.rb @@ -0,0 +1,134 @@ +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 encrypted_data_bag_secret=(value) + puts "DEPRECATION: Chef encrypted_data_bag_secret has no effect anymore." + puts "Remove this from your Vagrantfile since it'll be removed in the next" + puts "Vagrant version." + 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 = "/tmp/vagrant-chef" if @provisioning_path == UNSET_VALUE + @file_backup_path = "/var/chef/backup" if @file_backup_path == UNSET_VALUE + @file_cache_path = "/var/chef/cache" 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 diff --git a/plugins/provisioners/chef/config/chef_apply.rb b/plugins/provisioners/chef/config/chef_apply.rb new file mode 100644 index 000000000..8f7081875 --- /dev/null +++ b/plugins/provisioners/chef/config/chef_apply.rb @@ -0,0 +1,46 @@ +require_relative "base" + +module VagrantPlugins + module Chef + module Config + class ChefApply < Base + # 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 missing?(recipe) + errors << I18n.t("vagrant.provisioners.chef.recipe_empty") + end + + if missing?(upload_path) + errors << I18n.t("vagrant.provisioners.chef.upload_path_empty") + end + + { "chef apply provisioner" => errors } + end + end + end + end +end diff --git a/plugins/provisioners/chef/config/chef_client.rb b/plugins/provisioners/chef/config/chef_client.rb index 9684d2b0e..7d4f4d27a 100644 --- a/plugins/provisioners/chef/config/chef_client.rb +++ b/plugins/provisioners/chef/config/chef_client.rb @@ -1,47 +1,67 @@ -require File.expand_path("../base", __FILE__) - require "vagrant/util/which" +require_relative "base_runner" + module VagrantPlugins module Chef module Config - class ChefClient < Base + class ChefClient < BaseRunner + # 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 + @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 + @chef_server_url = nil if @chef_server_url == UNSET_VALUE @client_key_path = "/etc/chef/client.pem" if @client_key_path == UNSET_VALUE - @delete_client = false if @delete_client == UNSET_VALUE - @delete_node = false if @delete_node == 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 + @validation_key_path = nil if @validation_key_path == UNSET_VALUE end def validate(machine) - errors = _detected_errors - errors.concat(validate_base(machine)) - errors << I18n.t("vagrant.config.chef.server_url_empty") if \ - !chef_server_url || chef_server_url.strip == "" - errors << I18n.t("vagrant.config.chef.validation_key_path") if \ - !validation_key_path + errors = validate_base(machine) + + if chef_server_url.to_s.strip.empty? + errors << I18n.t("vagrant.config.chef.server_url_empty") + end + + if validation_key_path.to_s.strip.empty? + errors << I18n.t("vagrant.config.chef.validation_key_path") + end if delete_client || delete_node if !Vagrant::Util::Which.which("knife") diff --git a/plugins/provisioners/chef/config/chef_solo.rb b/plugins/provisioners/chef/config/chef_solo.rb index 59c3b858e..ee4a2ac2e 100644 --- a/plugins/provisioners/chef/config/chef_solo.rb +++ b/plugins/provisioners/chef/config/chef_solo.rb @@ -1,25 +1,60 @@ -require File.expand_path("../base", __FILE__) +require_relative "base_runner" module VagrantPlugins module Chef module Config - class ChefSolo < Base + class ChefSolo < BaseRunner + # 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 + + # A URL download a remote recipe from. Note: you should use chef-apply + # instead. + # + # @deprecated + # + # @return [String] attr_accessor :recipe_url + + # 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 - @recipe_url = UNSET_VALUE - @roles_path = UNSET_VALUE - @synced_folder_type = UNSET_VALUE + @cookbooks_path = UNSET_VALUE + @data_bags_path = UNSET_VALUE + @environments_path = UNSET_VALUE + @recipe_url = UNSET_VALUE + @roles_path = UNSET_VALUE + @synced_folder_type = UNSET_VALUE + end + + # @deprecated This is deprecated in Chef and will be removed in Chef 12. + def recipe_url=(value) + puts "DEPRECATION: The 'recipe_url' setting for the Chef Solo" + puts "provisioner is deprecated. This value will be removed in" + puts "Chef 12. It is recommended you use the Chef Apply provisioner" + puts "instead. The 'recipe_url' setting will be removed in the next" + puts "version of Vagrant." + + if value + @recipe_url = value + end end def nfs=(value) @@ -63,12 +98,15 @@ module VagrantPlugins end def validate(machine) - errors = _detected_errors - errors.concat(validate_base(machine)) - errors << I18n.t("vagrant.config.chef.cookbooks_path_empty") if \ - !cookbooks_path || [cookbooks_path].flatten.empty? - errors << I18n.t("vagrant.config.chef.environment_path_required") if \ - environment && environments_path.empty? + errors = validate_base(machine) + + if [cookbooks_path].flatten.compact.empty? + errors << I18n.t("vagrant.config.chef.cookbooks_path_empty") + 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 @@ -76,7 +114,8 @@ module VagrantPlugins 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) + path: raw_path.to_s + ) end end @@ -93,6 +132,8 @@ module VagrantPlugins # 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) diff --git a/plugins/provisioners/chef/config/chef_zero.rb b/plugins/provisioners/chef/config/chef_zero.rb new file mode 100644 index 000000000..d28de3dc9 --- /dev/null +++ b/plugins/provisioners/chef/config/chef_zero.rb @@ -0,0 +1,108 @@ +require_relative "chef_solo" + +module VagrantPlugins + module Chef + module Config + class ChefZero < BaseRunner + # 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 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 + @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 + @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) + @roles_path = prepare_folders_config(@roles_path) + @environments_path = prepare_folders_config(@environments_path) + + end + + def validate(machine) + errors = validate_base(machine) + + if [cookbooks_path].flatten.compact.empty? + errors << I18n.t("vagrant.config.chef.cookbooks_path_empty") + 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, path] if !path.is_a?(Array) + path + end + end + end + end + end +end diff --git a/plugins/provisioners/chef/installer.rb b/plugins/provisioners/chef/installer.rb new file mode 100644 index 000000000..16467111c --- /dev/null +++ b/plugins/provisioners/chef/installer.rb @@ -0,0 +1,46 @@ +module VagrantPlugins + module Chef + class Installer + def initialize(machine, options = {}) + @machine = machine + @version = options.fetch(:version, :latest) + @prerelease = options.fetch(:prerelease, :latest) + @force = options.fetch(:force, false) + @download_path = options.fetch(:download_path, nil) + 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, @version, @prerelease, @download_path) + + if !@machine.guest.capability(:chef_installed, @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, @version) + end + end + end +end diff --git a/plugins/provisioners/chef/omnibus.rb b/plugins/provisioners/chef/omnibus.rb new file mode 100644 index 000000000..5c2eef2fb --- /dev/null +++ b/plugins/provisioners/chef/omnibus.rb @@ -0,0 +1,32 @@ +module VagrantPlugins + module Chef + module Omnibus + OMNITRUCK = "https://www.chef.io/chef/install.sh".freeze + + # Read more about the Omnibus installer here: + # https://docs.getchef.com/install_omnibus.html + def build_command(version, prerelease = false, download_path = nil) + command = "curl -sL #{OMNITRUCK} | sudo bash" + + if prerelease || version != :latest || download_path != nil + command << " -s --" + end + + if prerelease + command << " -p" + end + + if version != :latest + command << " -v \"#{version}\"" + end + + if download_path + command << " -d \"#{download_path}\"" + end + + command + end + module_function :build_command + end + end +end diff --git a/plugins/provisioners/chef/plugin.rb b/plugins/provisioners/chef/plugin.rb index afe068047..5a1b2876f 100644 --- a/plugins/provisioners/chef/plugin.rb +++ b/plugins/provisioners/chef/plugin.rb @@ -2,37 +2,82 @@ require "pathname" require "vagrant" +require_relative "command_builder" + module VagrantPlugins module Chef - root = Pathname.new(File.expand_path("../", __FILE__)) - autoload :CommandBuilder, root.join("command_builder") - class Plugin < Vagrant.plugin("2") name "chef" description <<-DESC Provides support for provisioning your virtual machines with - Chef via `chef-solo` or `chef-client`. + Chef via `chef-solo`, `chef-client`, `chef-zero` or `chef-apply`. DESC - config(:chef_solo, :provisioner) do - require File.expand_path("../config/chef_solo", __FILE__) - Config::ChefSolo + config(:chef_apply, :provisioner) do + require_relative "config/chef_apply" + Config::ChefApply end config(:chef_client, :provisioner) do - require File.expand_path("../config/chef_client", __FILE__) + require_relative "config/chef_client" Config::ChefClient end - provisioner(:chef_solo) do - require File.expand_path("../provisioner/chef_solo", __FILE__) - Provisioner::ChefSolo + 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 File.expand_path("../provisioner/chef_client", __FILE__) + 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(:linux, :chef_installed) do + require_relative "cap/linux/chef_installed" + Cap::Linux::ChefInstalled + end + + guest_capability(:debian, :chef_install) do + require_relative "cap/debian/chef_install" + Cap::Debian::ChefInstall + end + + guest_capability(:redhat, :chef_install) do + require_relative "cap/redhat/chef_install" + Cap::Redhat::ChefInstall + end + + guest_capability(:omnios, :chef_installed) do + require_relative "cap/omnios/chef_installed" + Cap::OmniOS::ChefInstalled + end + + guest_capability(:omnios, :chef_install) do + require_relative "cap/omnios/chef_install" + Cap::OmniOS::ChefInstall + end + end end end diff --git a/plugins/provisioners/chef/provisioner/base.rb b/plugins/provisioners/chef/provisioner/base.rb index a891b1251..a5efb5be4 100644 --- a/plugins/provisioners/chef/provisioner/base.rb +++ b/plugins/provisioners/chef/provisioner/base.rb @@ -2,6 +2,8 @@ require 'tempfile' require "vagrant/util/template_renderer" +require_relative "../installer" + module VagrantPlugins module Chef module Provisioner @@ -13,14 +15,34 @@ module VagrantPlugins error_namespace("vagrant.provisioners.chef") end + def initialize(machine, config) + super + + @logger = Log4r::Logger.new("vagrant::provisioners::chef") + end + + def install_chef + return if !config.install + + @logger.info("Checking for Chef installation...") + installer = Installer.new(@machine, + force: config.install == :force, + version: config.version, + prerelease: config.prerelease, + 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. @machine.communicate.sudo( - "which #{binary}", + "sh -c 'command -v #{binary}'", error_class: ChefError, error_key: :chef_not_detected, - binary: binary) + binary: binary, + ) end # This returns the command to run Chef for the given client @@ -71,6 +93,7 @@ module VagrantPlugins 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, diff --git a/plugins/provisioners/chef/provisioner/chef_apply.rb b/plugins/provisioners/chef/provisioner/chef_apply.rb new file mode 100644 index 000000000..1ff4790b6 --- /dev/null +++ b/plugins/provisioners/chef/provisioner/chef_apply.rb @@ -0,0 +1,68 @@ +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 + @machine.communicate.sudo("mkdir -p #{config.upload_path}") + @machine.communicate.sudo("chown -R #{user} #{config.upload_path}") + + # 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 + file = Tempfile.new(["vagrant-chef-apply", ".rb"]) + file.write(config.recipe) + file.rewind + + # Upload the tempfile to the guest + @machine.communicate.upload(file.path, target_recipe_path) + ensure + # Delete our template + file.close + file.unlink + end + end + end + end +end diff --git a/plugins/provisioners/chef/provisioner/chef_client.rb b/plugins/provisioners/chef/provisioner/chef_client.rb index d91b3c22c..c9b0ab6c0 100644 --- a/plugins/provisioners/chef/provisioner/chef_client.rb +++ b/plugins/provisioners/chef/provisioner/chef_client.rb @@ -3,7 +3,7 @@ require 'pathname' require 'vagrant' require 'vagrant/util/subprocess' -require File.expand_path("../base", __FILE__) +require_relative "base" module VagrantPlugins module Chef @@ -18,6 +18,7 @@ module VagrantPlugins end def provision + install_chef verify_binary(chef_binary_path("chef-client")) chown_provisioning_folder create_client_key_folder @@ -109,7 +110,7 @@ module VagrantPlugins # Knife is not part of the current Vagrant bundle, so it needs to run # in the context of the system. Vagrant.global_lock do - Bundler.with_clean_env do + Vagrant::Util::Env.with_clean_env do command = ["knife", deletable, "delete", "--yes", node_name] r = Vagrant::Util::Subprocess.execute(*command) if r.exit_code != 0 diff --git a/plugins/provisioners/chef/provisioner/chef_solo.rb b/plugins/provisioners/chef/provisioner/chef_solo.rb index bf66c99d5..18bdc9e1d 100644 --- a/plugins/provisioners/chef/provisioner/chef_solo.rb +++ b/plugins/provisioners/chef/provisioner/chef_solo.rb @@ -1,8 +1,12 @@ +require "digest/md5" +require "securerandom" +require "set" + require "log4r" require "vagrant/util/counter" -require File.expand_path("../base", __FILE__) +require_relative "base" module VagrantPlugins module Chef @@ -11,6 +15,8 @@ module VagrantPlugins 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 :role_folders @@ -19,6 +25,7 @@ module VagrantPlugins def initialize(machine, config) super @logger = Log4r::Logger.new("vagrant::provisioners::chef_solo") + @shared_folders = [] end def configure(root_config) @@ -27,22 +34,22 @@ module VagrantPlugins @data_bags_folders = expanded_folders(@config.data_bags_path, "data_bags") @environments_folders = expanded_folders(@config.environments_path, "environments") - share_folders(root_config, "csc", @cookbook_folders) - share_folders(root_config, "csr", @role_folders) - share_folders(root_config, "csdb", @data_bags_folders) - share_folders(root_config, "cse", @environments_folders) + 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) end - def provision + def provision(mode = :solo) + install_chef # Verify that the proper shared folders exist. check = [] - [@cookbook_folders, @role_folders, @data_bags_folders, @environments_folders].each do |folders| - 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 + @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 @@ -51,7 +58,7 @@ module VagrantPlugins upload_encrypted_data_bag_secret setup_json setup_solo_config - run_chef_solo + run_chef(mode) delete_encrypted_data_bag_secret end @@ -72,8 +79,10 @@ module VagrantPlugins local_path = File.expand_path(path, @machine.env.root_path) if File.exist?(local_path) - # Path exists on the host, setup the remote path - remote_path = "#{@config.provisioning_path}/chef-solo-#{get_and_update_counter(:cookbooks_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 = "#{@config.provisioning_path}/#{key}" else @machine.ui.warn(I18n.t("vagrant.provisioners.chef.cookbook_folder_not_found_warning", path: local_path.to_s)) @@ -103,33 +112,49 @@ module VagrantPlugins # 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) - folders.each do |type, local_path, remote_path| - if type == :host - opts = {} - opts[:id] = "v-#{prefix}-#{self.class.get_and_update_counter(:shared_folder)}" - opts[:type] = @config.synced_folder_type if @config.synced_folder_type - - root_config.vm.synced_folder(local_path, remote_path, opts) + 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. + if existing_set.include?(remote_path) + @logger.debug("Not sharing #{local_path}, exists as #{remote_path}") + next + end + + opts = {} + opts[:id] = "v-#{prefix}-#{self.class.get_and_update_counter(:shared_folder)}" + 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 - cookbooks_path = guest_paths(@cookbook_folders) - roles_path = guest_paths(@role_folders) - data_bags_path = guest_paths(@data_bags_folders).first - environments_path = guest_paths(@environments_folders).first - setup_config("provisioners/chef_solo/solo", "solo.rb", { - cookbooks_path: cookbooks_path, - recipe_url: @config.recipe_url, - roles_path: roles_path, - data_bags_path: data_bags_path, - environments_path: environments_path, - }) + setup_config("provisioners/chef_solo/solo", "solo.rb", solo_config) end - def run_chef_solo + def solo_config + { + cookbooks_path: guest_paths(@cookbook_folders), + recipe_url: @config.recipe_url, + roles_path: guest_paths(@role_folders), + data_bags_path: guest_paths(@data_bags_folders).first, + environments_path: guest_paths(@environments_folders).first + } + end + + def run_chef(mode) if @config.run_list && @config.run_list.empty? @machine.ui.warn(I18n.t("vagrant.chef_run_list_empty")) end @@ -142,9 +167,9 @@ module VagrantPlugins @config.attempts.times do |attempt| if attempt == 0 - @machine.ui.info I18n.t("vagrant.provisioners.chef.running_solo") + @machine.ui.info I18n.t("vagrant.provisioners.chef.running_#{mode}") else - @machine.ui.info I18n.t("vagrant.provisioners.chef.running_solo_again") + @machine.ui.info I18n.t("vagrant.provisioners.chef.running_#{mode}_again") end opts = { error_check: false, elevated: true } diff --git a/plugins/provisioners/chef/provisioner/chef_zero.rb b/plugins/provisioners/chef/provisioner/chef_zero.rb new file mode 100644 index 000000000..1a63be73f --- /dev/null +++ b/plugins/provisioners/chef/provisioner/chef_zero.rb @@ -0,0 +1,209 @@ +require "digest/md5" +require "securerandom" +require "set" + +require "log4r" + +require "vagrant/util/counter" + +require_relative "base" + +module VagrantPlugins + module Chef + module Provisioner + # This class implements provisioning via chef-zero. + class ChefZero < Base + extend Vagrant::Util::Counter + include Vagrant::Util::Counter + include Vagrant::Action::Builtin::MixinSyncedFolders + + attr_reader :environments_folders + attr_reader :cookbook_folders + attr_reader :role_folders + attr_reader :data_bags_folders + + def initialize(machine, config) + super + @logger = Log4r::Logger.new("vagrant::provisioners::chef_zero") + @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") + + 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) + end + + def provision(mode = :client) + 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(mode) + 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 = "#{@config.provisioning_path}/#{key}" + else + @machine.ui.warn(I18n.t("vagrant.provisioners.chef.cookbook_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. + remote_path = File.expand_path(path, @config.provisioning_path) + + # 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 = remote_path.gsub(/^[a-zA-Z]:/, "") + end + + # If we have specified a folder name to append then append it + remote_path += "/#{appended_folder}" if appended_folder + + # 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. + if existing_set.include?(remote_path) + @logger.debug("Not sharing #{local_path}, exists as #{remote_path}") + next + end + + opts = {} + opts[:id] = "v-#{prefix}-#{self.class.get_and_update_counter(:shared_folder)}" + 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_zero_config + setup_config("provisioners/chef_zero/zero", "client.rb", { + local_mode: true, + enable_reporting: false, + cookbooks_path: guest_paths(@cookbook_folders), + roles_path: guest_paths(@role_folders), + data_bags_path: guest_paths(@data_bags_folders).first, + environments_path: guest_paths(@environments_folders).first, + }) + end + + def run_chef(mode) + if @config.run_list && @config.run_list.empty? + @machine.ui.warn(I18n.t("vagrant.chef_run_list_empty")) + end + + if @machine.guest.capability?(:wait_for_reboot) + @machine.guest.capability(:wait_for_reboot) + end + + command = build_command(:client) + + @config.attempts.times do |attempt| + if attempt == 0 + @machine.ui.info I18n.t("vagrant.provisioners.chef.running_#{mode}") + else + @machine.ui.info I18n.t("vagrant.provisioners.chef.running_#{mode}_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 + + # 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 diff --git a/plugins/provisioners/docker/cap/debian/docker_install.rb b/plugins/provisioners/docker/cap/debian/docker_install.rb index 0d376a243..4e4a2ae6e 100644 --- a/plugins/provisioners/docker/cap/debian/docker_install.rb +++ b/plugins/provisioners/docker/cap/debian/docker_install.rb @@ -14,8 +14,8 @@ module VagrantPlugins end comm.sudo("apt-get update -y") comm.sudo("apt-get install -y --force-yes -q curl") - comm.sudo("curl http://get.docker.io/gpg | apt-key add -") - comm.sudo("echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list") + comm.sudo("curl -sSL https://get.docker.com/gpg | apt-key add -") + comm.sudo("echo deb http://get.docker.com/ubuntu docker main > /etc/apt/sources.list.d/docker.list") comm.sudo("apt-get update") comm.sudo("echo lxc lxc/directory string /var/lib/lxc | debconf-set-selections") comm.sudo("apt-get install -y --force-yes -q xz-utils #{package} -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'") diff --git a/plugins/provisioners/docker/cap/linux/docker_configure_vagrant_user.rb b/plugins/provisioners/docker/cap/linux/docker_configure_vagrant_user.rb index 7a3d87e94..5283a9ef1 100644 --- a/plugins/provisioners/docker/cap/linux/docker_configure_vagrant_user.rb +++ b/plugins/provisioners/docker/cap/linux/docker_configure_vagrant_user.rb @@ -7,7 +7,7 @@ module VagrantPlugins ssh_info = machine.ssh_info machine.communicate.tap do |comm| - if !comm.test("id -Gn | grep docker") + if comm.test("getent group docker") && !comm.test("id -Gn | grep docker") comm.sudo("usermod -a -G docker #{ssh_info[:username]}") end end diff --git a/plugins/provisioners/docker/cap/linux/docker_installed.rb b/plugins/provisioners/docker/cap/linux/docker_installed.rb index f1d05fa2f..11acdac5a 100644 --- a/plugins/provisioners/docker/cap/linux/docker_installed.rb +++ b/plugins/provisioners/docker/cap/linux/docker_installed.rb @@ -4,7 +4,19 @@ module VagrantPlugins module Linux module DockerInstalled def self.docker_installed(machine) - machine.communicate.test("test -f /usr/bin/docker", sudo: true) + paths = [ + "/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 diff --git a/plugins/provisioners/docker/config.rb b/plugins/provisioners/docker/config.rb index c96e97305..7e6c37787 100644 --- a/plugins/provisioners/docker/config.rb +++ b/plugins/provisioners/docker/config.rb @@ -70,8 +70,8 @@ module VagrantPlugins @__containers.each do |name, params| params[:image] ||= name - params[:auto_assign_name] = true if !params.has_key?(:auto_assign_name) - params[:daemonize] = true if !params.has_key?(:daemonize) + params[:auto_assign_name] = true if !params.key?(:auto_assign_name) + params[:daemonize] = true if !params.key?(:daemonize) end end end diff --git a/plugins/provisioners/puppet/config/puppet.rb b/plugins/provisioners/puppet/config/puppet.rb index a6140b037..eccd5f780 100644 --- a/plugins/provisioners/puppet/config/puppet.rb +++ b/plugins/provisioners/puppet/config/puppet.rb @@ -1,11 +1,7 @@ -require "vagrant/util/counter" - module VagrantPlugins module Puppet module Config class Puppet < Vagrant.plugin("2", :config) - extend Vagrant::Util::Counter - attr_accessor :facter attr_accessor :hiera_config_path attr_accessor :manifest_file @@ -90,15 +86,8 @@ module VagrantPlugins @module_path = nil if @module_path == UNSET_VALUE @synced_folder_type = nil if @synced_folder_type == UNSET_VALUE - @temp_dir = nil if @temp_dir == UNSET_VALUE + @temp_dir = "/tmp/vagrant-puppet" if @temp_dir == UNSET_VALUE @working_directory = nil if @working_directory == UNSET_VALUE - - # Set a default temp dir that has an increasing counter so - # that multiple Puppet definitions won't overwrite each other - if !@temp_dir - counter = self.class.get_and_update_counter(:puppet_config) - @temp_dir = "/tmp/vagrant-puppet-#{counter}" - end end # Returns the module paths as an array of paths expanded relative to the diff --git a/plugins/provisioners/puppet/provisioner/puppet.rb b/plugins/provisioners/puppet/provisioner/puppet.rb index 14b64087e..66461af5e 100644 --- a/plugins/provisioners/puppet/provisioner/puppet.rb +++ b/plugins/provisioners/puppet/provisioner/puppet.rb @@ -1,3 +1,5 @@ +require "digest/md5" + require "log4r" module VagrantPlugins @@ -21,8 +23,9 @@ module VagrantPlugins # Setup the module paths @module_paths = [] - @expanded_module_paths.each_with_index do |path, i| - @module_paths << [path, File.join(config.temp_dir, "modules-#{i}")] + @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 = {} @@ -117,7 +120,8 @@ module VagrantPlugins def manifests_guest_path if config.manifests_path[0] == :host # The path is on the host, so point to where it is shared - File.join(config.temp_dir, "manifests") + 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] @@ -217,6 +221,7 @@ module VagrantPlugins opts = { elevated: true, + error_class: Vagrant::Errors::VagrantError, error_key: :ssh_bad_exit_status_muted, good_exit: [0,2], } diff --git a/plugins/provisioners/salt/config.rb b/plugins/provisioners/salt/config.rb index 0a99d3fc8..4d97d942c 100644 --- a/plugins/provisioners/salt/config.rb +++ b/plugins/provisioners/salt/config.rb @@ -12,6 +12,7 @@ module VagrantPlugins attr_accessor :master_config attr_accessor :master_key attr_accessor :master_pub + attr_accessor :grains_config attr_accessor :run_highstate attr_accessor :run_overstate attr_accessor :always_install @@ -38,6 +39,7 @@ module VagrantPlugins @master_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 @@ -63,6 +65,7 @@ module VagrantPlugins @master_config = nil if @master_config == UNSET_VALUE @master_key = nil if @master_key == UNSET_VALUE @master_pub = nil if @master_pub == UNSET_VALUE + @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 @@ -115,6 +118,13 @@ module VagrantPlugins 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 diff --git a/plugins/provisioners/salt/provisioner.rb b/plugins/provisioners/salt/provisioner.rb index 6225fd339..87af7cc7d 100644 --- a/plugins/provisioners/salt/provisioner.rb +++ b/plugins/provisioners/salt/provisioner.rb @@ -75,7 +75,7 @@ module VagrantPlugins end def need_configure - @config.minion_config or @config.minion_key or @config.master_config or @config.master_key + @config.minion_config or @config.minion_key or @config.master_config or @config.master_key or @config.grains_config end def need_install @@ -181,6 +181,11 @@ module VagrantPlugins @machine.env.ui.info "Copying salt master config to vm." @machine.communicate.upload(expanded_path(@config.master_config).to_s, temp_config_dir + "/master") end + + if @config.grains_config + @machine.env.ui.info "Copying salt grains config to vm." + @machine.communicate.upload(expanded_path(@config.grains_config).to_s, temp_config_dir + "/grains") + end end # Copy master and minion keys to VM @@ -236,7 +241,9 @@ module VagrantPlugins bootstrap_destination = File.join(config_dir, "bootstrap_salt.sh") end - @machine.communicate.sudo("rm -f %s" % bootstrap_destination) + if @machine.communicate.test("test -f %s" % bootstrap_destination) + @machine.communicate.sudo("rm -f %s" % bootstrap_destination) + end @machine.communicate.upload(bootstrap_path.to_s, bootstrap_destination) @machine.communicate.sudo("chmod +x %s" % bootstrap_destination) if @machine.config.vm.communicator == :winrm @@ -302,9 +309,9 @@ module VagrantPlugins @machine.env.ui.info "Calling state.highstate... (this may take a while)" if @config.install_master @machine.communicate.sudo("salt '*' saltutil.sync_all") - @machine.communicate.sudo("salt '*' state.highstate --retcode-passthrough --verbose#{get_loglevel}#{get_colorize}#{get_pillar}") do |type, data| + @machine.communicate.sudo("salt '*' state.highstate --verbose#{get_loglevel}#{get_colorize}#{get_pillar}") do |type, data| if @config.verbose - @machine.env.ui.info(data) + @machine.env.ui.info(data.rstrip) end end else @@ -313,14 +320,14 @@ module VagrantPlugins @machine.communicate.execute("C:\\salt\\salt-call.exe saltutil.sync_all", opts) @machine.communicate.execute("C:\\salt\\salt-call.exe state.highstate --retcode-passthrough #{get_loglevel}#{get_colorize}#{get_pillar}", opts) do |type, data| if @config.verbose - @machine.env.ui.info(data) + @machine.env.ui.info(data.rstrip) end end else @machine.communicate.sudo("salt-call saltutil.sync_all") @machine.communicate.sudo("salt-call state.highstate --retcode-passthrough #{get_loglevel}#{get_colorize}#{get_pillar}") do |type, data| if @config.verbose - @machine.env.ui.info(data) + @machine.env.ui.info(data.rstrip) end end end diff --git a/plugins/provisioners/shell/config.rb b/plugins/provisioners/shell/config.rb index fbb7c812e..e1d046f2d 100644 --- a/plugins/provisioners/shell/config.rb +++ b/plugins/provisioners/shell/config.rb @@ -10,6 +10,7 @@ module VagrantPlugins attr_accessor :privileged attr_accessor :binary attr_accessor :keep_color + attr_accessor :powershell_args def initialize @args = UNSET_VALUE @@ -19,6 +20,7 @@ module VagrantPlugins @privileged = UNSET_VALUE @binary = UNSET_VALUE @keep_color = UNSET_VALUE + @powershell_args = UNSET_VALUE end def finalize! @@ -29,6 +31,7 @@ module VagrantPlugins @privileged = true if @privileged == UNSET_VALUE @binary = false if @binary == UNSET_VALUE @keep_color = false if @keep_color == UNSET_VALUE + @powershell_args = "-ExecutionPolicy Bypass" if @powershell_args == UNSET_VALUE if @args && args_valid? @args = @args.is_a?(Array) ? @args.map { |a| a.to_s } : @args.to_s diff --git a/plugins/provisioners/shell/provisioner.rb b/plugins/provisioners/shell/provisioner.rb index 674c9c5fb..867de7553 100644 --- a/plugins/provisioners/shell/provisioner.rb +++ b/plugins/provisioners/shell/provisioner.rb @@ -2,10 +2,13 @@ require "pathname" require "tempfile" require "vagrant/util/downloader" +require "vagrant/util/retryable" module VagrantPlugins module Shell class Provisioner < Vagrant.plugin("2", :provisioner) + include Vagrant::Util::Retryable + def provision args = "" if config.args.is_a?(String) @@ -50,7 +53,13 @@ module VagrantPlugins # Upload the script to the machine @machine.communicate.tap do |comm| # Reset upload path permissions for the current ssh user - user = @machine.ssh_info[:username] + info = nil + retryable(on: Vagrant::Errors::SSHNotReady, tries: 3, sleep: 2) do + info = @machine.ssh_info + raise Vagrant::Errors::SSHNotReady if info.nil? + end + + user = info[:username] comm.sudo("chown -R #{user} #{config.upload_path}", error_check: false) @@ -100,14 +109,22 @@ module VagrantPlugins exec_path.gsub!('/', '\\') exec_path = "c:#{exec_path}" if exec_path.start_with?("\\") - # For PowerShell scripts bypass the execution policy + # 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}" - command = "powershell -executionpolicy bypass -file #{command}" if + command = "powershell #{shell_args.to_s} -file #{command}" if File.extname(exec_path).downcase == '.ps1' if config.path - @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", - script: exec_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")) diff --git a/plugins/pushes/atlas/config.rb b/plugins/pushes/atlas/config.rb new file mode 100644 index 000000000..55543fae6 --- /dev/null +++ b/plugins/pushes/atlas/config.rb @@ -0,0 +1,147 @@ +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 + @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 diff --git a/plugins/pushes/atlas/errors.rb b/plugins/pushes/atlas/errors.rb new file mode 100644 index 000000000..7ba1d712a --- /dev/null +++ b/plugins/pushes/atlas/errors.rb @@ -0,0 +1,13 @@ +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 diff --git a/plugins/pushes/atlas/locales/en.yml b/plugins/pushes/atlas/locales/en.yml new file mode 100644 index 000000000..3377f62dd --- /dev/null +++ b/plugins/pushes/atlas/locales/en.yml @@ -0,0 +1,22 @@ +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. diff --git a/plugins/pushes/atlas/plugin.rb b/plugins/pushes/atlas/plugin.rb new file mode 100644 index 000000000..2eee2517d --- /dev/null +++ b/plugins/pushes/atlas/plugin.rb @@ -0,0 +1,35 @@ +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 diff --git a/plugins/pushes/atlas/push.rb b/plugins/pushes/atlas/push.rb new file mode 100644 index 000000000..73ec117b9 --- /dev/null +++ b/plugins/pushes/atlas/push.rb @@ -0,0 +1,78 @@ +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 + end + end +end diff --git a/plugins/pushes/ftp/adapter.rb b/plugins/pushes/ftp/adapter.rb new file mode 100644 index 000000000..d0d076491 --- /dev/null +++ b/plugins/pushes/ftp/adapter.rb @@ -0,0 +1,127 @@ +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 + 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) + @server.upload!(local, remote, mkdir: true) + end + end + end +end diff --git a/plugins/pushes/ftp/config.rb b/plugins/pushes/ftp/config.rb new file mode 100644 index 000000000..e493259db --- /dev/null +++ b/plugins/pushes/ftp/config.rb @@ -0,0 +1,130 @@ +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 diff --git a/plugins/pushes/ftp/locales/en.yml b/plugins/pushes/ftp/locales/en.yml new file mode 100644 index 000000000..0bbbe51f1 --- /dev/null +++ b/plugins/pushes/ftp/locales/en.yml @@ -0,0 +1,11 @@ +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 diff --git a/plugins/pushes/ftp/plugin.rb b/plugins/pushes/ftp/plugin.rb new file mode 100644 index 000000000..ef333807b --- /dev/null +++ b/plugins/pushes/ftp/plugin.rb @@ -0,0 +1,33 @@ +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 diff --git a/plugins/pushes/ftp/push.rb b/plugins/pushes/ftp/push.rb new file mode 100644 index 000000000..7a69c4f02 --- /dev/null +++ b/plugins/pushes/ftp/push.rb @@ -0,0 +1,120 @@ +require "net/ftp" +require "pathname" + +require_relative "adapter" + +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 = Hash[*all_files.flat_map do |file| + relative_path = relative_path_for(file, config.dir) + destination = File.expand_path(File.join(config.destination, relative_path)) + file = File.expand_path(file, env.root_path) + [file, destination] + end] + + ftp = "#{config.username}@#{config.host}:#{config.destination}" + env.ui.info "Uploading #{env.root_path} to #{ftp}" + + connect do |ftp| + files.each do |local, remote| + @logger.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("#{config.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, config.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 + end + end +end diff --git a/plugins/pushes/heroku/config.rb b/plugins/pushes/heroku/config.rb new file mode 100644 index 000000000..927c1a7b9 --- /dev/null +++ b/plugins/pushes/heroku/config.rb @@ -0,0 +1,74 @@ +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 diff --git a/plugins/pushes/heroku/errors.rb b/plugins/pushes/heroku/errors.rb new file mode 100644 index 000000000..c92a7b76c --- /dev/null +++ b/plugins/pushes/heroku/errors.rb @@ -0,0 +1,21 @@ +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 diff --git a/plugins/pushes/heroku/locales/en.yml b/plugins/pushes/heroku/locales/en.yml new file mode 100644 index 000000000..e6f6bf441 --- /dev/null +++ b/plugins/pushes/heroku/locales/en.yml @@ -0,0 +1,30 @@ +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: + + http://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. diff --git a/plugins/pushes/heroku/plugin.rb b/plugins/pushes/heroku/plugin.rb new file mode 100644 index 000000000..40865ee5d --- /dev/null +++ b/plugins/pushes/heroku/plugin.rb @@ -0,0 +1,33 @@ +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 diff --git a/plugins/pushes/heroku/push.rb b/plugins/pushes/heroku/push.rb new file mode 100644 index 000000000..b894eba81 --- /dev/null +++ b/plugins/pushes/heroku/push.rb @@ -0,0 +1,136 @@ +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, + "branch", + ) + + # 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) + result = Vagrant::Util::Subprocess.execute(*cmd) + + if result.exit_code != 0 + raise Errors::CommandFailed, + cmd: cmd.join(" "), + stdout: result.stdout, + stderr: result.stderr + end + + result + end + end + end +end diff --git a/plugins/pushes/local-exec/config.rb b/plugins/pushes/local-exec/config.rb new file mode 100644 index 000000000..747ff8925 --- /dev/null +++ b/plugins/pushes/local-exec/config.rb @@ -0,0 +1,48 @@ +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 + + def initialize + @script = UNSET_VALUE + @inline = UNSET_VALUE + end + + def finalize! + @script = nil if @script == UNSET_VALUE + @inline = nil if @inline == UNSET_VALUE + 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 + + { "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 + end + end +end diff --git a/plugins/pushes/local-exec/errors.rb b/plugins/pushes/local-exec/errors.rb new file mode 100644 index 000000000..5e5b71cca --- /dev/null +++ b/plugins/pushes/local-exec/errors.rb @@ -0,0 +1,13 @@ +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 diff --git a/plugins/pushes/local-exec/locales/en.yml b/plugins/pushes/local-exec/locales/en.yml new file mode 100644 index 000000000..08808a879 --- /dev/null +++ b/plugins/pushes/local-exec/locales/en.yml @@ -0,0 +1,22 @@ +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 diff --git a/plugins/pushes/local-exec/plugin.rb b/plugins/pushes/local-exec/plugin.rb new file mode 100644 index 000000000..e58fe4bed --- /dev/null +++ b/plugins/pushes/local-exec/plugin.rb @@ -0,0 +1,33 @@ +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 diff --git a/plugins/pushes/local-exec/push.rb b/plugins/pushes/local-exec/push.rb new file mode 100644 index 000000000..bad308f3b --- /dev/null +++ b/plugins/pushes/local-exec/push.rb @@ -0,0 +1,45 @@ +require "fileutils" +require "tempfile" +require "vagrant/util/safe_exec" + +require_relative "errors" + +module VagrantPlugins + module LocalExecPush + class Push < Vagrant.plugin("2", :push) + def push + if config.inline + execute_inline!(config.inline) + else + execute_script!(config.script) + end + end + + # Execute the inline script by writing it to a tempfile and executing. + def execute_inline!(inline) + script = Tempfile.new(["vagrant-local-exec-script", ".sh"]) + script.write(inline) + script.rewind + + execute_script!(script.path) + 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) + path = File.expand_path(path, env.root_path) + FileUtils.chmod("+x", path) + execute!(path) + end + + # Execute the script, raising an exception if it fails. + def execute!(*cmd) + Vagrant::Util::SafeExec.exec(cmd[0], *cmd[1..-1]) + end + end + end +end diff --git a/plugins/pushes/noop/config.rb b/plugins/pushes/noop/config.rb new file mode 100644 index 000000000..9b23fb450 --- /dev/null +++ b/plugins/pushes/noop/config.rb @@ -0,0 +1,16 @@ +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 diff --git a/plugins/pushes/noop/plugin.rb b/plugins/pushes/noop/plugin.rb new file mode 100644 index 000000000..d3dc6994d --- /dev/null +++ b/plugins/pushes/noop/plugin.rb @@ -0,0 +1,22 @@ +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 diff --git a/plugins/pushes/noop/push.rb b/plugins/pushes/noop/push.rb new file mode 100644 index 000000000..537ea1c7d --- /dev/null +++ b/plugins/pushes/noop/push.rb @@ -0,0 +1,9 @@ +module VagrantPlugins + module NoopDeploy + class Push < Vagrant.plugin("2", :push) + def push + puts "pushed" + end + end + end +end diff --git a/plugins/synced_folders/nfs/synced_folder.rb b/plugins/synced_folders/nfs/synced_folder.rb index f84c43b33..7cf51aabe 100644 --- a/plugins/synced_folders/nfs/synced_folder.rb +++ b/plugins/synced_folders/nfs/synced_folder.rb @@ -64,25 +64,28 @@ module VagrantPlugins export_folders = folders.dup export_folders.keys.each do |id| opts = export_folders[id] - if opts.has_key?(:nfs_export) && !opts[:nfs_export] + if opts.key?(:nfs_export) && !opts[:nfs_export] export_folders.delete(id) end end - # 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) + # 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 - rescue Vagrant::Errors::EnvironmentLockedError - sleep 1 - retry end end @@ -114,7 +117,7 @@ module VagrantPlugins def prepare_folder(machine, opts) opts[:map_uid] = prepare_permission(machine, :uid, opts) opts[:map_gid] = prepare_permission(machine, :gid, opts) - opts[:nfs_udp] = true if !opts.has_key?(:nfs_udp) + opts[:nfs_udp] = true if !opts.key?(:nfs_udp) opts[:nfs_version] ||= 3 # We use a CRC32 to generate a 32-bit checksum so that the @@ -125,11 +128,11 @@ module VagrantPlugins # Prepares the UID/GID settings for a single folder. def prepare_permission(machine, perm, opts) key = "map_#{perm}".to_sym - return nil if opts.has_key?(key) && opts[key].nil? + return nil if opts.key?(key) && opts[key].nil? # The options on the hash get priority, then the default # values - value = opts.has_key?(key) ? opts[key] : machine.config.nfs.send(key) + 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 diff --git a/plugins/synced_folders/rsync/command/rsync.rb b/plugins/synced_folders/rsync/command/rsync.rb index 6703ffaaa..e2ab5988f 100644 --- a/plugins/synced_folders/rsync/command/rsync.rb +++ b/plugins/synced_folders/rsync/command/rsync.rb @@ -27,6 +27,18 @@ module VagrantPlugins # 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 @@ -34,7 +46,7 @@ module VagrantPlugins end # Determine the rsync synced folders for this machine - folders = synced_folders(machine)[:rsync] + folders = synced_folders(machine, cached: true)[:rsync] next if !folders || folders.empty? # Get the SSH info for this machine so we can access it diff --git a/plugins/synced_folders/rsync/command/rsync_auto.rb b/plugins/synced_folders/rsync/command/rsync_auto.rb index 0ae393113..b2a4adc0b 100644 --- a/plugins/synced_folders/rsync/command/rsync_auto.rb +++ b/plugins/synced_folders/rsync/command/rsync_auto.rb @@ -48,6 +48,18 @@ module VagrantPlugins paths = {} ignores = [] 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 + cached = synced_folders(machine, cached: true) fresh = synced_folders(machine) diff = synced_folders_diff(cached, fresh) @@ -71,7 +83,7 @@ module VagrantPlugins folders.each do |id, folder_opts| # If we marked this folder to not auto sync, then # don't do it. - next if folder_opts.has_key?(:auto) && !folder_opts[:auto] + next if folder_opts.key?(:auto) && !folder_opts[:auto] hostpath = folder_opts[:hostpath] hostpath = File.expand_path(hostpath, machine.env.root_path) diff --git a/plugins/synced_folders/rsync/helper.rb b/plugins/synced_folders/rsync/helper.rb index bc7f6d116..c5bf8cbda 100644 --- a/plugins/synced_folders/rsync/helper.rb +++ b/plugins/synced_folders/rsync/helper.rb @@ -65,6 +65,7 @@ module VagrantPlugins "ssh -p #{ssh_info[:port]} " + proxy_command + "-o StrictHostKeyChecking=no " + + "-o IdentitiesOnly=true " + "-o UserKnownHostsFile=/dev/null", ssh_info[:private_key_path].map { |p| "-i '#{p}'" }, ].flatten.join(" ") diff --git a/plugins/synced_folders/smb/scripts/host_info.ps1 b/plugins/synced_folders/smb/scripts/host_info.ps1 index 7d283a7f4..801563f36 100644 --- a/plugins/synced_folders/smb/scripts/host_info.ps1 +++ b/plugins/synced_folders/smb/scripts/host_info.ps1 @@ -1,8 +1,8 @@ $ErrorAction = "Stop" -$net = Get-WmiObject -class win32_NetworkAdapterConfiguration -Filter 'ipenabled = "true"' +$net = Get-NetIPAddress | Where-Object {($_.IPAddress -ne "127.0.0.1") -and ($_.IPAddress -ne "::1")} $result = @{ - ip_addresses = $net.ipaddress + ip_addresses = $net.IPAddress } Write-Output $(ConvertTo-Json $result) diff --git a/plugins/synced_folders/smb/synced_folder.rb b/plugins/synced_folders/smb/synced_folder.rb index 2080efefc..e3f5a1af2 100644 --- a/plugins/synced_folders/smb/synced_folder.rb +++ b/plugins/synced_folders/smb/synced_folder.rb @@ -45,15 +45,17 @@ module VagrantPlugins script_path = File.expand_path("../scripts/set_share.ps1", __FILE__) # If we need auth information, then ask the user. - need_auth = false + have_auth = false folders.each do |id, data| - if !data[:smb_username] || !data[:smb_password] - need_auth = true + if data[:smb_username] && data[:smb_password] + @creds[:username] = data[:smb_username] + @creds[:password] = data[:smb_password] + have_auth = true break end end - if need_auth + if !have_auth machine.ui.detail(I18n.t("vagrant_sf_smb.warning_password") + "\n ") @creds[:username] = machine.ui.ask("Username: ") @creds[:password] = machine.ui.ask("Password (will be hidden): ", echo: false) @@ -150,29 +152,6 @@ module VagrantPlugins JSON.parse(r.stdout)["ip_addresses"] end - -=begin - def mount_shared_folders_to_windows - result = @env[:machine].provider.driver.execute('host_info.ps1', {}) - @smb_shared_folders.each do |id, data| - begin - options = { share_name: data[:share_name], - guest_path: data[:guestpath].gsub("/", "\\"), - guest_ip: ssh_info[:host], - username: ssh_info[:username], - host_ip: result["host_ip"], - password: @env[:machine].provider_config.guest.password, - host_share_username: @env[:machine].provider_config.host_share.username, - host_share_password: @env[:machine].provider_config.host_share.password} - @env[:ui].info("Linking #{data[:share_name]} to Guest at #{data[:guestpath]} ...") - @env[:machine].provider.driver.execute('mount_share.ps1', options) - rescue Error::SubprocessError => e - @env[:ui].info "Failed to link #{data[:share_name]} to Guest" - @env[:ui].info e.message - end - end - end -=end end end end diff --git a/scripts/website_push_docs.sh b/scripts/website_push_docs.sh index ec2d8a09f..fd3937fc4 100755 --- a/scripts/website_push_docs.sh +++ b/scripts/website_push_docs.sh @@ -8,5 +8,10 @@ DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" # Change into that directory cd $DIR +# Add the git remote if it doesn't exist +git remote | grep heroku-docs || { + git remote add heroku-docs git@heroku.com:vagrantup-docs.git +} + # Push the subtree (force) git push heroku-docs `git subtree split --prefix website/docs master`:master --force diff --git a/scripts/website_push_www.sh b/scripts/website_push_www.sh index 9e8bff265..1d5f180f9 100755 --- a/scripts/website_push_www.sh +++ b/scripts/website_push_www.sh @@ -8,5 +8,10 @@ DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" # Change into that directory cd $DIR +# Add the git remote if it doesn't exist +git remote | grep heroku-www || { + git remote add heroku-www git@heroku.com:vagrantup-www.git +} + # Push the subtree (force) git push heroku-www `git subtree split --prefix website/www master`:master --force diff --git a/templates/commands/init/Vagrantfile.erb b/templates/commands/init/Vagrantfile.erb index 7c8d05709..4cae3c990 100644 --- a/templates/commands/init/Vagrantfile.erb +++ b/templates/commands/init/Vagrantfile.erb @@ -1,15 +1,17 @@ # -*- mode: ruby -*- # vi: set ft=ruby : -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" +# 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. -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - # All Vagrant configuration is done here. The most common configuration - # options are documented and commented below. For a complete reference, - # please see the online documentation at vagrantup.com. - - # Every Vagrant virtual environment requires a box to build off of. + # Every Vagrant development environment requires a box. You can search for + # boxes at https://atlas.hashicorp.com/search. config.vm.box = "<%= box_name %>" <% if box_url -%> @@ -37,10 +39,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| # your network. # config.vm.network "public_network" - # If true, then any SSH connections made will enable agent forwarding. - # Default value: false - # config.ssh.forward_agent = true - # 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 @@ -52,77 +50,28 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| # Example for VirtualBox: # # config.vm.provider "virtualbox" do |vb| - # # Don't boot with headless mode + # # Display the VirtualBox GUI when booting the machine # vb.gui = true # - # # Use VBoxManage to customize the VM. For example to change memory: - # vb.customize ["modifyvm", :id, "--memory", "1024"] + # # Customize the amount of memory on the VM: + # vb.memory = "1024" # end # - # View the documentation for the provider you're using for more + # View the documentation for the provider you are using for more # information on available options. - # Enable provisioning with CFEngine. CFEngine Community packages are - # automatically installed. For example, configure the host as a - # policy server and optionally a policy file to run: - # - # config.vm.provision "cfengine" do |cf| - # cf.am_policy_hub = true - # # cf.run_file = "motd.cf" - # end - # - # You can also configure and bootstrap a client to an existing - # policy server: - # - # config.vm.provision "cfengine" do |cf| - # cf.policy_server_address = "10.0.2.15" + # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies + # such as FTP and Heroku are also available. See the documentation at + # https://docs.vagrantup.com/v2/push/atlas.html for more information. + # config.push.define "atlas" do |push| + # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" # end - # Enable provisioning with Puppet stand alone. Puppet manifests - # are contained in a directory path relative to this Vagrantfile. - # You will need to create the manifests directory and a manifest in - # the file default.pp in the manifests_path directory. - # - # config.vm.provision "puppet" do |puppet| - # puppet.manifests_path = "manifests" - # puppet.manifest_file = "default.pp" - # end - - # Enable provisioning with chef solo, specifying a cookbooks path, roles - # path, and data_bags path (all relative to this Vagrantfile), and adding - # some recipes and/or roles. - # - # config.vm.provision "chef_solo" do |chef| - # chef.cookbooks_path = "../my-recipes/cookbooks" - # chef.roles_path = "../my-recipes/roles" - # chef.data_bags_path = "../my-recipes/data_bags" - # chef.add_recipe "mysql" - # chef.add_role "web" - # - # # You may also specify custom JSON attributes: - # chef.json = { mysql_password: "foo" } - # end - - # Enable provisioning with chef server, specifying the chef server URL, - # and the path to the validation key (relative to this Vagrantfile). - # - # The Opscode Platform uses HTTPS. Substitute your organization for - # ORGNAME in the URL and validation key. - # - # If you have your own Chef Server, use the appropriate URL, which may be - # HTTP instead of HTTPS depending on your configuration. Also change the - # validation key to validation.pem. - # - # config.vm.provision "chef_client" do |chef| - # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" - # chef.validation_key_path = "ORGNAME-validator.pem" - # end - # - # If you're using the Opscode platform, your validator client is - # ORGNAME-validator, replacing ORGNAME with your organization name. - # - # If you have your own Chef Server, the default validation client name is - # chef-validator, unless you changed the configuration. - # - # chef.validation_client_name = "ORGNAME-validator" + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + # config.vm.provision "shell", inline: <<-SHELL + # sudo apt-get update + # sudo apt-get install -y apache2 + # SHELL end diff --git a/templates/commands/init/Vagrantfile.min.erb b/templates/commands/init/Vagrantfile.min.erb index a1f886d47..ac70f5e7c 100644 --- a/templates/commands/init/Vagrantfile.min.erb +++ b/templates/commands/init/Vagrantfile.min.erb @@ -1,10 +1,4 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" - -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| +Vagrant.configure(2) do |config| config.vm.box = "<%= box_name %>" <% if box_url -%> config.vm.box_url = "<%= box_url %>" diff --git a/templates/guests/arch/network_dhcp.erb b/templates/guests/arch/network_dhcp.erb index cea4e8587..377784574 100644 --- a/templates/guests/arch/network_dhcp.erb +++ b/templates/guests/arch/network_dhcp.erb @@ -1,4 +1,4 @@ Description='A basic dhcp ethernet connection' -Interface=eth<%= options[:interface] %> +Interface=<%= options[:device] %> Connection=ethernet IP=dhcp diff --git a/templates/guests/arch/network_static.erb b/templates/guests/arch/network_static.erb index 6cf2b9a62..ee58ee75a 100644 --- a/templates/guests/arch/network_static.erb +++ b/templates/guests/arch/network_static.erb @@ -1,5 +1,5 @@ Connection=ethernet Description='A basic static ethernet connection' -Interface=eth<%= options[:interface] %> +Interface=<%= options[:device] %> IP=static Address=('<%= options[:ip]%>/24') diff --git a/templates/locales/comm_winrm.yml b/templates/locales/comm_winrm.yml index 9de47a9cf..0f01a2755 100644 --- a/templates/locales/comm_winrm.yml +++ b/templates/locales/comm_winrm.yml @@ -1,11 +1,10 @@ en: vagrant_winrm: errors: - auth_error: |- + authentication_failed: |- An authorization error occurred while connecting to WinRM. User: %{user} - Password: %{password} Endpoint: %{endpoint} Message: %{message} winrm_bad_exit_status: |- @@ -29,6 +28,15 @@ en: 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 @@ -39,3 +47,36 @@ en: 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. diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 2ec1d5c54..5159c6c9a 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -87,6 +87,14 @@ en: 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, @@ -130,8 +138,13 @@ en: Starting Docker containers... inserted_key: |- Key inserted! Disconnecting and reconnecting using new SSH key... - inserting_insecure_key: |- - Inserting Vagrant public key within guest... + 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... list_commands: |- Below is a listing of all available Vagrant commands and a brief description of what they do. @@ -195,6 +208,10 @@ en: 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_ssh_password: |- The machine you're rsyncing folders to is configured to use password-based authentication. Vagrant can't script rsync to automatically @@ -212,6 +229,10 @@ en: 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 @@ -222,7 +243,7 @@ en: Press ctrl-c now to exit if you want to remove some boxes or free up some disk space. - Press any other key to continue. + Press the Enter or Return key to continue. version_current: |- Installed Version: %{version} version_latest: |- @@ -264,7 +285,7 @@ en: 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_networks" + 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} @@ -348,6 +369,9 @@ en: provider. Double-check your requested provider to verify you didn't simply misspell it. + If you're adding a box from HashiCorp's Atlas, make sure the box is + released. + Name: %{name} Address: %{url} Requested provider: %{requested} @@ -364,7 +388,7 @@ en: 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 Vagrant Cloud, please verify you're logged in via + box on HashiCorp's Atlas, please verify you're logged in via `vagrant login`. Also, please double-check the name. The expanded URL and error message are shown below: @@ -526,16 +550,14 @@ en: %{versions} 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. + A URL to an Atlas 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. + To set a URL to an Atlas 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 @@ -850,6 +872,21 @@ en: 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. @@ -907,6 +944,29 @@ en: You can however, install a plugin with the same name to replace these plugins. User-installed plugins take priority over system-installed plugins. + 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 @@ -1045,6 +1105,16 @@ en: 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 @@ -1190,6 +1260,18 @@ en: 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_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 @@ -1212,6 +1294,15 @@ en: 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_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 vm_creation_required: |- VM must be created before running this command. Run `vagrant up` first. vm_inaccessible: |- @@ -1248,8 +1339,8 @@ en: 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." + 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 @@ -1447,6 +1538,9 @@ en: 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. specific_not_found: |- Specific bridge '%{bridge}' not found. You may be asked to specify which network to bridge to. @@ -1579,6 +1673,8 @@ en: 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! @@ -1689,12 +1785,32 @@ en: "The cookbook path '%{path}' doesn't exist. Ignoring..." json: "Generating chef JSON and uploading..." client_key_folder: "Creating folder to hold client key..." + 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-zero..." + running_zero_again: "Running chef-zero 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 @@ -1719,6 +1835,9 @@ en: 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. deleting_from_server: "Deleting %{deletable} \"%{name}\" from Chef server..." file: @@ -1784,6 +1903,7 @@ en: 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." ansible: @@ -1802,7 +1922,13 @@ en: The specified minion_config file could not be found. master_config_nonexist: |- The specified master_config 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! + + pushes: + file: + no_destination: "File destination must be specified." diff --git a/templates/locales/providers_docker.yml b/templates/locales/providers_docker.yml index ca128a7cc..c04248a95 100644 --- a/templates/locales/providers_docker.yml +++ b/templates/locales/providers_docker.yml @@ -33,6 +33,8 @@ en: 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 @@ -151,6 +153,11 @@ en: is functional and properly configured. Host VM ID: %{id} + 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 diff --git a/templates/provisioners/chef_solo/solo.erb b/templates/provisioners/chef_solo/solo.erb index fd52fff91..25d3346b7 100644 --- a/templates/provisioners/chef_solo/solo.erb +++ b/templates/provisioners/chef_solo/solo.erb @@ -5,11 +5,7 @@ file_cache_path "<%= file_cache_path %>" file_backup_path "<%= file_backup_path %>" cookbook_path <%= cookbooks_path.inspect %> <% if roles_path %> -if Chef::VERSION.to_f < 11.8 - role_path <%= roles_path.first.inspect %> -else - role_path <%= roles_path.inspect %> -end +role_path <%= roles_path.size == 1 ? roles_path.first.inspect : roles_path.inspect %> <% end %> log_level <%= log_level.inspect %> verbose_logging <%= verbose_logging.inspect %> @@ -32,6 +28,13 @@ environment_path <%= environments_path.inspect %> environment "<%= environment %>" <% end -%> +<% if local_mode -%> +local_mode true +<% end -%> +<% if node_path -%> +node_path <%= node_path.inspect %> +<% end -%> + http_proxy <%= http_proxy.inspect %> http_proxy_user <%= http_proxy_user.inspect %> http_proxy_pass <%= http_proxy_pass.inspect %> diff --git a/templates/provisioners/chef_zero/zero.erb b/templates/provisioners/chef_zero/zero.erb new file mode 100644 index 000000000..29de30d23 --- /dev/null +++ b/templates/provisioners/chef_zero/zero.erb @@ -0,0 +1,44 @@ +<% 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.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 node_path -%> +node_path <%= node_path.inspect %> +<% end -%> + +<% if formatter %> +add_formatter "<%= formatter %>" +<% end %> + +<% if custom_configuration -%> +Chef::Config.from_file "<%= custom_configuration %>" +<% end -%> diff --git a/test/unit/base.rb b/test/unit/base.rb index 3020c7b91..e3c68c099 100644 --- a/test/unit/base.rb +++ b/test/unit/base.rb @@ -4,6 +4,7 @@ require "rubygems" # Gems require "checkpoint" require "rspec/autorun" +require "webmock/rspec" # Require Vagrant itself so we can reference the proper # classes to test. @@ -28,7 +29,6 @@ $stderr.sync = true # Configure RSpec RSpec.configure do |c| - c.expect_with :rspec, :stdlib c.treat_symbols_as_metadata_keys_with_true_values = true if Vagrant::Util::Platform.windows? diff --git a/test/unit/plugins/commands/login/client_test.rb b/test/unit/plugins/commands/login/client_test.rb new file mode 100644 index 000000000..f57670eb0 --- /dev/null +++ b/test/unit/plugins/commands/login/client_test.rb @@ -0,0 +1,130 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/login/command") + +describe VagrantPlugins::LoginCommand::Client do + include_context "unit" + + let(:env) { isolated_environment.create_vagrant_env } + + subject { described_class.new(env) } + + before do + stub_env("ATLAS_TOKEN" => nil) + subject.clear_token + end + + describe "#logged_in?" do + let(:url) { "#{Vagrant.server_url}/api/v1/authenticate?access_token=#{token}" } + let(:headers) { { "Content-Type" => "application/json" } } + + before { allow(subject).to receive(:token).and_return(token) } + + context "when there is no token" do + let(:token) { nil } + + it "returns false" do + expect(subject.logged_in?).to be(false) + end + end + + context "when there is a token" do + let(:token) { "ABCD1234" } + + it "returns true if the endpoint returns a 200" do + stub_request(:get, url) + .with(headers: headers) + .to_return(body: JSON.pretty_generate("token" => token)) + expect(subject.logged_in?).to be(true) + end + + it "returns false if the endpoint returns a non-200" do + stub_request(:get, url) + .with(headers: headers) + .to_return(body: JSON.pretty_generate("bad" => true), status: 401) + expect(subject.logged_in?).to be(false) + end + + it "raises an exception if the server cannot be found" do + stub_request(:get, url) + .to_raise(SocketError) + expect { subject.logged_in? } + .to raise_error(VagrantPlugins::LoginCommand::Errors::ServerUnreachable) + end + end + end + + describe "#login" do + it "returns the access token after successful login" do + request = { + "user" => { + "login" => "foo", + "password" => "bar", + }, + } + + response = { + "token" => "baz", + } + + headers = { "Content-Type" => "application/json" } + + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + with(body: JSON.dump(request), headers: headers). + to_return(status: 200, body: JSON.dump(response)) + + expect(subject.login("foo", "bar")).to eq("baz") + end + + it "returns nil on bad login" do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_return(status: 401, body: "") + + expect(subject.login("foo", "bar")).to be(false) + end + + it "raises an exception if it can't reach the sever" do + stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate"). + to_raise(SocketError) + + expect { subject.login("foo", "bar") }. + to raise_error(VagrantPlugins::LoginCommand::Errors::ServerUnreachable) + end + end + + describe "#token" do + it "reads ATLAS_TOKEN" do + stub_env("ATLAS_TOKEN" => "ABCD1234") + expect(subject.token).to eq("ABCD1234") + end + + it "reads the stored file" do + subject.store_token("EFGH5678") + expect(subject.token).to eq("EFGH5678") + end + + it "prefers the environment variable" do + stub_env("ATLAS_TOKEN" => "ABCD1234") + subject.store_token("EFGH5678") + expect(subject.token).to eq("ABCD1234") + end + + it "returns nil if there's no token set" do + expect(subject.token).to be(nil) + end + end + + describe "#store_token, #clear_token" do + it "stores the token and can re-access it" do + subject.store_token("foo") + expect(subject.token).to eq("foo") + expect(described_class.new(env).token).to eq("foo") + end + + it "deletes the token" do + subject.store_token("foo") + subject.clear_token + expect(subject.token).to be_nil + end + end +end diff --git a/test/unit/plugins/commands/login/command_test.rb b/test/unit/plugins/commands/login/command_test.rb new file mode 100644 index 000000000..c92c51174 --- /dev/null +++ b/test/unit/plugins/commands/login/command_test.rb @@ -0,0 +1,96 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/login/command") + +describe VagrantPlugins::LoginCommand::Command do + include_context "unit" + + let(:env) { isolated_environment.create_vagrant_env } + + let(:token_path) { env.data_dir.join("vagrant_login_token") } + + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + + subject { described_class.new(argv, env) } + + before do + stub_env("ATLAS_TOKEN" => "") + end + + describe "#execute" do + context "with no args" do + let(:argv) { [] } + end + + context "with --check" do + let(:argv) { ["--check"] } + + context "when there is a token" do + before do + stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate}) + .to_return(status: 200) + end + + before do + File.open(token_path, "w+") { |f| f.write("abcd1234") } + end + + it "returns 0" do + expect(subject.execute).to eq(0) + end + end + + context "when there is no token" do + it "returns 1" do + expect(subject.execute).to eq(1) + end + end + end + + context "with --logout" do + let(:argv) { ["--logout"] } + + it "returns 0" do + expect(subject.execute).to eq(0) + end + + it "clears the token" do + subject.execute + expect(File.exist?(token_path)).to be(false) + end + end + + context "with --token" do + let(:argv) { ["--token", "efgh5678"] } + + context "when the token is valid" do + before do + stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate}) + .to_return(status: 200) + end + + it "sets the token" do + subject.execute + token = File.read(token_path).strip + expect(token).to eq("efgh5678") + end + + it "returns 0" do + expect(subject.execute).to eq(0) + end + end + + context "when the token is invalid" do + before do + stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate}) + .to_return(status: 401) + end + + it "returns 1" do + expect(subject.execute).to eq(1) + end + end + end + end +end diff --git a/test/unit/plugins/commands/login/middleware/add_authentication_test.rb b/test/unit/plugins/commands/login/middleware/add_authentication_test.rb new file mode 100644 index 000000000..99c99a8af --- /dev/null +++ b/test/unit/plugins/commands/login/middleware/add_authentication_test.rb @@ -0,0 +1,88 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/login/middleware/add_authentication") + +describe VagrantPlugins::LoginCommand::AddAuthentication do + include_context "unit" + + let(:app) { lambda { |env| } } + let(:env) { { + env: iso_env, + } } + + let(:iso_env) { isolated_environment.create_vagrant_env } + let(:server_url) { "http://foo.com" } + + subject { described_class.new(app, env) } + + before do + allow(Vagrant).to receive(:server_url).and_return(server_url) + 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) + VagrantPlugins::LoginCommand::Client.new(iso_env).store_token("foo") + + original = ["foo", "#{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 + + it "appends the access token to the URL of server URLs" do + token = "foobarbaz" + VagrantPlugins::LoginCommand::Client.new(iso_env).store_token(token) + + original = [ + "http://google.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 "appends 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) + + token = "foobarbaz" + VagrantPlugins::LoginCommand::Client.new(iso_env).store_token(token) + + original = [ + "http://google.com/box.box", + "http://vagrantcloud.com/foo.box", + "http://vagrantcloud.com/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 + end +end diff --git a/test/unit/plugins/commands/push/command_test.rb b/test/unit/plugins/commands/push/command_test.rb new file mode 100644 index 000000000..a97f4caba --- /dev/null +++ b/test/unit/plugins/commands/push/command_test.rb @@ -0,0 +1,141 @@ +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" + end + VF + iso_env.create_vagrant_env + end + + let(:argv) { [] } + let(:pushes) { {} } + + subject { described_class.new(argv, env) } + + before do + Vagrant.plugin("2").manager.stub(pushes: 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.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 diff --git a/test/unit/plugins/communicators/winrm/communicator_test.rb b/test/unit/plugins/communicators/winrm/communicator_test.rb index 1433f4317..6e5b406d7 100644 --- a/test/unit/plugins/communicators/winrm/communicator_test.rb +++ b/test/unit/plugins/communicators/winrm/communicator_test.rb @@ -28,11 +28,16 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do expect(subject.ready?).to be_true end - it "returns false if hostname command fails to execute without error" do - expect(shell).to receive(:powershell).with("hostname").and_raise(Vagrant::Errors::VagrantError) + it "returns false if hostname command fails with a transient error" do + expect(shell).to receive(:powershell).with("hostname").and_raise(VagrantPlugins::CommunicatorWinRM::Errors::TransientError) expect(subject.ready?).to be_false end + it "raises an error if hostname command fails with an unknown error" do + expect(shell).to receive(:powershell).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(:powershell).with("hostname") do sleep 2 # winrm.timeout = 1 @@ -75,15 +80,23 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do describe ".test" do it "returns true when exit code is zero" do - expect(shell).to receive(:powershell).with(kind_of(String)).and_return({ exitcode: 0 }) + output = { exitcode: 0, data:[{ stderr: '' }] } + expect(shell).to receive(:powershell).with(kind_of(String)).and_return(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({ exitcode: 1 }) + output = { exitcode: 1, data:[{ stderr: '' }] } + expect(shell).to receive(:powershell).with(kind_of(String)).and_return(output) expect(subject.test("test -d /tmp/foobar")).to be_false end + it "returns false when stderr contains output" do + output = { exitcode: 0, data:[{ 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 @@ -101,6 +114,6 @@ describe VagrantPlugins::CommunicatorWinRM::Communicator do expect(shell).to receive(:download).with("from", "to") subject.download("from", "to") end - end + end end diff --git a/test/unit/plugins/communicators/winrm/shell_test.rb b/test/unit/plugins/communicators/winrm/shell_test.rb index 477108b97..de7b1dc19 100644 --- a/test/unit/plugins/communicators/winrm/shell_test.rb +++ b/test/unit/plugins/communicators/winrm/shell_test.rb @@ -1,14 +1,25 @@ 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(:session) { double("winrm_session") } + 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.finalize! + end + } subject do - described_class.new('localhost', 'username', 'password').tap do |comm| + described_class.new('localhost', port, config).tap do |comm| allow(comm).to receive(:new_session).and_return(session) end end @@ -19,11 +30,15 @@ describe VagrantPlugins::CommunicatorWinRM::WinRMShell do expect(subject.powershell("dir")[:exitcode]).to eq(0) end - it "should raise auth error when exception message contains 401" do - expect(session).to receive(:powershell).with(/^dir.+/).and_raise( - StandardError.new("Oh no! a 401 SOAP error!")) + it "should retry when a WinRMAuthorizationError is received" do + expect(session).to receive(:powershell).with(/^dir.+/).exactly(3).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!! Unauthrorized") + ) expect { subject.powershell("dir") }.to raise_error( - VagrantPlugins::CommunicatorWinRM::Errors::AuthError) + VagrantPlugins::CommunicatorWinRM::Errors::AuthenticationFailed) end it "should raise an execution error when an exception occurs" do @@ -42,8 +57,22 @@ describe VagrantPlugins::CommunicatorWinRM::WinRMShell do end describe ".endpoint" do - it "should create winrm endpoint address" do - expect(subject.send(:endpoint)).to eq("http://localhost:5985/wsman") + 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 :plaintext" do + it "should create winrm endpoint address using http" do + expect(subject.send(:endpoint)).to eq("http://localhost:5985/wsman") + end end end @@ -51,7 +80,7 @@ describe VagrantPlugins::CommunicatorWinRM::WinRMShell do it "should create endpoint options" do expect(subject.send(:endpoint_options)).to eq( { user: "username", pass: "password", host: "localhost", port: 5985, - operation_timeout: 60, basic_auth_only: true }) + basic_auth_only: true, no_ssl_peer_verification: false }) end end diff --git a/test/unit/plugins/guests/windows/cap/mount_shared_folder_test.rb b/test/unit/plugins/guests/windows/cap/mount_shared_folder_test.rb index 93fe41799..bd443142b 100644 --- a/test/unit/plugins/guests/windows/cap/mount_shared_folder_test.rb +++ b/test/unit/plugins/guests/windows/cap/mount_shared_folder_test.rb @@ -79,4 +79,23 @@ describe "VagrantPlugins::GuestWindows::Cap::MountSharedFolder" do 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", + }) + described_class.mount_smb_shared_folder(machine, 'name', 'guestpath', {:smb_username => "user", :smb_password => "pass", :smb_host => "host"}) + end + end + end + end diff --git a/test/unit/plugins/guests/windows/cap/rsync_test.rb b/test/unit/plugins/guests/windows/cap/rsync_test.rb new file mode 100644 index 000000000..e28a5b3fb --- /dev/null +++ b/test/unit/plugins/guests/windows/cap/rsync_test.rb @@ -0,0 +1,26 @@ +require File.expand_path("../../../../../base", __FILE__) + +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'") + described_class.rsync_pre(machine, guestpath: '/sync_dir') + end + end +end diff --git a/test/unit/plugins/kernel_v2/config/push_test.rb b/test/unit/plugins/kernel_v2/config/push_test.rb new file mode 100644 index 000000000..e1c67996f --- /dev/null +++ b/test/unit/plugins/kernel_v2/config/push_test.rb @@ -0,0 +1,352 @@ +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 + 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 diff --git a/test/unit/plugins/kernel_v2/config/vm_test.rb b/test/unit/plugins/kernel_v2/config/vm_test.rb index bbe2a5979..50f185c9c 100644 --- a/test/unit/plugins/kernel_v2/config/vm_test.rb +++ b/test/unit/plugins/kernel_v2/config/vm_test.rb @@ -23,6 +23,13 @@ describe VagrantPlugins::Kernel_V2::VMConfig do 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") env.stub(root_path: nil) @@ -121,28 +128,6 @@ describe VagrantPlugins::Kernel_V2::VMConfig do end end - describe "#define" do - it "should allow regular names" do - subject.define "foo" - subject.finalize! - - assert_valid - end - - [ - "foo [1]", - "bar {2}", - "foo/bar", - ].each do |name| - it "should disallow names with brackets" do - subject.define name - subject.finalize! - - assert_invalid - end - end - end - describe "#guest" do it "is nil by default" do subject.finalize! @@ -183,6 +168,7 @@ describe VagrantPlugins::Kernel_V2::VMConfig do subject.finalize! n = subject.networks expect(n.length).to eq(2) + expect(n[0][0]).to eq(:forwarded_port) expect(n[0][1][:guest]).to eq(5985) expect(n[0][1][:host]).to eq(55985) @@ -190,9 +176,10 @@ describe VagrantPlugins::Kernel_V2::VMConfig do expect(n[0][1][:id]).to eq("winrm") expect(n[1][0]).to eq(:forwarded_port) - expect(n[1][1][:guest]).to eq(22) - expect(n[1][1][:host]).to eq(2222) - expect(n[1][1][:id]).to eq("ssh") + 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 "allows overriding SSH" do @@ -214,12 +201,22 @@ describe VagrantPlugins::Kernel_V2::VMConfig do guest: 22, host: 14100, id: "winrm" subject.finalize! - n = subject.networks - expect(n.length).to eq(2) - 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("winrm") + winrm_network = find_network 'winrm' + expect(winrm_network[:guest]).to eq(22) + expect(winrm_network[:host]).to eq(14100) + expect(winrm_network[:id]).to eq("winrm") + end + + it "allows overriding WinRM SSL" do + subject.communicator = :winrmssl + subject.network "forwarded_port", + guest: 22, host: 14100, id: "winrmssl" + subject.finalize! + + winrmssl_network = find_network 'winrmssl' + expect(winrmssl_network[:guest]).to eq(22) + expect(winrmssl_network[:host]).to eq(14100) + expect(winrmssl_network[:id]).to eq("winrmssl") end it "turns all forwarded port ports to ints" do @@ -262,6 +259,41 @@ describe VagrantPlugins::Kernel_V2::VMConfig do 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| @@ -306,8 +338,8 @@ describe VagrantPlugins::Kernel_V2::VMConfig do end it "allows provisioner settings to be overriden" do - subject.provision("shell", path: "foo", id: "s") { |s| s.inline = "foo" } - subject.provision("shell", inline: "bar", id: "s") { |s| s.args = "bar" } + 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 @@ -377,12 +409,12 @@ describe VagrantPlugins::Kernel_V2::VMConfig do end it "uses the proper order when merging overrides" do - subject.provision("shell", inline: "foo", id: "original") - subject.provision("shell", inline: "other", id: "other") + 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("shell", inline: "foo-overload", id: "original") + other.provision("original", inline: "foo-overload", type: "shell") merged = subject.merge(other) merged_provs = merged.provisioners @@ -397,13 +429,13 @@ describe VagrantPlugins::Kernel_V2::VMConfig do end it "can preserve order for overrides" do - subject.provision("shell", inline: "foo", id: "original") - subject.provision("shell", inline: "other", id: "other") + 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( - "shell", inline: "foo-overload", id: "original", + "original", inline: "foo-overload", type: "shell", preserve_order: true) merged = subject.merge(other) diff --git a/test/unit/plugins/providers/docker/config_test.rb b/test/unit/plugins/providers/docker/config_test.rb index ac20f6bc3..0d1937994 100644 --- a/test/unit/plugins/providers/docker/config_test.rb +++ b/test/unit/plugins/providers/docker/config_test.rb @@ -47,8 +47,14 @@ describe VagrantPlugins::DockerProvider::Config do 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 diff --git a/test/unit/plugins/providers/docker/driver_test.rb b/test/unit/plugins/providers/docker/driver_test.rb index 337436a51..05ac67f88 100644 --- a/test/unit/plugins/providers/docker/driver_test.rb +++ b/test/unit/plugins/providers/docker/driver_test.rb @@ -17,7 +17,7 @@ describe VagrantPlugins::DockerProvider::Driver do ports: '8080:80', volumes: '/host/path:guest/path', detach: true, - links: {janis: 'joplin'}, + links: [[:janis, 'joplin'], [:janis, 'janis']], env: {key: 'value'}, name: cid, hostname: 'jimi-hendrix', @@ -43,7 +43,9 @@ describe VagrantPlugins::DockerProvider::Driver do end it 'links containers' do - expect(cmd_executed).to match(/--link #{params[:links].to_a.flatten.join(':')} .+ #{Regexp.escape params[:image]}/) + params[:links].each do |link| + expect(cmd_executed).to match(/--link #{link.join(':')} .+ #{Regexp.escape params[:image]}/) + end end it 'sets environmental variables' do @@ -140,7 +142,12 @@ describe VagrantPlugins::DockerProvider::Driver do it 'stops the container' do subject.should_receive(:execute).with('docker', 'stop', '-t', '1', cid) - subject.stop(cid) + subject.stop(cid, 1) + end + + it "stops the container with the set timeout" do + subject.should_receive(:execute).with('docker', 'stop', '-t', '5', cid) + subject.stop(cid, 5) end end @@ -149,7 +156,7 @@ describe VagrantPlugins::DockerProvider::Driver do it 'does not stop container' do subject.should_not_receive(:execute).with('docker', 'stop', '-t', '1', cid) - subject.stop(cid) + subject.stop(cid, 1) end end end diff --git a/test/unit/plugins/providers/hyperv/config_test.rb b/test/unit/plugins/providers/hyperv/config_test.rb index 5177dca59..a3c46c619 100644 --- a/test/unit/plugins/providers/hyperv/config_test.rb +++ b/test/unit/plugins/providers/hyperv/config_test.rb @@ -9,10 +9,37 @@ describe VagrantPlugins::HyperV::Config do 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 "#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 end diff --git a/test/unit/plugins/providers/virtualbox/action/network_test.rb b/test/unit/plugins/providers/virtualbox/action/network_test.rb new file mode 100644 index 000000000..82dc14865 --- /dev/null +++ b/test/unit/plugins/providers/virtualbox/action/network_test.rb @@ -0,0 +1,141 @@ +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| + m.provider.stub(driver: driver) + end + end + + let(:env) {{ machine: machine, ui: machine.ui }} + let(:app) { lambda { |*args| }} + let(:driver) { double("driver") } + + 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 + + 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 + + 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 "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) + + subject.call(env) + + expect(driver).to have_received(:create_host_only_network).with({ + adapter_ip: '172.28.128.1', + netmask: '255.255.255.0', + }) + + expect(driver).to have_received(:create_dhcp_server).with('vboxnet0', { + adapter_ip: "172.28.128.1", + auto_config: true, + ip: "172.28.128.1", + mac: nil, + netmask: "255.255.255.0", + nic_type: nil, + type: :dhcp, + dhcp_ip: "172.28.128.2", + dhcp_lower: "172.28.128.3", + dhcp_upper: "172.28.128.254", + adapter: 2 + }) + + expect(guest).to have_received(:capability).with(:configure_networks, [{ + type: :dhcp, + adapter_ip: "172.28.128.1", + ip: "172.28.128.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 +end diff --git a/test/unit/plugins/providers/virtualbox/action/prepare_nfs_settings_test.rb b/test/unit/plugins/providers/virtualbox/action/prepare_nfs_settings_test.rb index 8acc6fa28..3cead74f4 100644 --- a/test/unit/plugins/providers/virtualbox/action/prepare_nfs_settings_test.rb +++ b/test/unit/plugins/providers/virtualbox/action/prepare_nfs_settings_test.rb @@ -22,10 +22,21 @@ describe VagrantPlugins::ProviderVirtualBox::Action::PrepareNFSSettings do 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 } + end + it "calls the next action in the chain" do + driver.stub(read_network_interfaces: {2 => {type: :hostonly, hostonly: "vmnet2"}}) + driver.stub(read_host_only_interfaces: [{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 } diff --git a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb index cb84e2325..88f199a82 100644 --- a/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb +++ b/test/unit/plugins/providers/virtualbox/support/shared/virtualbox_driver_version_4_x_examples.rb @@ -3,6 +3,76 @@ shared_examples "a version 4.x virtualbox driver" do |options| 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" @@ -38,5 +108,108 @@ shared_examples "a version 4.x virtualbox driver" do |options| 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', + status: 'Up', + }]) + 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', status: 'Up'}, + {name: 'vboxnet1', ip: '10.0.0.1', netmask: '255.255.255.0', status: 'Up'}, + ]) + 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 diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index 45951e8b3..05fa8fae3 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -50,6 +50,9 @@ VF let(:generated_inventory_file) { File.join(generated_inventory_dir, 'vagrant_ansible_inventory') } before do + Vagrant::Util::Platform.stub(solaris?: false) + subject.instance_variable_get(:@logger).stub(:debug?).and_return(false) + machine.stub(ssh_info: ssh_info) machine.env.stub(active_machines: [[iso_env.machine_names[0], :dummy], [iso_env.machine_names[1], :dummy]]) @@ -60,13 +63,17 @@ VF # Class methods for code reuse across examples # - def self.it_should_set_arguments_and_environment_variables(expected_args_count = 5, expected_vars_count = 3, expected_host_key_checking = false) + def self.it_should_set_arguments_and_environment_variables( + expected_args_count = 7, expected_vars_count = 4, expected_host_key_checking = false, expected_transport_mode = "ssh") + it "sets implicit arguments in a specific order" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| expect(args[0]).to eq("ansible-playbook") expect(args[1]).to eq("--private-key=#{machine.ssh_info[:private_key_path][0]}") expect(args[2]).to eq("--user=#{machine.ssh_info[:username]}") + expect(args[3]).to eq("--connection=ssh") + expect(args[4]).to eq("--timeout=30") inventory_count = args.count { |x| x =~ /^--inventory-file=.+$/ } expect(inventory_count).to be > 0 @@ -77,18 +84,18 @@ VF it "sets --limit argument" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| - raw_limits = [] + all_limits = args.select { |x| x =~ /^(--limit=|-l)/ } if config.raw_arguments raw_limits = config.raw_arguments.select { |x| x =~ /^(--limit=|-l)/ } - end - all_limits = args.select { |x| x =~ /^(--limit=|-l)/ } - expect(all_limits.length - raw_limits.length).to eq(1) - - if config.limit - limit = config.limit.kind_of?(Array) ? config.limit.join(',') : config.limit - expect(all_limits.last).to eq("--limit=#{limit}") + expect(all_limits.length - raw_limits.length).to eq(1) + expect(all_limits.last).to eq(raw_limits.last) else - expect(all_limits.first).to eq("--limit=#{machine.name}") + 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 } end @@ -96,6 +103,13 @@ VF it "exports environment variables" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*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]['ANSIBLE_HOST_KEY_CHECKING']).to eql(expected_host_key_checking.to_s) expect(cmd_opts[:env]['PYTHONUNBUFFERED']).to eql(1) @@ -109,6 +123,15 @@ VF expect(args.last[:env].length).to eq(expected_vars_count) } end + + it "enables '#{expected_transport_mode}' transport mode" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*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 + } + end + end def self.it_should_set_optional_arguments(arg_map) @@ -126,35 +149,7 @@ VF end end - def self.it_should_use_smart_transport_mode - it "does not export ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| - cmd_opts = args.last - expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to be_nil - } - end - - it "does not force any transport mode" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| - total = args.count { |x| x =~ /^--connection=\w+$/ } - expect(total).to eql(0) - } - end - end - - def self.it_should_use_transport_mode(transport_mode) - it "enables '#{transport_mode}' transport mode" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| - index = args.rindex("--connection=#{transport_mode}") - expect(index).to be > 0 - expect(find_last_argument_after(index, args, /--connection=\w+/)).to be_false - } - end - end - - def self.it_should_force_ssh_transport_mode - it_should_use_transport_mode('ssh') - + 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 { |*args| cmd_opts = args.last @@ -210,7 +205,6 @@ VF describe "with default options" do it_should_set_arguments_and_environment_variables - it_should_use_smart_transport_mode it_should_create_and_use_generated_inventory it "does not add any group section to the generated inventory" do @@ -279,8 +273,7 @@ VF config.host_key_checking = true end - it_should_set_arguments_and_environment_variables 5, 3, true - it_should_use_smart_transport_mode + it_should_set_arguments_and_environment_variables 7, 4, true end describe "with boolean (flag) options disabled" do @@ -292,7 +285,7 @@ VF config.sudo_user = 'root' end - it_should_set_arguments_and_environment_variables 6 + it_should_set_arguments_and_environment_variables 8 it_should_set_optional_arguments({ "sudo_user" => "--sudo-user=root" }) it "it does not set boolean flag when corresponding option is set to false" do @@ -308,6 +301,7 @@ VF before do config.sudo = false config.skip_tags = %w(foo bar) + config.limit = "all" config.raw_arguments = ["--connection=paramiko", "--skip-tags=ignored", "--module-path=/other/modules", @@ -316,12 +310,11 @@ VF "--limit=foo", "--limit=bar", "--inventory-file=/forget/it/my/friend", + "--user=lion", "--new-arg=yeah"] end - it_should_set_arguments_and_environment_variables 15 - it_should_create_and_use_generated_inventory - it_should_use_transport_mode('paramiko') + it_should_set_arguments_and_environment_variables 18, 4, false, "paramiko" it "sets all raw arguments" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| @@ -331,9 +324,12 @@ VF } end - it "sets raw arguments before arguments related to supported options" do + it "sets raw arguments after arguments related to supported options" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| - expect(args.index("--skip-tags=foo,bar")).to be > args.index("--skip-tags=ignored") + 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") } end @@ -359,7 +355,6 @@ VF end it_should_set_arguments_and_environment_variables - it_should_use_smart_transport_mode it "does not generate the inventory and uses given inventory path instead" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| @@ -375,7 +370,7 @@ VF config.ask_vault_pass = true end - it_should_set_arguments_and_environment_variables 6 + it_should_set_arguments_and_environment_variables 8 it "should ask the vault password" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| @@ -389,7 +384,7 @@ VF config.vault_password_file = existing_file end - it_should_set_arguments_and_environment_variables 6 + it_should_set_arguments_and_environment_variables 8 it "uses the given vault password file" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| @@ -403,8 +398,8 @@ VF config.raw_ssh_args = ['-o ControlMaster=no', '-o ForwardAgent=no'] end - it_should_set_arguments_and_environment_variables 6, 4 - it_should_force_ssh_transport_mode + it_should_set_arguments_and_environment_variables 7, 4 + 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 { |*args| @@ -437,8 +432,8 @@ VF ssh_info[:private_key_path] = ['/path/to/my/key', '/an/other/identity', '/yet/an/other/key'] end - it_should_set_arguments_and_environment_variables 6, 4 - it_should_force_ssh_transport_mode + it_should_set_arguments_and_environment_variables 7, 4 + 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 { |*args| @@ -454,8 +449,8 @@ VF ssh_info[:forward_agent] = true end - it_should_set_arguments_and_environment_variables 6, 4 - it_should_force_ssh_transport_mode + it_should_set_arguments_and_environment_variables 7, 4 + 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 { |*args| @@ -465,17 +460,30 @@ VF end end + describe "with VAGRANT_LOG=debug, but without verbose option" do + before do + subject.instance_variable_get(:@logger).stub(:debug?).and_return(true) + config.verbose = false + end + + it "shows the ansible-playbook command" do + expect(machine.env.ui).to receive(:detail).with { |full_command| + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_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 --private-key=/path/to/my/key --user=testuser --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} playbook.yml") + } + end + end + describe "with verbose option" do before do config.verbose = 'v' end - it_should_set_arguments_and_environment_variables 6 + it_should_set_arguments_and_environment_variables 8 it_should_set_optional_arguments({ "verbose" => "-v" }) it "shows the ansible-playbook command" do expect(machine.env.ui).to receive(:detail).with { |full_command| - expect(full_command).to eq("ANSIBLE_FORCE_COLOR=true ANSIBLE_HOST_KEY_CHECKING=false PYTHONUNBUFFERED=1 ansible-playbook --private-key=/path/to/my/key --user=testuser --limit='machine1' --inventory-file=#{generated_inventory_dir} -v playbook.yml") + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --private-key=/path/to/my/key --user=testuser --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} -v playbook.yml") } end end @@ -508,8 +516,8 @@ VF config.raw_ssh_args = ['-o ControlMaster=no'] end - it_should_set_arguments_and_environment_variables 20, 4, true - it_should_force_ssh_transport_mode + it_should_set_arguments_and_environment_variables 21, 4, true + it_should_explicitly_enable_ansible_ssh_control_persist_defaults it_should_set_optional_arguments({ "extra_vars" => "--extra-vars=@#{File.expand_path(__FILE__)}", "sudo" => "--sudo", "sudo_user" => "--sudo-user=deployer", @@ -522,7 +530,7 @@ VF "limit" => "--limit=machine*:&vagrant:!that_one", "start_at_task" => "--start-at-task=an awesome task", }) - + it "also includes given raw arguments" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| expect(args).to include("--su-user=foot") @@ -533,10 +541,84 @@ VF it "shows the ansible-playbook command, with additional quotes when required" do expect(machine.env.ui).to receive(:detail).with { |full_command| - expect(full_command).to eq("ANSIBLE_FORCE_COLOR=true ANSIBLE_HOST_KEY_CHECKING=true PYTHONUNBUFFERED=1 ANSIBLE_SSH_ARGS='-o IdentityFile=/my/key2 -o ForwardAgent=yes -o ControlMaster=no -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --private-key=/my/key1 --user=testuser --connection=ssh --why-not --su-user=foot --ask-su-pass --limit='all' --inventory-file=#{generated_inventory_dir} --extra-vars=@#{File.expand_path(__FILE__)} --sudo --sudo-user=deployer -vvv --ask-sudo-pass --ask-vault-pass --vault-password-file=#{File.expand_path(__FILE__)} --tags=db,www --skip-tags=foo,bar --limit='machine*:&vagrant:!that_one' --start-at-task='an awesome task' playbook.yml") + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_SSH_ARGS='-o IdentitiesOnly=yes -o IdentityFile=/my/key2 -o ForwardAgent=yes -o ControlMaster=no -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --private-key=/my/key1 --user=testuser --connection=ssh --timeout=30 --limit='machine*:&vagrant:!that_one' --inventory-file=#{generated_inventory_dir} --extra-vars=@#{File.expand_path(__FILE__)} --sudo --sudo-user=deployer -vvv --ask-sudo-pass --ask-vault-pass --vault-password-file=#{File.expand_path(__FILE__)} --tags=db,www --skip-tags=foo,bar --start-at-task='an awesome task' --why-not --su-user=foot --ask-su-pass --limit='all' 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| + h.stub(ssh_info: fake_host_ssh_info) + end + } + + before do + machine.stub(provider_name: :docker) + machine.provider.stub(host_vm?: true) + machine.provider.stub(host_vm: fake_host_vm) + end + + it "uses an SSH ProxyCommand to reach the VM" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*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'") + } + end + end + + # + # Special cases related to the Vagrant Host operating system in use + # + + context "with a Solaris-like host" do + before do + Vagrant::Util::Platform.stub(solaris?: true) + end + + it "does not set IdentitiesOnly=yes in ANSIBLE_SSH_ARGS" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + cmd_opts = args.last + expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to_not include("-o IdentitiesOnly=yes") + + # Note: + # The expectation below is a workaround to a possible misuse (or bug) in RSpec/Ruby stack. + # If 'args' variable is not required by in this block, the "Vagrant::Util::Subprocess).to receive" + # surprisingly expects to receive "no args". + # This problem can be "solved" by using args the "unnecessary" (but harmless) expectation below: + expect(true).to be_true + } + 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 { |*args| + cmd_opts = args.last + expect(cmd_opts[:env]).to_not include('ANSIBLE_SSH_ARGS') + + # Note: + # The expectation below is a workaround to a possible misuse (or bug) in RSpec/Ruby stack. + # If 'args' variable is not required by in this block, the "Vagrant::Util::Subprocess).to receive" + # surprisingly expects to receive "no args". + # This problem can be "solved" by using args the "unnecessary" (but harmless) expectation below: + expect(true).to be_true + } + end + end + + end end end diff --git a/test/unit/plugins/provisioners/chef/config/base_runner_test.rb b/test/unit/plugins/provisioners/chef/config/base_runner_test.rb new file mode 100644 index 000000000..3ef7e2fab --- /dev/null +++ b/test/unit/plugins/provisioners/chef/config/base_runner_test.rb @@ -0,0 +1,262 @@ +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 a tmp_path" do + subject.finalize! + expect(subject.provisioning_path).to eq("/tmp/vagrant-chef") + end + end + + describe "#file_backup_path" do + it "defaults to /var/chef/backup" do + subject.finalize! + expect(subject.file_backup_path).to eq("/var/chef/backup") + end + end + + describe "#file_cache_path" do + it "defaults to /var/chef/cache" do + subject.finalize! + expect(subject.file_cache_path).to eq("/var/chef/cache") + 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) { "/path/to/file" } + + 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 diff --git a/test/unit/plugins/provisioners/chef/config/base_test.rb b/test/unit/plugins/provisioners/chef/config/base_test.rb new file mode 100644 index 000000000..4b018f11b --- /dev/null +++ b/test/unit/plugins/provisioners/chef/config/base_test.rb @@ -0,0 +1,78 @@ +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 "#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 "#prerelease" do + it "defaults to true" do + subject.finalize! + expect(subject.prerelease).to be(false) + 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 +end diff --git a/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb b/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb new file mode 100644 index 000000000..135c04213 --- /dev/null +++ b/test/unit/plugins/provisioners/chef/config/chef_apply_test.rb @@ -0,0 +1,85 @@ +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 diff --git a/test/unit/plugins/provisioners/chef/config/chef_client_test.rb b/test/unit/plugins/provisioners/chef/config/chef_client_test.rb new file mode 100644 index 000000000..17f2dc321 --- /dev/null +++ b/test/unit/plugins/provisioners/chef/config/chef_client_test.rb @@ -0,0 +1,136 @@ +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 /etc/chef/client.pem" do + subject.finalize! + expect(subject.client_key_path).to eq("/etc/chef/client.pem") + 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 + + context "when #delete_client is given" do + before { subject.delete_client = true } + + context "when knife does not exist" do + before do + allow(Vagrant::Util::Which) + .to receive(:which) + .with("knife") + .and_return(nil) + end + + it "returns an error" do + subject.finalize! + expect(errors).to eq([I18n.t("vagrant.chef_config_knife_not_found")]) + end + end + end + + context "when #delete_node is given" do + before { subject.delete_node = true } + + context "when knife does not exist" do + before do + allow(Vagrant::Util::Which) + .to receive(:which) + .with("knife") + .and_return(nil) + end + + it "returns an error" do + subject.finalize! + expect(errors).to eq([I18n.t("vagrant.chef_config_knife_not_found")]) + end + end + end + end +end diff --git a/test/unit/plugins/provisioners/chef/config/chef_solo_test.rb b/test/unit/plugins/provisioners/chef/config/chef_solo_test.rb new file mode 100644 index 000000000..d6a2a03af --- /dev/null +++ b/test/unit/plugins/provisioners/chef/config/chef_solo_test.rb @@ -0,0 +1,147 @@ +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 "#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 diff --git a/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb b/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb new file mode 100644 index 000000000..2f9cd82aa --- /dev/null +++ b/test/unit/plugins/provisioners/chef/config/chef_zero_test.rb @@ -0,0 +1,140 @@ +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 "#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 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 diff --git a/test/unit/plugins/provisioners/chef/omnibus_test.rb b/test/unit/plugins/provisioners/chef/omnibus_test.rb new file mode 100644 index 000000000..b053f94bc --- /dev/null +++ b/test/unit/plugins/provisioners/chef/omnibus_test.rb @@ -0,0 +1,55 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/provisioners/chef/omnibus") + +describe VagrantPlugins::Chef::Omnibus do + let(:prefix) { "curl -sL #{described_class.const_get(:OMNITRUCK)}" } + + let(:version) { :latest } + let(:prerelease) { false } + let(:download_path) { nil } + + let(:build_command) { described_class.build_command(version, prerelease, download_path) } + + context "when prerelease is given" do + let(:prerelease) { true } + + it "returns the correct command" do + expect(build_command).to eq("#{prefix} | sudo bash -s -- -p") + end + end + + context "when download_path is given" do + let(:download_path) { '/tmp/path/to/omnibuses' } + + it "returns the correct command" do + expect(build_command).to eq("#{prefix} | sudo bash -s -- -d \"/tmp/path/to/omnibuses\"") + end + end + + context "when version is :latest" do + let(:version) { :latest } + + it "returns the correct command" do + expect(build_command).to eq("#{prefix} | sudo bash") + end + end + + context "when version is a string" do + let(:version) { "1.2.3" } + + it "returns the correct command" do + expect(build_command).to eq("#{prefix} | sudo bash -s -- -v \"1.2.3\"") + end + end + + context "when prerelease and version and download_path are given" do + let(:version) { "1.2.3" } + let(:prerelease) { true } + let(:download_path) { "/some/path" } + + it "returns the correct command" do + expect(build_command).to eq("#{prefix} | sudo bash -s -- -p -v \"1.2.3\" -d \"/some/path\"") + end + end +end diff --git a/test/unit/plugins/provisioners/salt/config_test.rb b/test/unit/plugins/provisioners/salt/config_test.rb index c640a7497..24bf24796 100644 --- a/test/unit/plugins/provisioners/salt/config_test.rb +++ b/test/unit/plugins/provisioners/salt/config_test.rb @@ -60,5 +60,23 @@ describe VagrantPlugins::Salt::Config do 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 end end diff --git a/test/unit/plugins/pushes/atlas/config_test.rb b/test/unit/plugins/pushes/atlas/config_test.rb new file mode 100644 index 000000000..580a39ab0 --- /dev/null +++ b/test/unit/plugins/pushes/atlas/config_test.rb @@ -0,0 +1,190 @@ +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") } + + 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 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 diff --git a/test/unit/plugins/pushes/atlas/push_test.rb b/test/unit/plugins/pushes/atlas/push_test.rb new file mode 100644 index 000000000..72eb46057 --- /dev/null +++ b/test/unit/plugins/pushes/atlas/push_test.rb @@ -0,0 +1,176 @@ +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 + + 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 + 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 + dir = temporary_dir + + allow(Vagrant).to receive(:in_installer?).and_return(true) + allow(Vagrant).to receive(:installer_embedded_dir).and_return(dir.to_s) + + bin_path = dir.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 + dir = temporary_dir + + allow(Vagrant).to receive(:in_installer?).and_return(true) + allow(Vagrant).to receive(:installer_embedded_dir).and_return(dir.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 diff --git a/test/unit/plugins/pushes/ftp/adapter_test.rb b/test/unit/plugins/pushes/ftp/adapter_test.rb new file mode 100644 index 000000000..e929078cd --- /dev/null +++ b/test/unit/plugins/pushes/ftp/adapter_test.rb @@ -0,0 +1,111 @@ +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 = FakeFtp::Server.new(21212, 21213) + @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 + 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" + end + end +end diff --git a/test/unit/plugins/pushes/ftp/config_test.rb b/test/unit/plugins/pushes/ftp/config_test.rb new file mode 100644 index 000000000..f66eeb791 --- /dev/null +++ b/test/unit/plugins/pushes/ftp/config_test.rb @@ -0,0 +1,171 @@ +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 diff --git a/test/unit/plugins/pushes/ftp/push_test.rb b/test/unit/plugins/pushes/ftp/push_test.rb new file mode 100644 index 000000000..1f6773e24 --- /dev/null +++ b/test/unit/plugins/pushes/ftp/push_test.rb @@ -0,0 +1,297 @@ +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:51234", + username: "sethvargo", + password: "bacon", + passive: false, + secure: false, + destination: "/var/www/site", + ) + end + let(:ui) do + double("ui", + info: nil, + ) + end + + 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 = FakeFtp::Server.new(51234, 21213) + @server.start + + @dir = Dir.mktmpdir + + 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 + + + it "pushes the files to the server" do + subject.push + expect(server.files).to eq(%w(Gemfile data.txt)) + 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 + + 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 + + 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 diff --git a/test/unit/plugins/pushes/heroku/config_test.rb b/test/unit/plugins/pushes/heroku/config_test.rb new file mode 100644 index 000000000..e451ad152 --- /dev/null +++ b/test/unit/plugins/pushes/heroku/config_test.rb @@ -0,0 +1,99 @@ +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 diff --git a/test/unit/plugins/pushes/heroku/push_test.rb b/test/unit/plugins/pushes/heroku/push_test.rb new file mode 100644 index 000000000..c0337e41f --- /dev/null +++ b/test/unit/plugins/pushes/heroku/push_test.rb @@ -0,0 +1,324 @@ +require_relative "../../../base" + +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(:root_path) { "/handy/dandy" } + let(:dir) { "#{root_path}/#{config.dir}" } + + 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 prefixed with a star" do + let(:stdout) { "*bacon" } + + it "returns the correct name" do + expect(branch).to eq("bacon") + end + end + + context "when the branch is prefixed with a star space" do + let(:stdout) { "* bacon" } + + it "returns the correct name" do + expect(branch).to eq("bacon") + end + end + + 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 diff --git a/test/unit/plugins/pushes/local-exec/config_test.rb b/test/unit/plugins/pushes/local-exec/config_test.rb new file mode 100644 index 000000000..045872d2f --- /dev/null +++ b/test/unit/plugins/pushes/local-exec/config_test.rb @@ -0,0 +1,85 @@ +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 "#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 + 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 + 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 diff --git a/test/unit/plugins/pushes/local-exec/push_test.rb b/test/unit/plugins/pushes/local-exec/push_test.rb new file mode 100644 index 000000000..1efdfa3b1 --- /dev/null +++ b/test/unit/plugins/pushes/local-exec/push_test.rb @@ -0,0 +1,101 @@ +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, + ) + 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) + 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) + 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") + end + + it "executes the script" do + expect(subject).to receive(:execute_script!) + subject.execute_inline!("echo") + 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") + 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") + end + + it "calls execute!" do + expect(subject).to receive(:execute!) + .with(File.expand_path("foo.sh", env.root_path)) + subject.execute_script!("./foo.sh") + end + end + + describe "#execute!" do + it "safe execs" do + expect(Vagrant::Util::SafeExec).to receive(:exec) + expect { subject.execute! }.to_not raise_error + end + end +end diff --git a/test/unit/plugins/pushes/noop/config_test.rb b/test/unit/plugins/pushes/noop/config_test.rb new file mode 100644 index 000000000..05be6c3f8 --- /dev/null +++ b/test/unit/plugins/pushes/noop/config_test.rb @@ -0,0 +1,14 @@ +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 diff --git a/test/unit/support/dummy_provider.rb b/test/unit/support/dummy_provider.rb index 4fba81c9a..c16865ae2 100644 --- a/test/unit/support/dummy_provider.rb +++ b/test/unit/support/dummy_provider.rb @@ -22,8 +22,13 @@ module VagrantTests end def state - state_id = :running - state_id = state_file.read.to_sym if state_file.file? + 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 diff --git a/test/unit/support/shared/base_context.rb b/test/unit/support/shared/base_context.rb index 35c26788a..0f823948d 100644 --- a/test/unit/support/shared/base_context.rb +++ b/test/unit/support/shared/base_context.rb @@ -83,6 +83,18 @@ shared_context "unit" do return Pathname.new(d) 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| + allow(ENV).to receive(:[]) + .with(key.to_s) + .and_return(value.to_s) + 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 diff --git a/test/unit/vagrant/action/builtin/box_add_test.rb b/test/unit/vagrant/action/builtin/box_add_test.rb index 03e064920..ad3a56dfc 100644 --- a/test/unit/vagrant/action/builtin/box_add_test.rb +++ b/test/unit/vagrant/action/builtin/box_add_test.rb @@ -34,12 +34,14 @@ describe Vagrant::Action::Builtin::BoxAdd do FileChecksum.new(path, Digest::SHA1).checksum end - def with_web_server(path) + def with_web_server(path, **opts) tf = Tempfile.new("vagrant") tf.close + opts[:json_type] ||= "application/json" + mime_types = WEBrick::HTTPUtils::DefaultMimeTypes - mime_types.store "json", "application/json" + mime_types.store "json", opts[:json_type] port = 3838 server = WEBrick::HTTPServer.new( @@ -258,6 +260,51 @@ describe Vagrant::Action::Builtin::BoxAdd do end end + it "adds from HTTP URL with complex JSON mime type" do + box_path = iso_env.box2_file(:virtualbox) + tf = Tempfile.new(["vagrant", ".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 { |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) diff --git a/test/unit/vagrant/action/builtin/handle_box_test.rb b/test/unit/vagrant/action/builtin/handle_box_test.rb index 6b7d85818..17f32563f 100644 --- a/test/unit/vagrant/action/builtin/handle_box_test.rb +++ b/test/unit/vagrant/action/builtin/handle_box_test.rb @@ -106,4 +106,31 @@ describe Vagrant::Action::Builtin::HandleBox do subject.call(env) end end + + context "with a box with a checksum set" do + before do + machine.stub(box: 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 { |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 diff --git a/test/unit/vagrant/action/builtin/ssh_exec_test.rb b/test/unit/vagrant/action/builtin/ssh_exec_test.rb index 17f355497..94b42b99c 100644 --- a/test/unit/vagrant/action/builtin/ssh_exec_test.rb +++ b/test/unit/vagrant/action/builtin/ssh_exec_test.rb @@ -1,7 +1,5 @@ require File.expand_path("../../../../base", __FILE__) -require "vagrant/util/ssh" - describe Vagrant::Action::Builtin::SSHExec do let(:app) { lambda { |env| } } let(:env) { { machine: machine } } @@ -16,7 +14,6 @@ describe Vagrant::Action::Builtin::SSHExec do before(:each) do # Stub the methods so that even if we test incorrectly, no side # effects actually happen. - allow(ssh_klass).to receive(:check_key_permissions) allow(ssh_klass).to receive(:exec) end @@ -29,23 +26,6 @@ describe Vagrant::Action::Builtin::SSHExec do to raise_error(Vagrant::Errors::SSHNotReady) end - it "should check key permissions then exec" do - key_path = "/foo" - machine_ssh_info[:private_key_path] = [key_path] - - expect(ssh_klass).to receive(:check_key_permissions). - with(Pathname.new(key_path)). - once. - ordered - - expect(ssh_klass).to receive(:exec). - with(machine_ssh_info, nil). - once. - ordered - - described_class.new(app, env).call(env) - end - it "should exec with the SSH info in the env if given" do ssh_info = { foo: :bar } diff --git a/test/unit/vagrant/action/warden_test.rb b/test/unit/vagrant/action/warden_test.rb index b9758be7a..9c668e371 100644 --- a/test/unit/vagrant/action/warden_test.rb +++ b/test/unit/vagrant/action/warden_test.rb @@ -87,6 +87,6 @@ describe Vagrant::Action::Warden do expect { instance.call(data) }.to raise_error(SystemExit) # The recover should not have been called - expect(data.has_key?(:recover)).not_to be + expect(data.key?(:recover)).not_to be end end diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index a447ea9f1..c5cf8370d 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -253,7 +253,7 @@ describe Vagrant::Environment do 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(Vagrant.plugin("2", :provider)) + provider_cls = Class.new(VagrantTests::DummyProvider) register_plugin("2") do |p| p.provider(name, options) { provider_cls } @@ -798,13 +798,60 @@ VF end end - it "is VirtualBox if nothing else is usable" do + 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) do - expect(subject.default_provider).to eq(:virtualbox) + expect { subject.default_provider }.to raise_error( + Vagrant::Errors::NoDefaultProvider) + 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) do + expect(subject.default_provider).to eq(:foo) + 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) 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) do + expect(subject.default_provider(machine: :sub)).to eq(:bar) end end end @@ -921,6 +968,76 @@ VF 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 diff --git a/test/unit/vagrant/machine_test.rb b/test/unit/vagrant/machine_test.rb index d8e55e346..5da1be84f 100644 --- a/test/unit/vagrant/machine_test.rb +++ b/test/unit/vagrant/machine_test.rb @@ -7,11 +7,7 @@ describe Vagrant::Machine do include_context "unit" let(:name) { "foo" } - let(:provider) do - double("provider").tap do |obj| - obj.stub(_initialize: nil) - end - end + let(:provider) { new_provider_mock } let(:provider_cls) do obj = double("provider_cls") obj.stub(new: provider) @@ -46,6 +42,15 @@ describe Vagrant::Machine do subject { instance } + def new_provider_mock + double("provider").tap do |obj| + obj.stub(_initialize: nil) + obj.stub(machine_id_changed: 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, @@ -54,6 +59,17 @@ describe Vagrant::Machine do 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 + describe "communicator loading" do it "doesn't eager load SSH" do config.vm.communicator = :ssh @@ -86,8 +102,7 @@ describe Vagrant::Machine do received_machine = nil if !instance - instance = double("instance") - instance.stub(_initialize: nil) + instance = new_provider_mock end provider_cls = double("provider_cls") @@ -166,7 +181,7 @@ describe Vagrant::Machine do end it "should initialize the capabilities" do - instance = double("instance") + instance = new_provider_mock expect(instance).to receive(:_initialize).with { |p, m| expect(p).to eq(provider_name) expect(m.name).to eq(name) @@ -248,6 +263,17 @@ describe Vagrant::Machine do expect(foo).to eq(:bar) end + it "should pass any extra options to the environment as strings" do + action_name = :up + foo = nil + callable = lambda { |env| foo = env["foo"] } + + allow(provider).to receive(:action).with(action_name).and_return(callable) + instance.action(:up, "foo" => :bar) + + expect(foo).to eq(:bar) + end + it "should return the environment as a result" do action_name = :up callable = lambda { |env| env[:result] = "FOO" } @@ -410,6 +436,10 @@ describe Vagrant::Machine do 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") box.stub(name: "foo") @@ -451,11 +481,12 @@ describe Vagrant::Machine do 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") @@ -464,13 +495,16 @@ describe Vagrant::Machine do 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 "#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) @@ -480,9 +514,13 @@ describe Vagrant::Machine do 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| @@ -554,6 +592,30 @@ describe Vagrant::Machine do ]) 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 + + expect(ssh_klass).to receive(:check_key_permissions).once.with(Pathname.new(instance.env.default_private_key_path.to_s)) + 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" @@ -625,7 +687,7 @@ describe Vagrant::Machine do it "should query state from the provider" do state = Vagrant::MachineState.new(:id, "short", "long") - expect(provider).to receive(:state).and_return(state) + allow(provider).to receive(:state).and_return(state) expect(instance.state.id).to eq(:id) end @@ -649,18 +711,6 @@ describe Vagrant::Machine do expect(entry.state).to eq("short") env.machine_index.release(entry) end - - it "should set the ID to nil if the state is not created" do - state = Vagrant::MachineState.new( - Vagrant::MachineState::NOT_CREATED_ID, "short", "long") - - allow(provider).to receive(:machine_id_changed) - subject.id = "foo" - - expect(provider).to receive(:state).and_return(state) - expect(subject.state.id).to eq(Vagrant::MachineState::NOT_CREATED_ID) - expect(subject.id).to be_nil - end end describe "#with_ui" do diff --git a/test/unit/vagrant/plugin/v2/command_test.rb b/test/unit/vagrant/plugin/v2/command_test.rb index a111e9d5f..a6ac5d318 100644 --- a/test/unit/vagrant/plugin/v2/command_test.rb +++ b/test/unit/vagrant/plugin/v2/command_test.rb @@ -73,14 +73,16 @@ describe Vagrant::Plugin::V2::Command do to raise_error(Vagrant::Errors::NoEnvironmentError) end - it "should yield every VM in order is no name is given" do + it "should yield every VM in order if no name is given" do foo_vm = double("foo") foo_vm.stub(name: "foo", provider: :foobarbaz) foo_vm.stub(ui: Vagrant::UI::Silent.new) + foo_vm.stub(state: nil) bar_vm = double("bar") bar_vm.stub(name: "bar", provider: :foobarbaz) bar_vm.stub(ui: Vagrant::UI::Silent.new) + bar_vm.stub(state: nil) environment.stub(machine_names: [:foo, :bar]) allow(environment).to receive(:machine).with(:foo, environment.default_provider).and_return(foo_vm) @@ -106,6 +108,7 @@ describe Vagrant::Plugin::V2::Command do foo_vm = double("foo") foo_vm.stub(name: "foo", provider: :foobarbaz) foo_vm.stub(ui: Vagrant::UI::Silent.new) + foo_vm.stub(state: nil) allow(environment).to receive(:machine).with(:foo, environment.default_provider).and_return(foo_vm) @@ -114,12 +117,26 @@ describe Vagrant::Plugin::V2::Command do expect(vms).to eq([foo_vm]) end + it "calls state after yielding the vm to update the machine index" do + foo_vm = double("foo") + foo_vm.stub(name: "foo", provider: :foobarbaz) + foo_vm.stub(ui: Vagrant::UI::Silent.new) + foo_vm.stub(state: 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 "yields the given VM with proper provider if given" do foo_vm = double("foo") provider = :foobarbaz foo_vm.stub(name: "foo", provider: provider) foo_vm.stub(ui: Vagrant::UI::Silent.new) + foo_vm.stub(state: nil) allow(environment).to receive(:machine).with(:foo, provider).and_return(foo_vm) vms = [] @@ -144,6 +161,7 @@ describe Vagrant::Plugin::V2::Command do allow(environment).to receive(:machine).with(name, provider).and_return(vmware_vm) vmware_vm.stub(name: name, provider: provider) vmware_vm.stub(ui: Vagrant::UI::Silent.new) + vmware_vm.stub(state: nil) vms = [] instance.with_target_vms(name.to_s) { |vm| vms << vm } @@ -158,6 +176,7 @@ describe Vagrant::Plugin::V2::Command do environment.stub(active_machines: [[name, provider]]) allow(environment).to receive(:machine).with(name, provider).and_return(vmware_vm) vmware_vm.stub(name: name, provider: provider, ui: Vagrant::UI::Silent.new) + vmware_vm.stub(state: nil) vms = [] instance.with_target_vms(name.to_s, provider: provider) { |vm| vms << vm } @@ -171,6 +190,7 @@ describe Vagrant::Plugin::V2::Command do allow(environment).to receive(:machine).with(name, environment.default_provider).and_return(machine) machine.stub(name: name, provider: environment.default_provider) machine.stub(ui: Vagrant::UI::Silent.new) + machine.stub(state: nil) results = [] instance.with_target_vms(name.to_s) { |m| results << m } @@ -188,6 +208,7 @@ describe Vagrant::Plugin::V2::Command do environment.stub(primary_machine_name: name) vmware_vm.stub(name: name, provider: provider) vmware_vm.stub(ui: Vagrant::UI::Silent.new) + vmware_vm.stub(state: nil) vms = [] instance.with_target_vms(nil, single_target: true) { |vm| vms << vm } @@ -204,6 +225,7 @@ describe Vagrant::Plugin::V2::Command do environment.stub(primary_machine_name: name) machine.stub(name: name, provider: environment.default_provider) machine.stub(ui: Vagrant::UI::Silent.new) + machine.stub(state: nil) vms = [] instance.with_target_vms(nil, single_target: true) { |vm| vms << machine } diff --git a/test/unit/vagrant/plugin/v2/manager_test.rb b/test/unit/vagrant/plugin/v2/manager_test.rb index 45ebd5a0c..3ddbf00e9 100644 --- a/test/unit/vagrant/plugin/v2/manager_test.rb +++ b/test/unit/vagrant/plugin/v2/manager_test.rb @@ -189,6 +189,42 @@ describe Vagrant::Plugin::V2::Manager do 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" } diff --git a/test/unit/vagrant/plugin/v2/plugin_test.rb b/test/unit/vagrant/plugin/v2/plugin_test.rb index 944d38f9f..76ad40993 100644 --- a/test/unit/vagrant/plugin/v2/plugin_test.rb +++ b/test/unit/vagrant/plugin/v2/plugin_test.rb @@ -322,6 +322,42 @@ describe Vagrant::Plugin::V2::Plugin do 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 diff --git a/test/unit/vagrant/registry_test.rb b/test/unit/vagrant/registry_test.rb index d12f46ffe..f177a6a1c 100644 --- a/test/unit/vagrant/registry_test.rb +++ b/test/unit/vagrant/registry_test.rb @@ -90,6 +90,39 @@ describe Vagrant::Registry do 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 diff --git a/test/unit/vagrant/util/hash_with_indifferent_access_test.rb b/test/unit/vagrant/util/hash_with_indifferent_access_test.rb index 397773eed..fdebeb374 100644 --- a/test/unit/vagrant/util/hash_with_indifferent_access_test.rb +++ b/test/unit/vagrant/util/hash_with_indifferent_access_test.rb @@ -22,7 +22,7 @@ describe Vagrant::Util::HashWithIndifferentAccess do it "allows indifferent key lookup" do instance["foo"] = "bar" expect(instance.key?(:foo)).to be - expect(instance.has_key?(:foo)).to be + expect(instance.key?(:foo)).to be expect(instance.include?(:foo)).to be expect(instance.member?(:foo)).to be end diff --git a/test/unit/vagrant/util/keypair_test.rb b/test/unit/vagrant/util/keypair_test.rb new file mode 100644 index 000000000..30ab76eba --- /dev/null +++ b/test/unit/vagrant/util/keypair_test.rb @@ -0,0 +1,34 @@ +require "openssl" + +require File.expand_path("../../../base", __FILE__) + +require "vagrant/util/keypair" + +describe Vagrant::Util::Keypair 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 diff --git a/test/unit/vagrant/vagrantfile_test.rb b/test/unit/vagrant/vagrantfile_test.rb index 8e5b3584c..1354587a2 100644 --- a/test/unit/vagrant/vagrantfile_test.rb +++ b/test/unit/vagrant/vagrantfile_test.rb @@ -26,7 +26,7 @@ describe Vagrant::Vagrantfile do # A helper to register a provider for use in tests. def register_provider(name, config_class=nil, options=nil) - provider_cls = Class.new(Vagrant.plugin("2", :provider)) do + provider_cls = Class.new(VagrantTests::DummyProvider) do if options && options[:unusable] def self.usable?(raise_error=false) raise Vagrant::Errors::VagrantError if raise_error @@ -149,6 +149,21 @@ describe Vagrant::Vagrantfile do 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") diff --git a/test/unit/vagrant_test.rb b/test/unit/vagrant_test.rb index 03bb1126e..ec317a628 100644 --- a/test/unit/vagrant_test.rb +++ b/test/unit/vagrant_test.rb @@ -74,6 +74,17 @@ describe Vagrant do 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" + Vagrant::Plugin::Manager.instance.stub(installed_specs: 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 diff --git a/vagrant.gemspec b/vagrant.gemspec index 4a53983dc..0e68a6d08 100644 --- a/vagrant.gemspec +++ b/vagrant.gemspec @@ -15,28 +15,29 @@ Gem::Specification.new do |s| s.required_rubygems_version = ">= 1.3.6" s.rubyforge_project = "vagrant" - s.add_dependency "bundler", ">= 1.5.2" -#, "< 1.7.0" + s.add_dependency "bundler", ">= 1.5.2", "< 1.8.0" s.add_dependency "childprocess", "~> 0.5.0" s.add_dependency "erubis", "~> 2.7.0" - s.add_dependency "i18n", "~> 0.6.0" - s.add_dependency "listen", "~> 2.7.1" + s.add_dependency "i18n", ">= 0.6.0", "<= 0.8.0" + s.add_dependency "listen", "~> 2.8.0" s.add_dependency "hashicorp-checkpoint", "~> 0.1.1" s.add_dependency "log4r", "~> 1.1.9", "< 1.1.11" s.add_dependency "net-ssh", ">= 2.6.6", "< 2.10.0" + s.add_dependency "net-sftp", "~> 2.1" s.add_dependency "net-scp", "~> 1.1.0" s.add_dependency "rb-kqueue", "~> 0.2.0" + s.add_dependency "rest-client", ">= 1.6.0", "< 2.0" s.add_dependency "wdm", "~> 0.1.0" - s.add_dependency "winrm", "~> 1.1.3" + s.add_dependency "winrm", "~> 1.3" + s.add_dependency "winrm-fs", "~> 0.1.0" # We lock this down to avoid compilation issues. s.add_dependency "nokogiri", "= 1.6.3.1" s.add_development_dependency "rake" - s.add_development_dependency "contest", ">= 0.1.2" - s.add_development_dependency "minitest", "~> 2.5.1" - s.add_development_dependency "mocha" s.add_development_dependency "rspec", "~> 2.14.0" + s.add_development_dependency "webmock", "~> 1.20" + s.add_development_dependency "fake_ftp", "~> 0.1" # 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 diff --git a/version.txt b/version.txt index 9f05f9f2c..f8a696c8d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.6.5 +1.7.2 diff --git a/website/docs/Gemfile b/website/docs/Gemfile index 5f8d1e016..4a2399f5d 100644 --- a/website/docs/Gemfile +++ b/website/docs/Gemfile @@ -5,7 +5,7 @@ gem "middleman", "~> 3.0.6" gem "middleman-minify-html", "~> 3.0.0" gem "rack-contrib", "~> 1.1.0" gem "redcarpet", "~> 2.2.2" -gem "rb-inotify", "~> 0.9" +gem "rb-inotify", "~> 0.9", require: false gem "therubyracer", "~> 0.12.0" gem "thin", "~> 1.5.0" diff --git a/website/docs/Gemfile.lock b/website/docs/Gemfile.lock index c88f8653b..b0ae81928 100644 --- a/website/docs/Gemfile.lock +++ b/website/docs/Gemfile.lock @@ -1,42 +1,44 @@ GEM remote: https://rubygems.org/ specs: - POpen4 (0.1.4) - Platform (>= 0.4.0) - open4 - Platform (0.4.0) activesupport (3.2.13) i18n (= 0.6.1) multi_json (~> 1.0) - chunky_png (1.2.8) + chunky_png (1.3.4) coffee-script (2.2.0) coffee-script-source execjs coffee-script-source (1.3.3) commonjs (0.2.7) - compass (0.12.2) + compass (1.0.3) chunky_png (~> 1.2) - fssm (>= 0.2.7) - sass (~> 3.1) - daemons (1.1.9) - eventmachine (1.0.3) - execjs (1.4.0) + compass-core (~> 1.0.2) + compass-import-once (~> 1.0.5) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + sass (>= 3.3.13, < 3.5) + compass-core (1.0.3) multi_json (~> 1.0) - ffi (1.9.3) - fssm (0.2.10) - haml (4.0.3) + sass (>= 3.3.0, < 3.5) + compass-import-once (1.0.5) + sass (>= 3.2, < 3.5) + daemons (1.1.9) + eventmachine (1.0.7) + execjs (1.4.1) + multi_json (~> 1.0) + ffi (1.9.6) + haml (4.0.6) tilt - highline (1.6.19) + highline (1.6.21) hike (1.2.3) - htmlcompressor (0.0.7) - yui-compressor (~> 0.9.6) + htmlcompressor (0.1.2) http_router (0.10.2) rack (>= 1.0.0) url_mount (~> 0.2.1) i18n (0.6.1) less (2.2.2) commonjs (~> 0.2.6) - libv8 (3.16.14.3) + libv8 (3.16.14.7) listen (0.7.3) maruku (0.6.1) syntax (>= 1.0.0) @@ -74,8 +76,7 @@ GEM sprockets (~> 2.1) sprockets-helpers (~> 1.0.0) sprockets-sass (~> 1.0.0) - multi_json (1.8.0) - open4 (1.3.0) + multi_json (1.10.1) padrino-core (0.10.7) activesupport (~> 3.2.0) http_router (~> 0.10.2) @@ -88,32 +89,32 @@ GEM rack (1.4.5) rack-contrib (1.1.0) rack (>= 0.9.1) - rack-protection (1.5.0) + rack-protection (1.5.3) rack - rack-test (0.6.2) + rack-test (0.6.3) rack (>= 1.0) - rb-fsevent (0.9.3) - rb-inotify (0.9.3) + rb-fsevent (0.9.4) + rb-inotify (0.9.5) ffi (>= 0.5.0) redcarpet (2.2.2) ref (1.0.5) - sass (3.2.10) + sass (3.4.13) sinatra (1.3.6) rack (~> 1.4) rack-protection (~> 1.3) tilt (~> 1.3, >= 1.3.3) - sprockets (2.10.0) + sprockets (2.12.3) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) sprockets-helpers (1.0.1) sprockets (~> 2.0) - sprockets-sass (1.0.1) + sprockets-sass (1.0.3) sprockets (~> 2.0) tilt (~> 1.1) - syntax (1.0.0) - therubyracer (0.12.0) + syntax (1.2.0) + therubyracer (0.12.1) libv8 (~> 3.16.14.0) ref thin (1.5.1) @@ -127,8 +128,6 @@ GEM multi_json (~> 1.3) url_mount (0.2.1) rack - yui-compressor (0.9.6) - POpen4 (>= 0.1.4) PLATFORMS ruby diff --git a/website/docs/Vagrantfile b/website/docs/Vagrantfile index 7e26cbed8..7ce24fe70 100644 --- a/website/docs/Vagrantfile +++ b/website/docs/Vagrantfile @@ -1,9 +1,6 @@ # -*- mode: ruby -*- # vi: set ft=ruby : -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" - $script = <